Skip to content

Commit cf7ffea

Browse files
committed
add instrument symbol discovery helper
1 parent 247b3b3 commit cf7ffea

3 files changed

Lines changed: 124 additions & 4 deletions

File tree

src/instrumentinfo.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,43 @@ async function getInstrumentInfoForExchange(exchange: Exchange, filterOrSymbol?:
5151
}
5252
}
5353

54-
type InstrumentInfoFilter = {
54+
export async function findInstrumentSymbols(
55+
exchanges: Exchange[],
56+
filter: InstrumentInfoFilter,
57+
selector: InstrumentSymbolSelector = 'id'
58+
): Promise<InstrumentSymbols[]> {
59+
if (selector !== 'id' && selector !== 'datasetId') {
60+
throw new Error("Invalid selector. Supported values are 'id' and 'datasetId'.")
61+
}
62+
63+
return await Promise.all(
64+
exchanges.map(async (exchange) => {
65+
const instruments = (await getInstrumentInfoForExchange(exchange, filter)) as InstrumentInfo[]
66+
67+
return {
68+
exchange,
69+
symbols: instruments.map((instrument) => (selector === 'datasetId' ? instrument.datasetId ?? instrument.id : instrument.id))
70+
}
71+
})
72+
)
73+
}
74+
75+
export type InstrumentSymbolSelector = 'id' | 'datasetId'
76+
77+
export type InstrumentSymbols = {
78+
exchange: Exchange
79+
symbols: string[]
80+
}
81+
82+
export type InstrumentInfoFilter = {
5583
baseCurrency?: string | string[]
5684
quoteCurrency?: string | string[]
5785
type?: SymbolType | SymbolType[]
5886
contractType?: ContractType | ContractType[]
87+
underlyingType?: UnderlyingType | UnderlyingType[]
5988
active?: boolean
89+
availableSince?: string
90+
availableTo?: string
6091
}
6192

6293
export type ContractType =
@@ -76,6 +107,8 @@ export type ContractType =
76107
| 'repo'
77108
| 'index'
78109

110+
export type UnderlyingType = 'native' | 'equity' | 'commodity' | 'fixed_income' | 'fx' | 'index' | 'pre_market'
111+
79112
export interface InstrumentInfo {
80113
/** symbol id */
81114
id: string
@@ -104,6 +137,8 @@ export interface InstrumentInfo {
104137
expirationType?: 'daily' | 'weekly' | 'next_week' | 'quarter' | 'next_quarter'
105138
/** the underlying index for derivatives */
106139
underlyingIndex?: string
140+
/** underlying asset class */
141+
underlyingType?: UnderlyingType
107142
/** price tick size, price precision can be calculated from it */
108143
priceIncrement: number
109144
/** amount tick size, amount/size precision can be calculated from it */

test/instrumentinfo.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { jest } from '@jest/globals'
2+
import { findInstrumentSymbols, getInstrumentInfo, init, type InstrumentInfoFilter } from '../dist/index.js'
3+
import { describeLive } from './live.js'
4+
5+
function createFetchMock(...responses: Response[]) {
6+
const fetchMock = jest.fn<typeof fetch>()
7+
8+
for (const response of responses) {
9+
fetchMock.mockResolvedValueOnce(response)
10+
}
11+
12+
return fetchMock
13+
}
14+
15+
describe('findInstrumentSymbols', () => {
16+
const originalFetch = global.fetch
17+
18+
afterEach(() => {
19+
global.fetch = originalFetch
20+
init()
21+
jest.restoreAllMocks()
22+
})
23+
24+
test('returns dataset ids and falls back to id when datasetId is omitted', async () => {
25+
global.fetch = createFetchMock(
26+
new Response(JSON.stringify([{ id: 'btcusdt', datasetId: 'BTCUSDT' }, { id: 'ethusdt' }]), { status: 200 })
27+
)
28+
init({ endpoint: 'https://example.com/v1' })
29+
30+
await expect(findInstrumentSymbols(['binance'], { active: true }, 'datasetId')).resolves.toEqual([
31+
{ exchange: 'binance', symbols: ['BTCUSDT', 'ethusdt'] }
32+
])
33+
})
34+
35+
test('rejects invalid selector values at runtime', async () => {
36+
await expect(findInstrumentSymbols(['binance'], { active: true }, 'nativeId' as any)).rejects.toThrow('Invalid selector')
37+
})
38+
})
39+
40+
describeLive('instrument info live', () => {
41+
const bitmexXbtUsdPerpetualFilter: InstrumentInfoFilter = {
42+
baseCurrency: 'BTC',
43+
quoteCurrency: 'USD',
44+
type: 'perpetual',
45+
contractType: 'inverse_perpetual',
46+
underlyingType: 'native',
47+
active: true
48+
}
49+
50+
afterEach(() => {
51+
init()
52+
})
53+
54+
test('fetches public BitMEX instrument metadata', async () => {
55+
const instrument = await getInstrumentInfo('bitmex', 'XBTUSD')
56+
57+
expect(instrument).toMatchObject({
58+
id: 'XBTUSD',
59+
datasetId: 'XBTUSD',
60+
exchange: 'bitmex',
61+
baseCurrency: 'BTC',
62+
quoteCurrency: 'USD',
63+
type: 'perpetual',
64+
contractType: 'inverse_perpetual',
65+
underlyingType: 'native',
66+
active: true
67+
})
68+
})
69+
70+
test('finds public BitMEX symbol ids by metadata filter', async () => {
71+
await expect(findInstrumentSymbols(['bitmex'], bitmexXbtUsdPerpetualFilter)).resolves.toEqual([
72+
{ exchange: 'bitmex', symbols: ['XBTUSD'] }
73+
])
74+
})
75+
76+
test('finds public BitMEX dataset ids by metadata filter', async () => {
77+
await expect(findInstrumentSymbols(['bitmex'], bitmexXbtUsdPerpetualFilter, 'datasetId')).resolves.toEqual([
78+
{ exchange: 'bitmex', symbols: ['XBTUSD'] }
79+
])
80+
})
81+
})

test/package-exports.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ describe('package exports', () => {
2020
path.join(tempDir, 'import-test.mjs'),
2121
`
2222
import * as tardis from 'tardis-dev'
23-
import { replay, stream } from 'tardis-dev'
23+
import { findInstrumentSymbols, replay, stream } from 'tardis-dev'
2424
console.log(JSON.stringify({
2525
namespaceReplay: typeof tardis.replay,
2626
namespaceStream: typeof tardis.stream,
27+
namespaceFindInstrumentSymbols: typeof tardis.findInstrumentSymbols,
2728
hasDefault: Object.prototype.hasOwnProperty.call(tardis, 'default'),
2829
namedReplay: typeof replay,
29-
namedStream: typeof stream
30+
namedStream: typeof stream,
31+
namedFindInstrumentSymbols: typeof findInstrumentSymbols
3032
}))
3133
`.trim() + '\n'
3234
)
@@ -41,9 +43,11 @@ describe('package exports', () => {
4143
expect(importOutput).toEqual({
4244
namespaceReplay: 'function',
4345
namespaceStream: 'function',
46+
namespaceFindInstrumentSymbols: 'function',
4447
hasDefault: false,
4548
namedReplay: 'function',
46-
namedStream: 'function'
49+
namedStream: 'function',
50+
namedFindInstrumentSymbols: 'function'
4751
})
4852

4953
rmSync(tempDir, { force: true, recursive: true })

0 commit comments

Comments
 (0)