Skip to content

Commit e366a0c

Browse files
authored
Krakenv2 migration (#72)
* feat(krakenv2-migration): added mappers * feat(krakenv2-migration): mapping only updates, ignore snapshots * feat(krakenv2-migration): added streaming * feat(krakenv2-migration): added switch tests * feat(krakenv2-migration): so we will habdle only bbo events * feat(krakenv2-migration): added instrument channel * feat(krakenv2-migration): updates switch date
1 parent 33d263c commit e366a0c

7 files changed

Lines changed: 438 additions & 22 deletions

File tree

src/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const DERIBIT_CHANNELS = [
124124
'instrument.state.any'
125125
] as const
126126

127-
const KRAKEN_CHANNELS = ['trade', 'ticker', 'book', 'spread'] as const
127+
const KRAKEN_CHANNELS = ['trade', 'ticker', 'book', 'spread', 'instrument'] as const
128128

129129
const OKEX_CHANNELS = [
130130
'spot/trade',

src/mappers/kraken.ts

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import { asNonZeroNumberOrUndefined, upperCaseSymbols } from '../handy.ts'
1+
import { asNonZeroNumberOrUndefined, parseμs, upperCaseSymbols } from '../handy.ts'
22
import { BookChange, BookTicker, Trade } from '../types.ts'
33
import { Mapper } from './mapper.ts'
4-
import { exchangeMappers } from './registry.ts'
4+
import { exchangeMappers, mapper } from './registry.ts'
55

66
// https://www.kraken.com/features/websocket-api
7+
const KRAKEN_V2_API_SWITCH_DATE = new Date('2026-07-10T00:00:00.000Z')
78

89
export const krakenMappers = exchangeMappers({
910
kraken: {
10-
trades: () => krakenTradesMapper,
11-
bookChanges: () => krakenBookChangeMapper,
12-
bookTickers: () => krakenBookTickerMapper
11+
trades: mapper([{ until: KRAKEN_V2_API_SWITCH_DATE, use: () => krakenTradesMapper }, { use: () => new KrakenV2TradesMapper() }]),
12+
bookChanges: mapper([
13+
{ until: KRAKEN_V2_API_SWITCH_DATE, use: () => krakenBookChangeMapper },
14+
{ use: () => new KrakenV2BookChangeMapper() }
15+
]),
16+
bookTickers: mapper([
17+
{ until: KRAKEN_V2_API_SWITCH_DATE, use: () => krakenBookTickerMapper },
18+
{ use: () => new KrakenV2BookTickerMapper() }
19+
])
1320
}
1421
})
1522

@@ -226,3 +233,153 @@ type KrakenSpread = [
226233
'spread',
227234
'XBT/USD'
228235
]
236+
237+
/** @see https://docs.kraken.com/api/docs/websocket-v2/trade */
238+
class KrakenV2TradesMapper implements Mapper<'kraken', Trade> {
239+
canHandle(message: KrakenV2Trades) {
240+
return message.channel === 'trade' && message.type === 'update'
241+
}
242+
243+
getFilters(symbols?: string[]) {
244+
return [{ channel: 'trade', symbols } as const]
245+
}
246+
247+
*map(message: KrakenV2Trades, localTimestamp: Date): IterableIterator<Trade> {
248+
for (const trade of message.data) {
249+
const timestamp = new Date(trade.timestamp)
250+
timestamp.μs = parseμs(trade.timestamp)
251+
252+
yield {
253+
type: 'trade',
254+
symbol: trade.symbol,
255+
exchange: 'kraken',
256+
id: trade.trade_id.toString(),
257+
price: trade.price,
258+
amount: trade.qty,
259+
side: trade.side,
260+
timestamp,
261+
localTimestamp
262+
}
263+
}
264+
}
265+
}
266+
267+
/** @see https://docs.kraken.com/api/docs/websocket-v2/book */
268+
class KrakenV2BookChangeMapper implements Mapper<'kraken', BookChange> {
269+
canHandle(message: KrakenV2Book) {
270+
return message.channel === 'book'
271+
}
272+
273+
getFilters(symbols?: string[]) {
274+
return [{ channel: 'book', symbols } as const]
275+
}
276+
277+
*map(message: KrakenV2Book, localTimestamp: Date): IterableIterator<BookChange> {
278+
for (const book of message.data) {
279+
const timestamp = new Date(book.timestamp)
280+
timestamp.μs = parseμs(book.timestamp)
281+
282+
yield {
283+
type: 'book_change',
284+
symbol: book.symbol,
285+
exchange: 'kraken',
286+
isSnapshot: message.type === 'snapshot',
287+
bids: book.bids?.map(this.mapBookLevel) ?? [],
288+
asks: book.asks?.map(this.mapBookLevel) ?? [],
289+
timestamp,
290+
localTimestamp
291+
}
292+
}
293+
}
294+
295+
private mapBookLevel(level: KrakenV2BookLevel) {
296+
return {
297+
price: level.price,
298+
amount: level.qty
299+
}
300+
}
301+
}
302+
303+
/** @see https://docs.kraken.com/api/docs/websocket-v2/ticker */
304+
class KrakenV2BookTickerMapper implements Mapper<'kraken', BookTicker> {
305+
canHandle(message: KrakenV2Ticker) {
306+
return message.channel === 'ticker'
307+
}
308+
309+
getFilters(symbols?: string[]) {
310+
return [{ channel: 'ticker', symbols } as const]
311+
}
312+
313+
*map(message: KrakenV2Ticker, localTimestamp: Date): IterableIterator<BookTicker> {
314+
for (const tickerMessage of message.data) {
315+
const timestamp = new Date(tickerMessage.timestamp)
316+
timestamp.μs = parseμs(tickerMessage.timestamp)
317+
318+
yield {
319+
type: 'book_ticker',
320+
symbol: tickerMessage.symbol,
321+
exchange: 'kraken',
322+
askAmount: tickerMessage.ask_qty === 0 ? undefined : tickerMessage.ask_qty,
323+
askPrice: tickerMessage.ask === 0 ? undefined : tickerMessage.ask,
324+
bidPrice: tickerMessage.bid === 0 ? undefined : tickerMessage.bid,
325+
bidAmount: tickerMessage.bid_qty === 0 ? undefined : tickerMessage.bid_qty,
326+
timestamp,
327+
localTimestamp
328+
}
329+
}
330+
}
331+
}
332+
333+
type KrakenV2Message<Channel extends string, Data> = {
334+
channel: Channel
335+
type: 'snapshot' | 'update'
336+
data: Data[]
337+
}
338+
339+
type KrakenV2Trades = KrakenV2Message<
340+
'trade',
341+
{
342+
symbol: string
343+
side: 'buy' | 'sell'
344+
price: number
345+
qty: number
346+
ord_type?: string
347+
trade_id: number
348+
timestamp: string
349+
}
350+
>
351+
352+
type KrakenV2Book = KrakenV2Message<
353+
'book',
354+
{
355+
symbol: string
356+
bids?: KrakenV2BookLevel[]
357+
asks?: KrakenV2BookLevel[]
358+
checksum?: number
359+
timestamp: string
360+
}
361+
>
362+
363+
type KrakenV2BookLevel = {
364+
price: number
365+
qty: number
366+
}
367+
368+
type KrakenV2Ticker = KrakenV2Message<
369+
'ticker',
370+
{
371+
symbol: string
372+
bid: number
373+
bid_qty: number
374+
ask: number
375+
ask_qty: number
376+
last: number
377+
volume: number
378+
vwap: number
379+
low: number
380+
high: number
381+
change: number
382+
change_pct: number
383+
timestamp: string
384+
}
385+
>

src/realtimefeeds/kraken.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,47 @@ import { Filter } from '../types.ts'
22
import { RealTimeFeedBase } from './realtimefeed.ts'
33

44
export class KrakenRealTimeFeed extends RealTimeFeedBase {
5-
protected wssURL = 'wss://ws.kraken.com'
5+
private readonly channels = new Set(['trade', 'book', 'ticker', 'instrument'])
6+
protected wssURL = 'wss://ws.kraken.com/v2'
67

78
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
8-
return filters.map((filter) => {
9-
if (!filter.symbols || filter.symbols.length === 0) {
9+
return filters.flatMap(({ channel, symbols }): any[] => {
10+
if (channel !== 'instrument' && (!symbols || symbols.length === 0)) {
1011
throw new Error('KrakenRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
1112
}
1213

13-
let depth = undefined
14+
if (!this.channels.has(channel)) {
15+
throw new Error(`KrakenRealTimeFeed unsupported channel ${channel}`)
16+
}
1417

15-
if (filter.channel === 'book') {
16-
depth = 1000
18+
if (channel === 'instrument') {
19+
return [
20+
{
21+
method: 'subscribe',
22+
params: { channel, include_tokenized_assets: true }
23+
}
24+
]
1725
}
1826

19-
return {
20-
event: 'subscribe',
21-
pair: filter.symbols,
22-
subscription: {
23-
name: filter.channel,
24-
depth
27+
return [
28+
{
29+
method: 'subscribe',
30+
params: {
31+
channel,
32+
symbol: symbols,
33+
...(channel === 'ticker' ? { event_trigger: 'bbo' } : {}),
34+
...(channel === 'book' ? { depth: 1000 } : {})
35+
}
2536
}
26-
}
37+
]
2738
})
2839
}
2940

3041
protected messageIsError(message: any): boolean {
31-
return message.errorMessage !== undefined
42+
return message.errorMessage !== undefined || message.success === false
3243
}
3344

3445
protected messageIsHeartbeat(message: any): boolean {
35-
return message.event === 'heartbeat'
46+
return message.event === 'heartbeat' || message.channel === 'heartbeat'
3647
}
3748
}

test/__snapshots__/mappers.test.ts.snap

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10761,6 +10761,85 @@ exports[`mappers map kraken messages 7`] = `
1076110761

1076210762
exports[`mappers map kraken messages 8`] = `[]`;
1076310763

10764+
exports[`mappers map kraken v2 messages 1`] = `[]`;
10765+
10766+
exports[`mappers map kraken v2 messages 2`] = `
10767+
[
10768+
{
10769+
"amount": 0.25,
10770+
"exchange": "kraken",
10771+
"id": "123",
10772+
"localTimestamp": 2026-07-10T00:00:03.275Z,
10773+
"price": 100.1,
10774+
"side": "buy",
10775+
"symbol": "BTC/USD",
10776+
"timestamp": 2026-07-10T00:00:00.123456Z,
10777+
"type": "trade",
10778+
},
10779+
]
10780+
`;
10781+
10782+
exports[`mappers map kraken v2 messages 3`] = `
10783+
[
10784+
{
10785+
"asks": [
10786+
{
10787+
"amount": 0.8,
10788+
"price": 200.1,
10789+
},
10790+
],
10791+
"bids": [
10792+
{
10793+
"amount": 1.2,
10794+
"price": 199.9,
10795+
},
10796+
],
10797+
"exchange": "kraken",
10798+
"isSnapshot": true,
10799+
"localTimestamp": 2026-07-10T00:00:03.275Z,
10800+
"symbol": "AAPLx/USD",
10801+
"timestamp": 2026-07-10T00:00:01.123457Z,
10802+
"type": "book_change",
10803+
},
10804+
]
10805+
`;
10806+
10807+
exports[`mappers map kraken v2 messages 4`] = `
10808+
[
10809+
{
10810+
"asks": [],
10811+
"bids": [
10812+
{
10813+
"amount": 0.5,
10814+
"price": 99.9,
10815+
},
10816+
],
10817+
"exchange": "kraken",
10818+
"isSnapshot": false,
10819+
"localTimestamp": 2026-07-10T00:00:03.275Z,
10820+
"symbol": "BTC/USD",
10821+
"timestamp": 2026-07-10T00:00:01.223456Z,
10822+
"type": "book_change",
10823+
},
10824+
]
10825+
`;
10826+
10827+
exports[`mappers map kraken v2 messages 5`] = `
10828+
[
10829+
{
10830+
"askAmount": 2.3,
10831+
"askPrice": 100.1,
10832+
"bidAmount": 1.2,
10833+
"bidPrice": 99.9,
10834+
"exchange": "kraken",
10835+
"localTimestamp": 2026-07-10T00:00:03.275Z,
10836+
"symbol": "BTC/USD",
10837+
"timestamp": 2026-07-10T00:00:02.123456Z,
10838+
"type": "book_ticker",
10839+
},
10840+
]
10841+
`;
10842+
1076410843
exports[`mappers map okcoin messages 1`] = `
1076510844
[
1076610845
{

0 commit comments

Comments
 (0)