Skip to content

Commit d5bb391

Browse files
authored
feat(example-improvements): reshaped example into generic script allows to run stream and reply tests for any exchange/channel/symbol (#70)
1 parent b67a3da commit d5bb391

2 files changed

Lines changed: 154 additions & 31 deletions

File tree

ADD_NEW_EXCHANGE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,27 @@ Mapper tests should cover:
9292

9393
Run tests and validation — see AGENTS.md for the full checklist.
9494

95+
### 5. Validate live and replay output
96+
97+
After adding or changing a real-time feed, mapper, filter, or upstream API version branch, validate the implementation against actual data in addition to mapper snapshots. Build first because `example.js` imports `dist/index.js`.
98+
99+
```bash
100+
npm run build
101+
```
102+
103+
Use `example.js` to check native and normalized output for at least one active symbol and each channel or normalized data type touched by the change:
104+
105+
```bash
106+
node example.js stream <exchange> <symbol> <channel>
107+
node example.js --normalized stream <exchange> <symbol> <data-type>
108+
node example.js replay <exchange> <symbol> <channel> <from> <to>
109+
node example.js --normalized replay <exchange> <symbol> <data-type> <from> <to>
110+
```
111+
112+
For order book changes, confirm the expected snapshot behavior: snapshot-capable feeds should emit an initial `book_change` with `isSnapshot=true`, then deltas with `isSnapshot=false` when the exchange contract provides deltas. For trades, tickers, liquidations, and other mapped data types, confirm that messages are produced for the requested symbol and that key fields are populated from the documented exchange semantics.
113+
114+
If a channel is intentionally state-only or should emit nothing for a message variant, validate the paired channel or later payload that should produce the normalized output. Record any skipped live or replay checks in the PR notes.
115+
95116
## Decision Points
96117

97118
- **Date-based mapper versioning** — If the exchange changed its API format at some point, define the switch in the exchange mapper registry with `mapper([{ until, use }, { use }])`. Look at existing `*Mappers` exports in `src/mappers/{exchange}.ts` files for the pattern.

example.js

Lines changed: 133 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,137 @@
1-
import { replayNormalized, streamNormalized, normalizeTrades, compute, computeTradeBars } from 'tardis-dev'
2-
3-
const historicalMessages = replayNormalized(
4-
{
5-
exchange: 'binance',
6-
symbols: ['btcusdt'],
7-
from: '2024-03-01',
8-
to: '2024-03-02'
9-
},
10-
normalizeTrades
11-
)
12-
13-
const realTimeMessages = streamNormalized(
14-
{
15-
exchange: 'binance',
16-
symbols: ['btcusdt']
17-
},
18-
normalizeTrades
19-
)
20-
21-
async function produceVolumeBasedTradeBars(messages) {
22-
// aggregate by 1 BTC traded volume
23-
const withVolumeTradeBars = compute(messages, computeTradeBars({ kind: 'volume', interval: 1 }))
24-
25-
for await (const message of withVolumeTradeBars) {
26-
if (message.type === 'trade_bar') {
27-
console.log(message.name, message)
28-
}
1+
import {
2+
normalizeBookChanges,
3+
normalizeBookTickers,
4+
normalizeDerivativeTickers,
5+
normalizeLiquidations,
6+
normalizeOptionsSummary,
7+
normalizeTrades,
8+
replay,
9+
replayNormalized,
10+
stream,
11+
streamNormalized
12+
} from './dist/index.js'
13+
14+
const options = getOptions(process.argv.slice(2))
15+
const optionsError = getOptionsError(options)
16+
if (optionsError !== undefined) {
17+
console.error(`${optionsError}
18+
19+
Usage:
20+
node example.js stream <exchange> <symbol> <channel>
21+
node example.js replay <exchange> <symbol> <channel> <from> <to>
22+
node example.js --normalized stream <exchange> <symbol> <data-type>
23+
node example.js --normalized replay <exchange> <symbol> <data-type> <from> <to>
24+
25+
Examples:
26+
node example.js stream mexc-futures BTC_USDT push.depth
27+
node example.js replay mexc-futures BTC_USDT push.depth 2026-06-17 2026-06-18
28+
node example.js --normalized stream mexc-futures BTC_USDT book_change
29+
node example.js --normalized replay mexc-futures BTC_USDT book_change 2026-06-17 2026-06-18`)
30+
process.exit(1)
31+
}
32+
33+
for await (const message of createMessageStream(options)) {
34+
if (message === undefined || message?.type === 'disconnect') {
35+
console.log({ type: 'disconnect' })
36+
continue
37+
}
38+
39+
console.log(message)
40+
}
41+
42+
function getOptions(args) {
43+
const normalized = args.includes('--normalized')
44+
const positionalArgs = args.filter((arg) => arg !== '--normalized')
45+
46+
const normalizersByDataType = {
47+
trade: normalizeTrades,
48+
book_change: normalizeBookChanges,
49+
derivative_ticker: normalizeDerivativeTickers,
50+
option_summary: normalizeOptionsSummary,
51+
liquidation: normalizeLiquidations,
52+
book_ticker: normalizeBookTickers
53+
}
54+
55+
return {
56+
mode: positionalArgs[0], // 'stream' or 'replay'
57+
normalized,
58+
exchange: positionalArgs[1],
59+
symbols: [positionalArgs[2]],
60+
channel: normalized ? undefined : positionalArgs[3],
61+
dataType: normalized ? positionalArgs[3] : undefined,
62+
normalizer: normalized ? normalizersByDataType[positionalArgs[3]] : undefined,
63+
from: positionalArgs[4],
64+
to: positionalArgs[5]
65+
}
66+
}
67+
68+
function getOptionsError(options) {
69+
if (options.mode !== 'stream' && options.mode !== 'replay') {
70+
return 'Missing or invalid mode. Expected "stream" or "replay".'
71+
}
72+
if (options.exchange === undefined) {
73+
return 'Missing exchange name.'
74+
}
75+
if (options.symbols[0] === undefined) {
76+
return 'Missing symbol.'
77+
}
78+
if (options.normalized && options.dataType === undefined) {
79+
return 'Missing normalized data type.'
80+
}
81+
if (options.normalized === false && options.channel === undefined) {
82+
return 'Missing native channel.'
83+
}
84+
if (options.normalized && options.normalizer === undefined) {
85+
return `Invalid normalized data type "${options.dataType}".`
86+
}
87+
if (options.mode === 'replay' && (options.from === undefined || options.to === undefined)) {
88+
return 'Replay mode requires from and to dates.'
2989
}
3090
}
3191

32-
await produceVolumeBasedTradeBars(historicalMessages)
92+
function createMessageStream(options) {
93+
if (options.normalized) {
94+
if (options.mode === 'stream') {
95+
return streamNormalized(
96+
{
97+
exchange: options.exchange,
98+
symbols: options.symbols,
99+
timeoutIntervalMS: 20_000,
100+
withDisconnectMessages: true,
101+
onError: (error) => console.error(`[${options.exchange}] ${error.message}`)
102+
},
103+
options.normalizer
104+
)
105+
}
106+
107+
return replayNormalized(
108+
{
109+
exchange: options.exchange,
110+
symbols: options.symbols,
111+
from: options.from,
112+
to: options.to,
113+
withDisconnectMessages: true
114+
},
115+
options.normalizer
116+
)
117+
}
33118

34-
// or for real time data
35-
// await produceVolumeBasedTradeBars(realTimeMessages)
119+
const nativeFilters = [{ channel: options.channel, symbols: options.symbols }]
120+
if (options.mode === 'stream') {
121+
return stream({
122+
exchange: options.exchange,
123+
filters: nativeFilters,
124+
timeoutIntervalMS: 20_000,
125+
withDisconnects: true,
126+
onError: (error) => console.error(`[${options.exchange}] ${error.message}`)
127+
})
128+
}
129+
130+
return replay({
131+
exchange: options.exchange,
132+
from: options.from,
133+
to: options.to,
134+
filters: nativeFilters,
135+
withDisconnects: true
136+
})
137+
}

0 commit comments

Comments
 (0)