@imbingox/acex 0.1.0-beta.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +575 -42
  2. package/index.ts +1 -0
  3. package/package.json +19 -24
  4. package/src/adapters/binance/adapter.ts +53 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/market-catalog.ts +258 -0
  7. package/src/adapters/types.ts +43 -0
  8. package/src/client/context.ts +60 -0
  9. package/src/client/create-client.ts +6 -0
  10. package/src/client/runtime.ts +283 -0
  11. package/src/errors.ts +20 -0
  12. package/src/index.ts +5 -0
  13. package/src/internal/async-event-bus.ts +100 -0
  14. package/src/internal/filters.ts +119 -0
  15. package/src/internal/managed-websocket.ts +258 -0
  16. package/src/managers/account-manager.ts +315 -0
  17. package/src/managers/market-manager.ts +653 -0
  18. package/src/managers/order-manager.ts +304 -0
  19. package/src/types/account.ts +161 -0
  20. package/src/types/client.ts +79 -0
  21. package/src/types/index.ts +5 -0
  22. package/src/types/market.ts +138 -0
  23. package/src/types/order.ts +143 -0
  24. package/src/types/shared.ts +78 -0
  25. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  26. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  27. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -179
  28. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -537
  29. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  30. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  31. package/dist/adapters/types.d.ts +0 -210
  32. package/dist/adapters/types.js +0 -1
  33. package/dist/core/client.d.ts +0 -37
  34. package/dist/core/client.js +0 -45
  35. package/dist/core/recovery.d.ts +0 -22
  36. package/dist/core/recovery.js +0 -18
  37. package/dist/core/runtime.d.ts +0 -26
  38. package/dist/core/runtime.js +0 -150
  39. package/dist/errors/acex-error.d.ts +0 -25
  40. package/dist/errors/acex-error.js +0 -54
  41. package/dist/index.d.ts +0 -5
  42. package/dist/index.js +0 -3
  43. package/dist/managers/account-manager.d.ts +0 -41
  44. package/dist/managers/account-manager.js +0 -80
  45. package/dist/managers/market-manager.d.ts +0 -16
  46. package/dist/managers/market-manager.js +0 -28
  47. package/dist/managers/order-manager.d.ts +0 -87
  48. package/dist/managers/order-manager.js +0 -122
  49. package/dist/runtime/async-queue.d.ts +0 -8
  50. package/dist/runtime/async-queue.js +0 -88
  51. package/dist/runtime/request-id.d.ts +0 -1
  52. package/dist/runtime/request-id.js +0 -5
  53. package/dist/store/account-store.d.ts +0 -52
  54. package/dist/store/account-store.js +0 -18
  55. package/dist/store/health-store.d.ts +0 -16
  56. package/dist/store/health-store.js +0 -29
  57. package/dist/store/market-store.d.ts +0 -42
  58. package/dist/store/market-store.js +0 -51
  59. package/dist/store/order-store.d.ts +0 -38
  60. package/dist/store/order-store.js +0 -49
  61. package/dist/testing/create-fake-runtime.d.ts +0 -5
  62. package/dist/testing/create-fake-runtime.js +0 -7
  63. package/dist/types/public.d.ts +0 -11
  64. package/dist/types/public.js +0 -1
