@imbingox/acex 0.1.0-beta.0 → 0.1.0-beta.1
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.
- package/README.md +12 -54
- package/index.ts +1 -0
- package/package.json +15 -24
- package/src/adapters/binance/adapter.ts +53 -0
- package/src/adapters/binance/book-ticker.ts +123 -0
- package/src/adapters/binance/market-catalog.ts +251 -0
- package/src/adapters/types.ts +43 -0
- package/src/client/context.ts +60 -0
- package/src/client/create-client.ts +6 -0
- package/src/client/runtime.ts +283 -0
- package/src/errors.ts +20 -0
- package/src/index.ts +4 -0
- package/src/internal/async-event-bus.ts +100 -0
- package/src/internal/filters.ts +119 -0
- package/src/internal/managed-websocket.ts +258 -0
- package/src/managers/account-manager.ts +315 -0
- package/src/managers/market-manager.ts +642 -0
- package/src/managers/order-manager.ts +304 -0
- package/src/types/account.ts +160 -0
- package/src/types/client.ts +79 -0
- package/src/types/index.ts +5 -0
- package/src/types/market.ts +136 -0
- package/src/types/order.ts +142 -0
- package/src/types/shared.ts +78 -0
- package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
- package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
- package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -179
- package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -537
- package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
- package/dist/adapters/fake/fake-aster-adapter.js +0 -283
- package/dist/adapters/types.d.ts +0 -210
- package/dist/adapters/types.js +0 -1
- package/dist/core/client.d.ts +0 -37
- package/dist/core/client.js +0 -45
- package/dist/core/recovery.d.ts +0 -22
- package/dist/core/recovery.js +0 -18
- package/dist/core/runtime.d.ts +0 -26
- package/dist/core/runtime.js +0 -150
- package/dist/errors/acex-error.d.ts +0 -25
- package/dist/errors/acex-error.js +0 -54
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -3
- package/dist/managers/account-manager.d.ts +0 -41
- package/dist/managers/account-manager.js +0 -80
- package/dist/managers/market-manager.d.ts +0 -16
- package/dist/managers/market-manager.js +0 -28
- package/dist/managers/order-manager.d.ts +0 -87
- package/dist/managers/order-manager.js +0 -122
- package/dist/runtime/async-queue.d.ts +0 -8
- package/dist/runtime/async-queue.js +0 -88
- package/dist/runtime/request-id.d.ts +0 -1
- package/dist/runtime/request-id.js +0 -5
- package/dist/store/account-store.d.ts +0 -52
- package/dist/store/account-store.js +0 -18
- package/dist/store/health-store.d.ts +0 -16
- package/dist/store/health-store.js +0 -29
- package/dist/store/market-store.d.ts +0 -42
- package/dist/store/market-store.js +0 -51
- package/dist/store/order-store.d.ts +0 -38
- package/dist/store/order-store.js +0 -49
- package/dist/testing/create-fake-runtime.d.ts +0 -5
- package/dist/testing/create-fake-runtime.js +0 -7
- package/dist/types/public.d.ts +0 -11
- package/dist/types/public.js +0 -1
package/README.md
CHANGED
|
@@ -1,65 +1,23 @@
|
|
|
1
|
-
# acex
|
|
1
|
+
# @imbingox/acex
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Current Direction
|
|
6
|
-
|
|
7
|
-
- Official release verification now focuses on the Binance USD-M testnet path.
|
|
8
|
-
- `aster` remains in the repo as an exploration sample (via CCXT / CCXT Pro) but is not part of the current official support commitment.
|
|
9
|
-
- The Aster branch was kept to preserve the generic runtime and adapter validation work without blocking the Binance verification focus.
|
|
10
|
-
|
|
11
|
-
## Verification Direction
|
|
12
|
-
|
|
13
|
-
The current official verification path is Binance USD-M testnet. The current validation target scope consists of:
|
|
14
|
-
|
|
15
|
-
- market WebSocket connectivity
|
|
16
|
-
- private WebSocket connectivity
|
|
17
|
-
- baseline bootstrap for runtime readiness
|
|
18
|
-
- market `placeOrder`
|
|
19
|
-
- reduce-only market `placeOrder`
|
|
20
|
-
|
|
21
|
-
Run the verification script via:
|
|
3
|
+
To install dependencies:
|
|
22
4
|
|
|
23
5
|
```bash
|
|
24
|
-
bun
|
|
6
|
+
bun install
|
|
25
7
|
```
|
|
26
8
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
The script expects the following environment variables for Binance USD-M testnet:
|
|
30
|
-
|
|
31
|
-
- `BINANCE_USDM_API_KEY`
|
|
32
|
-
- `BINANCE_USDM_API_SECRET`
|
|
33
|
-
- `BINANCE_USDM_SYMBOL`
|
|
34
|
-
- `BINANCE_USDM_ORDER_SIZE`
|
|
35
|
-
- `BINANCE_USDM_ORDER_PRICE`
|
|
9
|
+
To run checks:
|
|
36
10
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- Verified product: `USD-M`
|
|
43
|
-
- Verified environment: `demo trading / testnet-style`
|
|
44
|
-
- Verified date: `2026-03-31`
|
|
45
|
-
- Verified runtime paths: `subscribeL1Book`, `fetchAccountBaseline`, `fetchOpenOrdersBaseline`, market `placeOrder`, reduce-only market `placeOrder`
|
|
46
|
-
- Verified transports with direct evidence: `public WS (bookTicker)`, `private WS (runtime-level order confirmation + balance confirmation)`, `REST baseline`
|
|
47
|
-
- Supplementary low-level evidence: direct `ccxt.pro.binanceusdm.watchOrders()` and `watchBalance()` both returned during the real demo open/close verification flow on `2026-03-31`
|
|
48
|
-
- Deferred coverage: `amendOrder`, `cancelAllOrders`
|
|
49
|
-
|
|
50
|
-
## Current Caveats
|
|
51
|
-
|
|
52
|
-
- Binance `watchTicker()` is not suitable for L1 mapping on USD-M because it returns a 24h ticker payload without `bid` / `ask`; the adapter now uses `watchBidsAsks()` / `bookTicker`.
|
|
53
|
-
- `fetchOpenOrders()` without `symbol` requires `warnOnFetchOpenOrdersWithoutSymbol = false` on the CCXT Binance USD-M exchange instance.
|
|
54
|
-
- Under Bun + CCXT Pro, websocket handles can remain alive after verification. The CLI script now exits explicitly after emitting the result so the verification command terminates deterministically.
|
|
55
|
-
- A simple `place/cancel` flow was insufficient to trigger `watchBalance()` on Binance demo during the 2026-03-31 investigation; the verification script now uses a real market open followed by a reduce-only market close so balance private-WS confirmation is part of the success gate.
|
|
56
|
-
|
|
57
|
-
## Aster Exploration Notes
|
|
11
|
+
```bash
|
|
12
|
+
bun run lint
|
|
13
|
+
bun run type-check
|
|
14
|
+
bun run test
|
|
15
|
+
```
|
|
58
16
|
|
|
59
|
-
|
|
17
|
+
To run the current entry:
|
|
60
18
|
|
|
61
19
|
```bash
|
|
62
|
-
bun run
|
|
20
|
+
bun run index.ts
|
|
63
21
|
```
|
|
64
22
|
|
|
65
|
-
|
|
23
|
+
This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index.ts";
|
package/package.json
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
|
+
"module": "index.ts",
|
|
5
6
|
"type": "module",
|
|
6
|
-
"packageManager": "bun@1.2.8",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"files": ["dist"],
|
|
11
|
-
"main": "./dist/index.js",
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
7
|
"exports": {
|
|
14
|
-
".":
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js"
|
|
17
|
-
}
|
|
8
|
+
".": "./index.ts"
|
|
18
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
19
14
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"format": "biome format --write ."
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"ccxt": "^4.5.45"
|
|
15
|
+
"lint": "biome check .",
|
|
16
|
+
"lint:fix": "biome check --write .",
|
|
17
|
+
"type-check": "tsc --noEmit",
|
|
18
|
+
"test": "bun test"
|
|
28
19
|
},
|
|
29
20
|
"devDependencies": {
|
|
30
|
-
"@biomejs/biome": "^
|
|
31
|
-
"
|
|
32
|
-
"
|
|
21
|
+
"@biomejs/biome": "^2.4.10",
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"typescript": "^6.0.2"
|
|
33
24
|
}
|
|
34
25
|
}
|
|
@@ -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,251 @@
|
|
|
1
|
+
import type { MarketDefinition, MarketType } from "../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
type FetchLike = typeof fetch;
|
|
4
|
+
|
|
5
|
+
export type BinanceMarketFamily = "spot" | "usdm" | "coinm";
|
|
6
|
+
|
|
7
|
+
export interface BinanceMarketDefinition extends MarketDefinition {
|
|
8
|
+
family: BinanceMarketFamily;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BinanceSymbolFilter {
|
|
12
|
+
filterType?: string;
|
|
13
|
+
tickSize?: string;
|
|
14
|
+
stepSize?: string;
|
|
15
|
+
minQty?: string;
|
|
16
|
+
minNotional?: string;
|
|
17
|
+
notional?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BinanceSpotSymbolInfo {
|
|
21
|
+
symbol: string;
|
|
22
|
+
status: string;
|
|
23
|
+
baseAsset: string;
|
|
24
|
+
quoteAsset: string;
|
|
25
|
+
filters?: BinanceSymbolFilter[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface BinanceSpotExchangeInfo {
|
|
29
|
+
symbols?: BinanceSpotSymbolInfo[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BinanceDerivativesSymbolInfo {
|
|
33
|
+
symbol: string;
|
|
34
|
+
status: string;
|
|
35
|
+
contractType?: string;
|
|
36
|
+
deliveryDate?: number;
|
|
37
|
+
baseAsset: string;
|
|
38
|
+
quoteAsset: string;
|
|
39
|
+
marginAsset?: string;
|
|
40
|
+
pricePrecision?: number;
|
|
41
|
+
quantityPrecision?: number;
|
|
42
|
+
contractSize?: number | string;
|
|
43
|
+
filters?: BinanceSymbolFilter[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BinanceDerivativesExchangeInfo {
|
|
47
|
+
symbols?: BinanceDerivativesSymbolInfo[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BINANCE_SPOT_EXCHANGE_INFO_URL =
|
|
51
|
+
"https://api.binance.com/api/v3/exchangeInfo";
|
|
52
|
+
const BINANCE_USDM_EXCHANGE_INFO_URL =
|
|
53
|
+
"https://fapi.binance.com/fapi/v1/exchangeInfo";
|
|
54
|
+
const BINANCE_COINM_EXCHANGE_INFO_URL =
|
|
55
|
+
"https://dapi.binance.com/dapi/v1/exchangeInfo";
|
|
56
|
+
|
|
57
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return value as Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFilter(
|
|
66
|
+
filters: BinanceSymbolFilter[] | undefined,
|
|
67
|
+
filterType: string,
|
|
68
|
+
): BinanceSymbolFilter | undefined {
|
|
69
|
+
return filters?.find((filter) => filter.filterType === filterType);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeStep(step: string | undefined, fallback = "1"): string {
|
|
73
|
+
return step && step.length > 0 ? step : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function precisionFromStep(step: string): number {
|
|
77
|
+
const normalized = step.replace(/0+$/, "");
|
|
78
|
+
const dotIndex = normalized.indexOf(".");
|
|
79
|
+
if (dotIndex === -1) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalized.length - dotIndex - 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatExpiry(expiry: number): string {
|
|
87
|
+
const date = new Date(expiry);
|
|
88
|
+
const year = date.getUTCFullYear();
|
|
89
|
+
const month = `${date.getUTCMonth() + 1}`.padStart(2, "0");
|
|
90
|
+
const day = `${date.getUTCDate()}`.padStart(2, "0");
|
|
91
|
+
return `${year}${month}${day}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function inferContractType(
|
|
95
|
+
contractType: string | undefined,
|
|
96
|
+
deliveryDate: number | undefined,
|
|
97
|
+
): MarketType {
|
|
98
|
+
if (contractType === "PERPETUAL") {
|
|
99
|
+
return "swap";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (deliveryDate && Number.isFinite(deliveryDate) && deliveryDate > 0) {
|
|
103
|
+
return "future";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return "swap";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildFuturesSymbol(
|
|
110
|
+
base: string,
|
|
111
|
+
quote: string,
|
|
112
|
+
settle: string,
|
|
113
|
+
type: MarketType,
|
|
114
|
+
expiry: number | undefined,
|
|
115
|
+
): string {
|
|
116
|
+
const prefix = `${base}/${quote}:${settle}`;
|
|
117
|
+
if (type !== "future" || !expiry) {
|
|
118
|
+
return prefix;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `${prefix}-${formatExpiry(expiry)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeSpotSymbol(
|
|
125
|
+
symbol: BinanceSpotSymbolInfo,
|
|
126
|
+
): BinanceMarketDefinition {
|
|
127
|
+
const priceFilter = getFilter(symbol.filters, "PRICE_FILTER");
|
|
128
|
+
const lotSizeFilter = getFilter(symbol.filters, "LOT_SIZE");
|
|
129
|
+
const notionalFilter =
|
|
130
|
+
getFilter(symbol.filters, "NOTIONAL") ??
|
|
131
|
+
getFilter(symbol.filters, "MIN_NOTIONAL");
|
|
132
|
+
const priceStep = normalizeStep(priceFilter?.tickSize);
|
|
133
|
+
const amountStep = normalizeStep(lotSizeFilter?.stepSize);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
exchange: "binance",
|
|
137
|
+
family: "spot",
|
|
138
|
+
symbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
|
|
139
|
+
id: symbol.symbol,
|
|
140
|
+
type: "spot",
|
|
141
|
+
base: symbol.baseAsset,
|
|
142
|
+
quote: symbol.quoteAsset,
|
|
143
|
+
active: symbol.status === "TRADING",
|
|
144
|
+
contract: false,
|
|
145
|
+
pricePrecision: precisionFromStep(priceStep),
|
|
146
|
+
amountPrecision: precisionFromStep(amountStep),
|
|
147
|
+
priceStep,
|
|
148
|
+
amountStep,
|
|
149
|
+
minAmount: lotSizeFilter?.minQty,
|
|
150
|
+
minNotional: notionalFilter?.minNotional ?? notionalFilter?.notional,
|
|
151
|
+
raw: toRecord(symbol),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeDerivativesSymbol(
|
|
156
|
+
symbol: BinanceDerivativesSymbolInfo,
|
|
157
|
+
family: BinanceMarketFamily,
|
|
158
|
+
): BinanceMarketDefinition {
|
|
159
|
+
const priceFilter = getFilter(symbol.filters, "PRICE_FILTER");
|
|
160
|
+
const lotSizeFilter = getFilter(symbol.filters, "LOT_SIZE");
|
|
161
|
+
const notionalFilter =
|
|
162
|
+
getFilter(symbol.filters, "NOTIONAL") ??
|
|
163
|
+
getFilter(symbol.filters, "MIN_NOTIONAL");
|
|
164
|
+
const priceStep = normalizeStep(priceFilter?.tickSize);
|
|
165
|
+
const amountStep = normalizeStep(lotSizeFilter?.stepSize);
|
|
166
|
+
const type = inferContractType(symbol.contractType, symbol.deliveryDate);
|
|
167
|
+
const settle =
|
|
168
|
+
symbol.marginAsset ??
|
|
169
|
+
(family === "usdm" ? symbol.quoteAsset : symbol.baseAsset);
|
|
170
|
+
const contractSize =
|
|
171
|
+
symbol.contractSize !== undefined
|
|
172
|
+
? `${symbol.contractSize}`
|
|
173
|
+
: family === "usdm"
|
|
174
|
+
? "1"
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
exchange: "binance",
|
|
179
|
+
family,
|
|
180
|
+
symbol: buildFuturesSymbol(
|
|
181
|
+
symbol.baseAsset,
|
|
182
|
+
symbol.quoteAsset,
|
|
183
|
+
settle,
|
|
184
|
+
type,
|
|
185
|
+
type === "future" ? symbol.deliveryDate : undefined,
|
|
186
|
+
),
|
|
187
|
+
id: symbol.symbol,
|
|
188
|
+
type,
|
|
189
|
+
base: symbol.baseAsset,
|
|
190
|
+
quote: symbol.quoteAsset,
|
|
191
|
+
settle,
|
|
192
|
+
active: symbol.status === "TRADING",
|
|
193
|
+
contract: true,
|
|
194
|
+
linear: family === "usdm",
|
|
195
|
+
inverse: family === "coinm",
|
|
196
|
+
contractSize,
|
|
197
|
+
pricePrecision: precisionFromStep(priceStep),
|
|
198
|
+
amountPrecision: precisionFromStep(amountStep),
|
|
199
|
+
priceStep,
|
|
200
|
+
amountStep,
|
|
201
|
+
minAmount: lotSizeFilter?.minQty,
|
|
202
|
+
minNotional: notionalFilter?.minNotional ?? notionalFilter?.notional,
|
|
203
|
+
expiry: type === "future" ? symbol.deliveryDate : undefined,
|
|
204
|
+
raw: toRecord(symbol),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function fetchJson<T>(fetchFn: FetchLike, url: string): Promise<T> {
|
|
209
|
+
const response = await fetchFn(url);
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Binance request failed: ${response.status} ${response.statusText}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (await response.json()) as T;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function sortMarkets(
|
|
220
|
+
markets: BinanceMarketDefinition[],
|
|
221
|
+
): BinanceMarketDefinition[] {
|
|
222
|
+
return [...markets].sort((left, right) =>
|
|
223
|
+
left.symbol.localeCompare(right.symbol),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function loadBinanceMarkets(
|
|
228
|
+
fetchFn: FetchLike = fetch,
|
|
229
|
+
): Promise<BinanceMarketDefinition[]> {
|
|
230
|
+
const [spot, usdm, coinm] = await Promise.all([
|
|
231
|
+
fetchJson<BinanceSpotExchangeInfo>(fetchFn, BINANCE_SPOT_EXCHANGE_INFO_URL),
|
|
232
|
+
fetchJson<BinanceDerivativesExchangeInfo>(
|
|
233
|
+
fetchFn,
|
|
234
|
+
BINANCE_USDM_EXCHANGE_INFO_URL,
|
|
235
|
+
),
|
|
236
|
+
fetchJson<BinanceDerivativesExchangeInfo>(
|
|
237
|
+
fetchFn,
|
|
238
|
+
BINANCE_COINM_EXCHANGE_INFO_URL,
|
|
239
|
+
),
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
return sortMarkets([
|
|
243
|
+
...(spot.symbols ?? []).map(normalizeSpotSymbol),
|
|
244
|
+
...(usdm.symbols ?? []).map((symbol) =>
|
|
245
|
+
normalizeDerivativesSymbol(symbol, "usdm"),
|
|
246
|
+
),
|
|
247
|
+
...(coinm.symbols ?? []).map((symbol) =>
|
|
248
|
+
normalizeDerivativesSymbol(symbol, "coinm"),
|
|
249
|
+
),
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
@@ -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
|
+
}
|