Line data Source code
1 : import type { Tick } from './tick_feed.js';
2 : import { parseFeedNumber, WebSocketTickFeed, type FeedMessage, type FeedProtocol, type WebSocketFeedOptions } from './websocket_feed.js';
3 :
4 : /// The public market data streams of Binance, the host only serves market data and needs no credentials
5 : export const BINANCE_FEED_URL = 'wss://data-stream.binance.vision/stream';
6 :
7 : /// The pairs of the board, as Binance names their streams
8 1 : export const BINANCE_PAIRS: readonly string[] = [
9 1 : 'btcusdt', 'ethusdt', 'bnbusdt', 'solusdt', 'xrpusdt', 'dogeusdt', 'adausdt', 'trxusdt', 'avaxusdt', 'linkusdt',
10 1 : 'dotusdt', 'ltcusdt', 'bchusdt', 'shibusdt', 'nearusdt', 'aptusdt', 'uniusdt', 'atomusdt', 'xlmusdt', 'etcusdt',
11 1 : ];
12 :
13 : /// The quote currencies of the pairs, the longest first so that USDT is not read as USD
14 1 : const QUOTES = ['USDT', 'USDC', 'FDUSD', 'BTC', 'ETH', 'EUR', 'USD'];
15 :
16 : /// The symbol of a pair on the board, e.g. BTCUSDT as BTC-USDT like the pairs of Coinbase
17 1 : export function binanceSymbol(pair: string): string {
18 1 : const upper = pair.toUpperCase();
19 1 : const quote = QUOTES.find(q => upper.endsWith(q) && upper.length > q.length);
20 1 : return quote == null ? upper : `${upper.slice(0, -quote.length)}-${quote}`;
21 1 : }
22 :
23 : /// The url of the combined stream of the trades and the best quotes of pairs
24 1 : export function binanceStreamUrl(url: string, pairs: readonly string[]): string {
25 1 : return `${url}?streams=${pairs.flatMap(p => [`${p}@trade`, `${p}@bookTicker`]).join('/')}`;
26 1 : }
27 :
28 : /// The best bid and ask of a pair
29 : interface Quote {
30 : bid: number | null;
31 : ask: number | null;
32 : }
33 :
34 : /// Reads the combined stream of trades and best quotes.
35 : /// A trade carries no quote, it gets the latest best bid and ask of its pair.
36 1 : export class BinanceProtocol implements FeedProtocol {
37 : /// The latest quote per pair
38 1 : private readonly quotes = new Map<string, Quote>();
39 :
40 1 : constructor(readonly url: string) { }
41 :
42 1 : subscription(): string | null {
43 1 : return null;
44 1 : }
45 :
46 1 : read(data: string): FeedMessage {
47 1 : let message: any;
48 1 : try {
49 1 : message = JSON.parse(data);
50 1 : } catch {
51 1 : return null;
52 1 : }
53 1 : if (message?.error != null) {
54 1 : return { error: typeof message.error.msg === 'string' ? message.error.msg : 'error' };
55 : }
56 1 : const payload = message?.data;
57 1 : if (typeof message?.stream !== 'string' || payload == null || typeof payload.s !== 'string') return null;
58 1 : if (message.stream.endsWith('@bookTicker')) {
59 1 : this.quotes.set(payload.s, { bid: parseFeedNumber(payload.b), ask: parseFeedNumber(payload.a) });
60 1 : return null;
61 1 : }
62 1 : if (payload.e !== 'trade') return null;
63 1 : const price = parseFeedNumber(payload.p);
64 1 : const ts = parseFeedNumber(payload.T);
65 1 : if (price == null || ts == null) return null;
66 1 : const quote = this.quotes.get(payload.s);
67 : const tick: Tick = {
68 1 : symbol: binanceSymbol(payload.s),
69 1 : ts,
70 1 : price,
71 1 : size: parseFeedNumber(payload.q) ?? 0,
72 1 : bid: quote?.bid ?? null,
73 1 : ask: quote?.ask ?? null,
74 : };
75 1 : return { tick };
76 1 : }
77 1 : }
78 :
79 : /// The options of the Binance feed
80 : export interface BinanceFeedOptions extends WebSocketFeedOptions {
81 : /// The url of the combined streams
82 : url?: string;
83 : /// The pairs to subscribe to, as Binance names their streams
84 : pairs?: readonly string[];
85 : }
86 :
87 : /// The trades of crypto pairs on Binance, from its public trade and book ticker streams
88 1 : export class BinanceFeed extends WebSocketTickFeed {
89 1 : constructor(options: BinanceFeedOptions = {}) {
90 1 : super(new BinanceProtocol(binanceStreamUrl(options.url ?? BINANCE_FEED_URL, options.pairs ?? BINANCE_PAIRS)), options);
91 1 : }
92 1 : }
|