@@ -0,0 +1,53 @@
1
+ import type { MarketDefinition } from "../../types/index.ts";
2
+ import type {
3
+ L1BookStreamCallbacks,
4
+ L1BookStreamOptions,
5
+ MarketAdapter,
6
+ StreamHandle,
7
+ } from "../types.ts";
8
+ import { subscribeBinanceBookTicker } from "./book-ticker.ts";
9
+ import {
10
+ type BinanceMarketDefinition,
11
+ loadBinanceMarkets,
12
+ } from "./market-catalog.ts";
13
+
14
+ export class BinanceMarketAdapter implements MarketAdapter {
15
+ readonly exchange = "binance" as const;
16
+
17
+ private readonly definitions = new Map<string, BinanceMarketDefinition>();
18
+
19
+ async loadMarkets(): Promise<MarketDefinition[]> {
20
+ const markets = await loadBinanceMarkets();
21
+ this.definitions.clear();
22
+
23
+ for (const market of markets) {
24
+ this.definitions.set(market.symbol, market);
25
+ }
26
+
27
+ return markets;
28
+ }
29
+
30
+ createL1BookStream(
31
+ market: MarketDefinition,
32
+ callbacks: L1BookStreamCallbacks,
33
+ options: L1BookStreamOptions,
34
+ ): StreamHandle {
35
+ const binanceMarket = this.definitions.get(market.symbol);
36
+ if (!binanceMarket) {
37
+ throw new Error(`Unknown Binance market: ${market.symbol}`);
38
+ }
39
+
40
+ return subscribeBinanceBookTicker(
41
+ binanceMarket,
42
+ {
43
+ onBookTicker(update) {
44
+ callbacks.onUpdate(update);
45
+ },
46
+ onFreshnessChange: callbacks.onFreshnessChange,
47
+ onDisconnected: callbacks.onDisconnected,
48
+ onError: callbacks.onError,
49
+ },
50
+ options,
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,123 @@
1
+ import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
2
+ import type { BinanceMarketDefinition } from "./market-catalog.ts";
3
+
4
+ export interface BinanceL1BookUpdate {
5
+ bidPrice: string;
6
+ bidSize: string;
7
+ askPrice: string;
8
+ askSize: string;
9
+ exchangeTs?: number;
10
+ receivedAt: number;
11
+ }
12
+
13
+ export interface BinanceBookTickerSubscription {
14
+ readonly ready: Promise<void>;
15
+ close(): void;
16
+ }
17
+
18
+ export interface BinanceBookTickerCallbacks {
19
+ onBookTicker(update: BinanceL1BookUpdate): void;
20
+ onFreshnessChange(
21
+ freshness: "fresh" | "stale",
22
+ reason?: "heartbeat_timeout",
23
+ ): void;
24
+ onDisconnected(): void;
25
+ onError?(error: Error): void;
26
+ }
27
+
28
+ export interface BinanceBookTickerOptions {
29
+ initialMessageTimeoutMs: number;
30
+ staleAfterMs: number;
31
+ reconnectDelayMs: number;
32
+ reconnectMaxDelayMs: number;
33
+ now?: () => number;
34
+ }
35
+
36
+ interface BinanceBookTickerMessage {
37
+ b?: string;
38
+ B?: string;
39
+ a?: string;
40
+ A?: string;
41
+ T?: number;
42
+ }
43
+
44
+ const BINANCE_SPOT_WS_BASE_URL = "wss://stream.binance.com:9443/ws";
45
+ const BINANCE_USDM_WS_BASE_URL = "wss://fstream.binance.com/ws";
46
+ const BINANCE_COINM_WS_BASE_URL = "wss://dstream.binance.com/ws";
47
+
48
+ function getWsBaseUrl(market: BinanceMarketDefinition): string {
49
+ switch (market.family) {
50
+ case "spot":
51
+ return BINANCE_SPOT_WS_BASE_URL;
52
+ case "usdm":
53
+ return BINANCE_USDM_WS_BASE_URL;
54
+ case "coinm":
55
+ return BINANCE_COINM_WS_BASE_URL;
56
+ }
57
+ }
58
+
59
+ function buildBookTickerUrl(market: BinanceMarketDefinition): string {
60
+ return `${getWsBaseUrl(market)}/${market.id.toLowerCase()}@bookTicker`;
61
+ }
62
+
63
+ function parseBookTickerMessage(
64
+ data: string,
65
+ ): BinanceBookTickerMessage | undefined {
66
+ const parsed = JSON.parse(data) as BinanceBookTickerMessage;
67
+ if (!parsed.b || !parsed.B || !parsed.a || !parsed.A) {
68
+ return undefined;
69
+ }
70
+
71
+ return parsed;
72
+ }
73
+
74
+ export function subscribeBinanceBookTicker(
75
+ market: BinanceMarketDefinition,
76
+ callbacks: BinanceBookTickerCallbacks,
77
+ options: BinanceBookTickerOptions,
78
+ ): BinanceBookTickerSubscription {
79
+ const session = createManagedWebSocket<BinanceBookTickerMessage>({
80
+ url: buildBookTickerUrl(market),
81
+ initialMessageTimeoutMs: options.initialMessageTimeoutMs,
82
+ now: options.now,
83
+ messageWatchdog: {
84
+ staleAfterMs: options.staleAfterMs,
85
+ onStale() {
86
+ callbacks.onFreshnessChange("stale", "heartbeat_timeout");
87
+ },
88
+ },
89
+ reconnect: {
90
+ initialDelayMs: options.reconnectDelayMs,
91
+ maxDelayMs: options.reconnectMaxDelayMs,
92
+ },
93
+ parseMessage: parseBookTickerMessage,
94
+ onMessage(message, receivedAt) {
95
+ if (!message.b || !message.B || !message.a || !message.A) {
96
+ return;
97
+ }
98
+
99
+ callbacks.onBookTicker({
100
+ bidPrice: message.b,
101
+ bidSize: message.B,
102
+ askPrice: message.a,
103
+ askSize: message.A,
104
+ exchangeTs: message.T,
105
+ receivedAt,
106
+ });
107
+ callbacks.onFreshnessChange("fresh");
108
+ },
109
+ onUnexpectedClose() {
110
+ callbacks.onDisconnected();
111
+ },
112
+ onError() {
113
+ callbacks.onError?.(new Error(`WebSocket error for ${market.symbol}`));
114
+ },
115
+ });
116
+
117
+ return {
118
+ ready: session.ready,
119
+ close() {
120
+ session.close();
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,258 @@
1
+ import BigNumber from "bignumber.js";
2
+ import type { MarketDefinition, MarketType } from "../../types/index.ts";
3
+
4
+ type FetchLike = typeof fetch;
5
+
6
+ export type BinanceMarketFamily = "spot" | "usdm" | "coinm";
7
+
8
+ export interface BinanceMarketDefinition extends MarketDefinition {
9
+ family: BinanceMarketFamily;
10
+ }
11
+
12
+ interface BinanceSymbolFilter {
13
+ filterType?: string;
14
+ tickSize?: string;
15
+ stepSize?: string;
16
+ minQty?: string;
17
+ minNotional?: string;
18
+ notional?: string;
19
+ }
20
+
21
+ interface BinanceSpotSymbolInfo {
22
+ symbol: string;
23
+ status: string;
24
+ baseAsset: string;
25
+ quoteAsset: string;
26
+ filters?: BinanceSymbolFilter[];
27
+ }
28
+
29
+ interface BinanceSpotExchangeInfo {
30
+ symbols?: BinanceSpotSymbolInfo[];
31
+ }
32
+
33
+ interface BinanceDerivativesSymbolInfo {
34
+ symbol: string;
35
+ status: string;
36
+ contractType?: string;
37
+ deliveryDate?: number;
38
+ baseAsset: string;
39
+ quoteAsset: string;
40
+ marginAsset?: string;
41
+ pricePrecision?: number;
42
+ quantityPrecision?: number;
43
+ contractSize?: number | string;
44
+ filters?: BinanceSymbolFilter[];
45
+ }
46
+
47
+ interface BinanceDerivativesExchangeInfo {
48
+ symbols?: BinanceDerivativesSymbolInfo[];
49
+ }
50
+
51
+ const BINANCE_SPOT_EXCHANGE_INFO_URL =
52
+ "https://api.binance.com/api/v3/exchangeInfo";
53
+ const BINANCE_USDM_EXCHANGE_INFO_URL =
54
+ "https://fapi.binance.com/fapi/v1/exchangeInfo";
55
+ const BINANCE_COINM_EXCHANGE_INFO_URL =
56
+ "https://dapi.binance.com/dapi/v1/exchangeInfo";
57
+
58
+ function toRecord(value: unknown): Record<string, unknown> {
59
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
60
+ return {};
61
+ }
62
+
63
+ return value as Record<string, unknown>;
64
+ }
65
+
66
+ function getFilter(
67
+ filters: BinanceSymbolFilter[] | undefined,
68
+ filterType: string,
69
+ ): BinanceSymbolFilter | undefined {
70
+ return filters?.find((filter) => filter.filterType === filterType);
71
+ }
72
+
73
+ function normalizeStep(step: string | undefined, fallback = "1"): string {
74
+ return step && step.length > 0 ? step : fallback;
75
+ }
76
+
77
+ function precisionFromStep(step: string): number {
78
+ const normalized = step.replace(/0+$/, "");
79
+ const dotIndex = normalized.indexOf(".");
80
+ if (dotIndex === -1) {
81
+ return 0;
82
+ }
83
+
84
+ return normalized.length - dotIndex - 1;
85
+ }
86
+
87
+ function formatExpiry(expiry: number): string {
88
+ const date = new Date(expiry);
89
+ const year = date.getUTCFullYear();
90
+ const month = `${date.getUTCMonth() + 1}`.padStart(2, "0");
91
+ const day = `${date.getUTCDate()}`.padStart(2, "0");
92
+ return `${year}${month}${day}`;
93
+ }
94
+
95
+ function inferContractType(
96
+ contractType: string | undefined,
97
+ deliveryDate: number | undefined,
98
+ ): MarketType {
99
+ if (contractType === "PERPETUAL") {
100
+ return "swap";
101
+ }
102
+
103
+ if (deliveryDate && Number.isFinite(deliveryDate) && deliveryDate > 0) {
104
+ return "future";
105
+ }
106
+
107
+ return "swap";
108
+ }
109
+
110
+ function buildFuturesSymbol(
111
+ base: string,
112
+ quote: string,
113
+ settle: string,
114
+ type: MarketType,
115
+ expiry: number | undefined,
116
+ ): string {
117
+ const prefix = `${base}/${quote}:${settle}`;
118
+ if (type !== "future" || !expiry) {
119
+ return prefix;
120
+ }
121
+
122
+ return `${prefix}-${formatExpiry(expiry)}`;
123
+ }
124
+
125
+ function normalizeSpotSymbol(
126
+ symbol: BinanceSpotSymbolInfo,
127
+ ): BinanceMarketDefinition {
128
+ const priceFilter = getFilter(symbol.filters, "PRICE_FILTER");
129
+ const lotSizeFilter = getFilter(symbol.filters, "LOT_SIZE");
130
+ const notionalFilter =
131
+ getFilter(symbol.filters, "NOTIONAL") ??
132
+ getFilter(symbol.filters, "MIN_NOTIONAL");
133
+ const priceStep = normalizeStep(priceFilter?.tickSize);
134
+ const amountStep = normalizeStep(lotSizeFilter?.stepSize);
135
+ const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
136
+
137
+ return {
138
+ exchange: "binance",
139
+ family: "spot",
140
+ symbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
141
+ id: symbol.symbol,
142
+ type: "spot",
143
+ base: symbol.baseAsset,
144
+ quote: symbol.quoteAsset,
145
+ active: symbol.status === "TRADING",
146
+ contract: false,
147
+ pricePrecision: precisionFromStep(priceStep),
148
+ amountPrecision: precisionFromStep(amountStep),
149
+ priceStep: new BigNumber(priceStep),
150
+ amountStep: new BigNumber(amountStep),
151
+ minAmount: lotSizeFilter?.minQty
152
+ ? new BigNumber(lotSizeFilter.minQty)
153
+ : undefined,
154
+ minNotional: notionalValue ? new BigNumber(notionalValue) : undefined,
155
+ raw: toRecord(symbol),
156
+ };
157
+ }
158
+
159
+ function normalizeDerivativesSymbol(
160
+ symbol: BinanceDerivativesSymbolInfo,
161
+ family: BinanceMarketFamily,
162
+ ): BinanceMarketDefinition {
163
+ const priceFilter = getFilter(symbol.filters, "PRICE_FILTER");
164
+ const lotSizeFilter = getFilter(symbol.filters, "LOT_SIZE");
165
+ const notionalFilter =
166
+ getFilter(symbol.filters, "NOTIONAL") ??
167
+ getFilter(symbol.filters, "MIN_NOTIONAL");
168
+ const priceStep = normalizeStep(priceFilter?.tickSize);
169
+ const amountStep = normalizeStep(lotSizeFilter?.stepSize);
170
+ const type = inferContractType(symbol.contractType, symbol.deliveryDate);
171
+ const settle =
172
+ symbol.marginAsset ??
173
+ (family === "usdm" ? symbol.quoteAsset : symbol.baseAsset);
174
+ const contractSize =
175
+ symbol.contractSize !== undefined
176
+ ? `${symbol.contractSize}`
177
+ : family === "usdm"
178
+ ? "1"
179
+ : undefined;
180
+ const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
181
+
182
+ return {
183
+ exchange: "binance",
184
+ family,
185
+ symbol: buildFuturesSymbol(
186
+ symbol.baseAsset,
187
+ symbol.quoteAsset,
188
+ settle,
189
+ type,
190
+ type === "future" ? symbol.deliveryDate : undefined,
191
+ ),
192
+ id: symbol.symbol,
193
+ type,
194
+ base: symbol.baseAsset,
195
+ quote: symbol.quoteAsset,
196
+ settle,
197
+ active: symbol.status === "TRADING",
198
+ contract: true,
199
+ linear: family === "usdm",
200
+ inverse: family === "coinm",
201
+ contractSize: contractSize ? new BigNumber(contractSize) : undefined,
202
+ pricePrecision: precisionFromStep(priceStep),
203
+ amountPrecision: precisionFromStep(amountStep),
204
+ priceStep: new BigNumber(priceStep),
205
+ amountStep: new BigNumber(amountStep),
206
+ minAmount: lotSizeFilter?.minQty
207
+ ? new BigNumber(lotSizeFilter.minQty)
208
+ : undefined,
209
+ minNotional: notionalValue ? new BigNumber(notionalValue) : undefined,
210
+ expiry: type === "future" ? symbol.deliveryDate : undefined,
211
+ raw: toRecord(symbol),
212
+ };
213
+ }
214
+
215
+ async function fetchJson<T>(fetchFn: FetchLike, url: string): Promise<T> {
216
+ const response = await fetchFn(url);
217
+ if (!response.ok) {
218
+ throw new Error(
219
+ `Binance request failed: ${response.status} ${response.statusText}`,
220
+ );
221
+ }
222
+
223
+ return (await response.json()) as T;
224
+ }
225
+
226
+ function sortMarkets(
227
+ markets: BinanceMarketDefinition[],
228
+ ): BinanceMarketDefinition[] {
229
+ return [...markets].sort((left, right) =>
230
+ left.symbol.localeCompare(right.symbol),
231
+ );
232
+ }
233
+
234
+ export async function loadBinanceMarkets(
235
+ fetchFn: FetchLike = fetch,
236
+ ): Promise<BinanceMarketDefinition[]> {
237
+ const [spot, usdm, coinm] = await Promise.all([
238
+ fetchJson<BinanceSpotExchangeInfo>(fetchFn, BINANCE_SPOT_EXCHANGE_INFO_URL),
239
+ fetchJson<BinanceDerivativesExchangeInfo>(
240
+ fetchFn,
241
+ BINANCE_USDM_EXCHANGE_INFO_URL,
242
+ ),
243
+ fetchJson<BinanceDerivativesExchangeInfo>(
244
+ fetchFn,
245
+ BINANCE_COINM_EXCHANGE_INFO_URL,
246
+ ),
247
+ ]);
248
+
249
+ return sortMarkets([
250
+ ...(spot.symbols ?? []).map(normalizeSpotSymbol),
251
+ ...(usdm.symbols ?? []).map((symbol) =>
252
+ normalizeDerivativesSymbol(symbol, "usdm"),
253
+ ),
254
+ ...(coinm.symbols ?? []).map((symbol) =>
255
+ normalizeDerivativesSymbol(symbol, "coinm"),
256
+ ),
257
+ ]);
258
+ }
@@ -0,0 +1,43 @@
1
+ import type { Exchange, MarketDefinition } from "../types/index.ts";
2
+
3
+ export interface StreamHandle {
4
+ readonly ready: Promise<void>;
5
+ close(): void;
6
+ }
7
+
8
+ export interface RawL1BookUpdate {
9
+ bidPrice: string;
10
+ bidSize: string;
11
+ askPrice: string;
12
+ askSize: string;
13
+ exchangeTs?: number;
14
+ receivedAt: number;
15
+ }
16
+
17
+ export interface L1BookStreamCallbacks {
18
+ onUpdate(update: RawL1BookUpdate): void;
19
+ onFreshnessChange(
20
+ freshness: "fresh" | "stale",
21
+ reason?: "heartbeat_timeout",
22
+ ): void;
23
+ onDisconnected(): void;
24
+ onError(error: Error): void;
25
+ }
26
+
27
+ export interface L1BookStreamOptions {
28
+ initialMessageTimeoutMs: number;
29
+ staleAfterMs: number;
30
+ reconnectDelayMs: number;
31
+ reconnectMaxDelayMs: number;
32
+ now?: () => number;
33
+ }
34
+
35
+ export interface MarketAdapter {
36
+ readonly exchange: Exchange;
37
+ loadMarkets(): Promise<MarketDefinition[]>;
38
+ createL1BookStream(
39
+ market: MarketDefinition,
40
+ callbacks: L1BookStreamCallbacks,
41
+ options: L1BookStreamOptions,
42
+ ): StreamHandle;
43
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ AccountCredentials,
3
+ AcexInternalError,
4
+ Exchange,
5
+ HealthEvent,
6
+ } from "../types/index.ts";
7
+
8
+ export interface RegisteredAccountRecord {
9
+ accountId: string;
10
+ exchange: Exchange;
11
+ credentials?: AccountCredentials;
12
+ options?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface ClientContext {
16
+ now(): number;
17
+ assertStarted(): void;
18
+ getRegisteredAccount(accountId: string): RegisteredAccountRecord;
19
+ ensurePrivateCredentials(accountId: string): void;
20
+ publishRuntimeError(
21
+ source: AcexInternalError["source"],
22
+ error: Error,
23
+ metadata?: Omit<AcexInternalError, "error" | "source" | "ts">,
24
+ ): void;
25
+ publishHealthEvent(event: HealthEvent): void;
26
+ }
27
+
28
+ export interface ManagerLifecycle {
29
+ onClientStarted(): void;
30
+ onClientStopping(now: number): void;
31
+ }
32
+
33
+ export interface AccountAwareManager {
34
+ onAccountRemoved(accountId: string, now: number): void;
35
+ onCredentialsUpdated(accountId: string, exchange: Exchange): void;
36
+ }
37
+
38
+ export interface HealthReporter<T> {
39
+ getStatuses(): T[];
40
+ }
41
+
42
+ export function hasPrivateCredentials(
43
+ credentials?: AccountCredentials,
44
+ ): boolean {
45
+ return Boolean(credentials?.apiKey && credentials.secret);
46
+ }
47
+
48
+ export function mergeCredentials(
49
+ current: AccountCredentials | undefined,
50
+ next: AccountCredentials,
51
+ ): AccountCredentials {
52
+ return {
53
+ ...current,
54
+ ...next,
55
+ extra: {
56
+ ...(current?.extra ?? {}),
57
+ ...(next.extra ?? {}),
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,6 @@
1
+ import type { AcexClient, CreateClientOptions } from "../types/index.ts";
2
+ import { AcexClientImpl } from "./runtime.ts";
3
+
4
+ export function createClient(options?: CreateClientOptions): AcexClient {
5
+ return new AcexClientImpl(options);
6
+ }