@imbingox/acex 0.1.0-beta.1 → 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.
- package/README.md +585 -10
- package/package.json +5 -1
- package/src/adapters/binance/market-catalog.ts +16 -9
- package/src/index.ts +1 -0
- package/src/managers/market-manager.ts +21 -10
- package/src/types/account.ts +14 -13
- package/src/types/market.ts +16 -14
- package/src/types/order.ts +7 -6
package/README.md
CHANGED
|
@@ -1,23 +1,598 @@
|
|
|
1
1
|
# @imbingox/acex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`acex` 是一个面向交易场景的状态型 SDK。调用方只需要持有一个 `AcexClient`,就可以通过统一的 `market`、`account`、`order` manager 读取最新快照、订阅增量事件、观察健康状态,而不需要自己维护本地缓存、ready barrier 或 websocket 生命周期。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
bun
|
|
8
|
+
bun add @imbingox/acex
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## 完整用法
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
### 1. 创建 client
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createClient } from "@imbingox/acex";
|
|
17
|
+
// SDK re-exports BigNumber,调用方无需单独安装 bignumber.js
|
|
18
|
+
import { BigNumber } from "@imbingox/acex";
|
|
19
|
+
|
|
20
|
+
const client = createClient();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`createClient` 接受一个可选的配置对象,当前真正生效的是 `market.*` 运行时参数:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const client = createClient({
|
|
27
|
+
market: {
|
|
28
|
+
l1InitialMessageTimeoutMs: 15_000, // L1 Book 首条消息超时(默认 15s)
|
|
29
|
+
l1StaleAfterMs: 15_000, // 多久没收到消息标记 stale(默认 15s)
|
|
30
|
+
l1ReconnectDelayMs: 1_000, // 断线重连初始延迟
|
|
31
|
+
l1ReconnectMaxDelayMs: 10_000, // 断线重连最大延迟(指数退避上限)
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> `sandbox`、`logger`、`logLevel` 已预留但当前未生效。
|
|
37
|
+
|
|
38
|
+
### 2. 生命周期
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// 启动 client(必须在所有 subscribe 之前)
|
|
42
|
+
await client.start();
|
|
43
|
+
|
|
44
|
+
// ... 使用 client ...
|
|
45
|
+
|
|
46
|
+
// 停止 client(释放所有 websocket、订阅关系)
|
|
47
|
+
await client.stop();
|
|
48
|
+
|
|
49
|
+
// 也可以指定优雅退出选项
|
|
50
|
+
await client.stop({ graceful: true, timeoutMs: 5000 });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Client 的状态机:`idle` → `starting` → `running` → `stopping` → `stopped`。
|
|
54
|
+
|
|
55
|
+
可以通过 `client.getStatus()` 随时查看当前状态。
|
|
56
|
+
|
|
57
|
+
### 3. Market Catalog(市场列表)
|
|
58
|
+
|
|
59
|
+
加载市场列表后可以发现可用交易对、读取精度参数:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// 拉取并缓存所有交易所的 market catalog
|
|
63
|
+
await client.market.loadMarkets();
|
|
64
|
+
|
|
65
|
+
// 列出所有 market
|
|
66
|
+
const markets = client.market.listMarkets();
|
|
67
|
+
// → MarketDefinition[]
|
|
68
|
+
|
|
69
|
+
// 只列出指定交易所的 market
|
|
70
|
+
const binanceMarkets = client.market.listMarkets("binance");
|
|
71
|
+
|
|
72
|
+
// 按交易所 + 统一 symbol 查询单个 market
|
|
73
|
+
const btcPerp = client.market.getMarket("binance", "BTC/USDT:USDT");
|
|
74
|
+
// → MarketDefinition | undefined
|
|
75
|
+
|
|
76
|
+
// 查询一个 symbol 在所有交易所的 market(多交易所场景)
|
|
77
|
+
const allBtcPerp = client.market.findMarkets("BTC/USDT:USDT");
|
|
78
|
+
// → MarketDefinition[]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
返回的 `MarketDefinition` 包含以下字段:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
{
|
|
85
|
+
exchange: "binance",
|
|
86
|
+
symbol: "BTC/USDT:USDT", // 统一 symbol
|
|
87
|
+
id: "BTCUSDT", // 交易所原始 symbol
|
|
88
|
+
type: "swap", // "spot" | "swap" | "future"
|
|
89
|
+
base: "BTC",
|
|
90
|
+
quote: "USDT",
|
|
91
|
+
settle: "USDT", // 结算币种(swap/future 才有)
|
|
92
|
+
active: true, // 是否可交易
|
|
93
|
+
contract: true, // 是否合约
|
|
94
|
+
linear: true, // U 本位
|
|
95
|
+
inverse: false, // 币本位
|
|
96
|
+
contractSize: BigNumber(1),
|
|
97
|
+
pricePrecision: 1,
|
|
98
|
+
amountPrecision: 3,
|
|
99
|
+
priceStep: BigNumber(0.10), // 最小价格变动
|
|
100
|
+
amountStep: BigNumber(0.001), // 最小数量变动
|
|
101
|
+
minNotional: BigNumber(5), // 最小名义价值
|
|
102
|
+
raw: { ... }, // 交易所原始数据
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> 所有价格、数量、金额字段均为 `BigNumber` 类型(来自 [bignumber.js](https://github.com/MikeMcl/bignumber.js)),可直接进行算术运算。
|
|
107
|
+
|
|
108
|
+
**统一 symbol 约定:**
|
|
109
|
+
|
|
110
|
+
| symbol 格式 | 含义 | 示例 |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `BASE/QUOTE` | spot 现货 | `BTC/USDT` |
|
|
113
|
+
| `BASE/QUOTE:SETTLE` | USDⓈ-M 永续 | `BTC/USDT:USDT` |
|
|
114
|
+
| `BASE/USD:BASE` | COIN-M 永续 | `BTC/USD:BTC` |
|
|
115
|
+
| `BASE/USD:BASE-YYYYMMDD` | COIN-M 交割 | `BTC/USD:BTC-20250627` |
|
|
116
|
+
|
|
117
|
+
### 4. L1 Book(最优买卖价)
|
|
118
|
+
|
|
119
|
+
这是当前最核心的实时数据能力。
|
|
120
|
+
|
|
121
|
+
#### 订阅
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// subscribeL1Book 是 ready barrier:
|
|
125
|
+
// await 返回后,getL1Book 已经可以拿到首个可用快照
|
|
126
|
+
await client.market.subscribeL1Book({
|
|
127
|
+
exchange: "binance",
|
|
128
|
+
symbol: "BTC/USDT:USDT",
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`subscribeL1Book` 会自动确保 market catalog 已加载,所以不必手动先调 `loadMarkets()`。
|
|
133
|
+
|
|
134
|
+
#### 读取快照
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const book = client.market.getL1Book({
|
|
138
|
+
exchange: "binance",
|
|
139
|
+
symbol: "BTC/USDT:USDT",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (book) {
|
|
143
|
+
console.log(book.bidPrice, book.bidSize); // 最优买(BigNumber)
|
|
144
|
+
console.log(book.askPrice, book.askSize); // 最优卖(BigNumber)
|
|
145
|
+
// 直接算术运算
|
|
146
|
+
const spread = book.askPrice.minus(book.bidPrice);
|
|
147
|
+
console.log(`spread: ${spread.toFixed()}`);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`L1Book` 完整结构:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
{
|
|
155
|
+
exchange: "binance",
|
|
156
|
+
symbol: "BTC/USDT:USDT",
|
|
157
|
+
bidPrice: BigNumber("104321.50"),
|
|
158
|
+
bidSize: BigNumber("1.234"),
|
|
159
|
+
askPrice: BigNumber("104321.60"),
|
|
160
|
+
askSize: BigNumber("0.567"),
|
|
161
|
+
exchangeTs: 1710000000000, // 交易所时间戳(可能为空)
|
|
162
|
+
receivedAt: 1710000000001, // SDK 收到时间
|
|
163
|
+
updatedAt: 1710000000001, // SDK 更新时间
|
|
164
|
+
version: 42, // 递增序号
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> 所有价格和数量都是 `BigNumber` 类型,避免浮点精度问题,可直接进行算术运算。
|
|
169
|
+
|
|
170
|
+
#### 消费增量事件
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// events.* 只消费事件,不会隐式触发订阅
|
|
174
|
+
// 必须先 subscribeL1Book 才会有数据流
|
|
175
|
+
for await (const event of client.market.events.l1BookUpdates({
|
|
176
|
+
exchange: "binance",
|
|
177
|
+
symbol: "BTC/USDT:USDT",
|
|
178
|
+
})) {
|
|
179
|
+
console.log(event.snapshot.bidPrice, event.snapshot.askPrice);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
也可以手动控制迭代器:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const iterator = client.market.events
|
|
187
|
+
.l1BookUpdates({ exchange: "binance", symbol: "BTC/USDT:USDT" })
|
|
188
|
+
[Symbol.asyncIterator]();
|
|
189
|
+
|
|
190
|
+
const { value, done } = await iterator.next();
|
|
191
|
+
if (!done) {
|
|
192
|
+
console.log(value.snapshot);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 不再消费时,释放迭代器
|
|
196
|
+
await iterator.return?.();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
不传 filter 可以接收所有 symbol 的更新:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
for await (const event of client.market.events.l1BookUpdates()) {
|
|
203
|
+
console.log(event.exchange, event.symbol, event.snapshot.bidPrice);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### 事件当触发器(推荐模式)
|
|
208
|
+
|
|
209
|
+
`event.snapshot` 是事件发生那一刻的快照,但由于事件异步消费,处理时内部状态可能已被更新。如果你需要同时读取多个 symbol 的最新价格(如套利、对冲),推荐把事件当触发器,用 `getL1Book()` 读最新值:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const pairs = [
|
|
213
|
+
{ exchange: "binance", symbol: "BTC/USDT:USDT" },
|
|
214
|
+
{ exchange: "binance", symbol: "BTC/USD:BTC" },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
for (const pair of pairs) {
|
|
218
|
+
await client.market.subscribeL1Book(pair);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 不带 filter — 任何 symbol 变动都触发
|
|
222
|
+
for await (const event of client.market.events.l1BookUpdates()) {
|
|
223
|
+
const books = pairs.map((pair) => ({
|
|
224
|
+
...pair,
|
|
225
|
+
book: client.market.getL1Book(pair),
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
if (books.some((b) => !b.book)) continue;
|
|
229
|
+
|
|
230
|
+
// 所有 symbol 的最新价格,执行你的策略逻辑
|
|
231
|
+
doSomething(books);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### 查看订阅状态
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const status = client.market.getMarketStatus({
|
|
239
|
+
exchange: "binance",
|
|
240
|
+
symbol: "BTC/USDT:USDT",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (status) {
|
|
244
|
+
status.activity; // "active" | "inactive"
|
|
245
|
+
status.ready; // 首次 ready 是否完成
|
|
246
|
+
status.freshness; // "fresh" | "stale" | "reconciling"
|
|
247
|
+
status.lastReceivedAt; // 最后收到数据的时间
|
|
248
|
+
status.reason; // 变 stale 的原因: "ws_disconnected" | "heartbeat_timeout"
|
|
249
|
+
}
|
|
15
250
|
```
|
|
16
251
|
|
|
17
|
-
|
|
252
|
+
#### 退订
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await client.market.unsubscribeL1Book({
|
|
256
|
+
exchange: "binance",
|
|
257
|
+
symbol: "BTC/USDT:USDT",
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
退订后最后一份快照仍可读,但 `activity` 会变成 `"inactive"`。调用方不应把旧快照当成实时值。
|
|
262
|
+
|
|
263
|
+
### 5. Account(账户余额和仓位)
|
|
264
|
+
|
|
265
|
+
> 当前 account 是占位实现,contract 已稳定但不是完整真实私有流。
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// ① 注册账户(start 之前或之后均可)
|
|
269
|
+
await client.registerAccount({
|
|
270
|
+
accountId: "main-binance",
|
|
271
|
+
exchange: "binance",
|
|
272
|
+
credentials: {
|
|
273
|
+
apiKey: process.env.BINANCE_API_KEY,
|
|
274
|
+
secret: process.env.BINANCE_API_SECRET,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await client.start();
|
|
279
|
+
|
|
280
|
+
// ② 订阅账户数据流
|
|
281
|
+
await client.account.subscribeAccount({ accountId: "main-binance" });
|
|
282
|
+
|
|
283
|
+
// ③ 读取快照
|
|
284
|
+
const snapshot = client.account.getAccountSnapshot("main-binance");
|
|
285
|
+
// → AccountSnapshot | undefined
|
|
286
|
+
|
|
287
|
+
const balances = client.account.getBalances("main-binance");
|
|
288
|
+
// → BalanceSnapshot[]
|
|
289
|
+
|
|
290
|
+
const usdtBalance = client.account.getBalance("main-binance", "USDT");
|
|
291
|
+
// → BalanceSnapshot | undefined
|
|
292
|
+
// { asset: "USDT", free: BigNumber("1000.00"), used: BigNumber("200.00"), total: BigNumber("1200.00"), ... }
|
|
293
|
+
|
|
294
|
+
const positions = client.account.getPositions("main-binance");
|
|
295
|
+
// → PositionSnapshot[]
|
|
296
|
+
|
|
297
|
+
const btcPosition = client.account.getPosition({
|
|
298
|
+
accountId: "main-binance",
|
|
299
|
+
symbol: "BTC/USDT:USDT",
|
|
300
|
+
side: "long", // 可选,不传则返回第一个匹配
|
|
301
|
+
});
|
|
302
|
+
// → PositionSnapshot | undefined
|
|
303
|
+
// { symbol, side, size, entryPrice, markPrice, unrealizedPnl, leverage, ... }
|
|
304
|
+
|
|
305
|
+
const risk = client.account.getRiskSnapshot("main-binance");
|
|
306
|
+
// → RiskSnapshot | undefined
|
|
307
|
+
// { equity, marginRatio, initialMargin, maintenanceMargin, ... }
|
|
308
|
+
|
|
309
|
+
// ④ 消费增量事件
|
|
310
|
+
for await (const event of client.account.events.updates({
|
|
311
|
+
accountId: "main-binance",
|
|
312
|
+
})) {
|
|
313
|
+
switch (event.type) {
|
|
314
|
+
case "balance.updated":
|
|
315
|
+
console.log(event.asset, event.snapshot.free);
|
|
316
|
+
break;
|
|
317
|
+
case "position.updated":
|
|
318
|
+
console.log(event.symbol, event.snapshot.size);
|
|
319
|
+
break;
|
|
320
|
+
case "risk.updated":
|
|
321
|
+
console.log(event.snapshot.marginRatio);
|
|
322
|
+
break;
|
|
323
|
+
case "account.snapshot_replaced":
|
|
324
|
+
console.log("全量快照替换");
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ⑤ 退订 & 移除账户
|
|
330
|
+
await client.account.unsubscribeAccount({ accountId: "main-binance" });
|
|
331
|
+
await client.removeAccount("main-binance");
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 6. Order(订单)
|
|
335
|
+
|
|
336
|
+
> 当前 order 是占位实现,contract 已稳定但不是完整真实私有流。
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
// 订阅订单流(需要先 registerAccount)
|
|
340
|
+
await client.order.subscribeOrders({ accountId: "main-binance" });
|
|
341
|
+
|
|
342
|
+
// 读取所有挂单
|
|
343
|
+
const openOrders = client.order.getOpenOrders("main-binance");
|
|
344
|
+
// → OrderSnapshot[]
|
|
345
|
+
|
|
346
|
+
// 按 symbol 过滤挂单
|
|
347
|
+
const btcOrders = client.order.getOpenOrders("main-binance", "BTC/USDT:USDT");
|
|
348
|
+
|
|
349
|
+
// 按 orderId 或 clientOrderId 查询单笔
|
|
350
|
+
const order = client.order.getOrder({
|
|
351
|
+
accountId: "main-binance",
|
|
352
|
+
orderId: "12345",
|
|
353
|
+
});
|
|
354
|
+
// → OrderSnapshot | undefined
|
|
355
|
+
// { symbol, side, type, status, price, amount, filled, remaining, ... }
|
|
356
|
+
|
|
357
|
+
// 消费订单事件
|
|
358
|
+
for await (const event of client.order.events.updates({
|
|
359
|
+
accountId: "main-binance",
|
|
360
|
+
})) {
|
|
361
|
+
switch (event.type) {
|
|
362
|
+
case "order.updated":
|
|
363
|
+
console.log("订单更新", event.snapshot.status);
|
|
364
|
+
break;
|
|
365
|
+
case "order.filled":
|
|
366
|
+
console.log("完全成交", event.snapshot.avgFillPrice);
|
|
367
|
+
break;
|
|
368
|
+
case "order.canceled":
|
|
369
|
+
console.log("已撤单");
|
|
370
|
+
break;
|
|
371
|
+
case "order.rejected":
|
|
372
|
+
console.log("被拒绝");
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 退订
|
|
378
|
+
await client.order.unsubscribeOrders({ accountId: "main-binance" });
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 7. 健康监控
|
|
382
|
+
|
|
383
|
+
#### 全局健康快照
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const health = client.getHealth();
|
|
387
|
+
// → ClientHealthSnapshot
|
|
388
|
+
// {
|
|
389
|
+
// clientStatus: "running",
|
|
390
|
+
// markets: MarketDataStatus[], // 所有 market 订阅的状态
|
|
391
|
+
// accounts: AccountDataStatus[], // 所有 account 订阅的状态
|
|
392
|
+
// orders: OrderDataStatus[], // 所有 order 订阅的状态
|
|
393
|
+
// updatedAt: 1710000000000,
|
|
394
|
+
// }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### 消费健康事件流
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
for await (const event of client.events.health()) {
|
|
401
|
+
switch (event.type) {
|
|
402
|
+
case "client.status_changed":
|
|
403
|
+
console.log("client 状态变化:", event.status);
|
|
404
|
+
break;
|
|
405
|
+
case "market.status_changed":
|
|
406
|
+
console.log("market 状态变化:", event.exchange, event.symbol);
|
|
407
|
+
break;
|
|
408
|
+
case "account.status_changed":
|
|
409
|
+
console.log("account 状态变化:", event.accountId);
|
|
410
|
+
break;
|
|
411
|
+
case "order.status_changed":
|
|
412
|
+
console.log("order 状态变化:", event.accountId);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
可以按 scope 过滤:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
// 只关心 market 相关的健康变化
|
|
422
|
+
for await (const event of client.events.health({ scope: "market" })) {
|
|
423
|
+
// ...
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 只关心特定交易所
|
|
427
|
+
for await (const event of client.events.health({ exchange: "binance" })) {
|
|
428
|
+
// ...
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### 消费内部错误流
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
for await (const err of client.events.errors()) {
|
|
436
|
+
console.error(`[${err.source}] ${err.error.message}`, {
|
|
437
|
+
exchange: err.exchange,
|
|
438
|
+
symbol: err.symbol,
|
|
439
|
+
accountId: err.accountId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
适合桥接到日志系统或告警系统。
|
|
445
|
+
|
|
446
|
+
### 8. 错误处理
|
|
447
|
+
|
|
448
|
+
SDK 的可预期错误统一通过 `AcexError` 抛出,包含结构化的 `code`:
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { AcexError } from "@imbingox/acex";
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
await client.market.subscribeL1Book({
|
|
455
|
+
exchange: "binance",
|
|
456
|
+
symbol: "INVALID/PAIR",
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof AcexError) {
|
|
460
|
+
switch (error.code) {
|
|
461
|
+
case "CLIENT_NOT_STARTED":
|
|
462
|
+
// client 还没 start()
|
|
463
|
+
break;
|
|
464
|
+
case "MARKET_NOT_FOUND":
|
|
465
|
+
// symbol 不存在
|
|
466
|
+
break;
|
|
467
|
+
case "MARKET_INACTIVE":
|
|
468
|
+
// 市场存在但不可交易
|
|
469
|
+
break;
|
|
470
|
+
case "MARKET_STREAM_TIMEOUT":
|
|
471
|
+
// L1 Book 首条消息超时
|
|
472
|
+
break;
|
|
473
|
+
case "EXCHANGE_NOT_SUPPORTED":
|
|
474
|
+
// 交易所未支持
|
|
475
|
+
break;
|
|
476
|
+
case "MARKET_CATALOG_LOAD_FAILED":
|
|
477
|
+
// market catalog 拉取失败
|
|
478
|
+
break;
|
|
479
|
+
case "ACCOUNT_ALREADY_EXISTS":
|
|
480
|
+
// 重复注册同一个 accountId
|
|
481
|
+
break;
|
|
482
|
+
case "ACCOUNT_NOT_FOUND":
|
|
483
|
+
// accountId 不存在
|
|
484
|
+
break;
|
|
485
|
+
case "CREDENTIALS_MISSING":
|
|
486
|
+
// 私有订阅缺少凭证
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## 完整示例
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
import { AcexError, createClient } from "@imbingox/acex";
|
|
497
|
+
|
|
498
|
+
async function main() {
|
|
499
|
+
const client = createClient({
|
|
500
|
+
market: {
|
|
501
|
+
l1InitialMessageTimeoutMs: 15_000,
|
|
502
|
+
l1StaleAfterMs: 15_000,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await client.start();
|
|
507
|
+
|
|
508
|
+
// 加载市场列表
|
|
509
|
+
await client.market.loadMarkets();
|
|
510
|
+
const symbols = client.market.listMarkets().map((m) => m.symbol);
|
|
511
|
+
console.log(`共 ${symbols.length} 个交易对`);
|
|
512
|
+
|
|
513
|
+
// 订阅 BTC 永续 L1 Book
|
|
514
|
+
const exchange = "binance";
|
|
515
|
+
const symbol = "BTC/USDT:USDT";
|
|
516
|
+
|
|
517
|
+
await client.market.subscribeL1Book({ exchange, symbol });
|
|
518
|
+
|
|
519
|
+
// 读取首个快照
|
|
520
|
+
const book = client.market.getL1Book({ exchange, symbol });
|
|
521
|
+
console.log(`BTC bid=${book?.bidPrice.toFixed()} ask=${book?.askPrice.toFixed()}`);
|
|
522
|
+
|
|
523
|
+
// 持续消费 5 条更新后退出
|
|
524
|
+
let count = 0;
|
|
525
|
+
for await (const event of client.market.events.l1BookUpdates({
|
|
526
|
+
exchange,
|
|
527
|
+
symbol,
|
|
528
|
+
})) {
|
|
529
|
+
console.log(
|
|
530
|
+
`#${++count} bid=${event.snapshot.bidPrice.toFixed()} ask=${event.snapshot.askPrice.toFixed()}`,
|
|
531
|
+
);
|
|
532
|
+
if (count >= 5) break;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
await client.market.unsubscribeL1Book({ exchange, symbol });
|
|
536
|
+
await client.stop();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
main().catch((error) => {
|
|
540
|
+
if (error instanceof AcexError) {
|
|
541
|
+
console.error(error.code, error.message);
|
|
542
|
+
} else {
|
|
543
|
+
console.error(error);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## 调用顺序总结
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
createClient()
|
|
552
|
+
↓
|
|
553
|
+
registerAccount() ← 如需私有能力(可选)
|
|
554
|
+
↓
|
|
555
|
+
client.start()
|
|
556
|
+
↓
|
|
557
|
+
loadMarkets() ← 如需市场列表(可选)
|
|
558
|
+
↓
|
|
559
|
+
subscribe*() ← 开始维护数据
|
|
560
|
+
↓
|
|
561
|
+
get*() / events.*() ← 读快照 / 消费增量
|
|
562
|
+
↓
|
|
563
|
+
unsubscribe*() ← 释放订阅
|
|
564
|
+
↓
|
|
565
|
+
removeAccount() ← 释放账户(可选)
|
|
566
|
+
↓
|
|
567
|
+
client.stop()
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## 核心语义
|
|
571
|
+
|
|
572
|
+
| 概念 | 说明 |
|
|
573
|
+
|---|---|
|
|
574
|
+
| `get*()` | 读 SDK 本地快照,不阻塞、不发网络请求 |
|
|
575
|
+
| `events.*()` | 返回 `AsyncIterable`,持续消费增量变化 |
|
|
576
|
+
| `event.snapshot` vs `get*()` | `event.snapshot` 是事件发生时的快照;`get*()` 是调用时的最新值。跨 symbol 比较建议用 `get*()` |
|
|
577
|
+
| `subscribe*()` | ready barrier — `await` 返回时对应的 `get*()` 已可用 |
|
|
578
|
+
| `events.*()` 与 `subscribe*()` | 独立。`events` 不会隐式触发订阅,必须显式 `subscribe` |
|
|
579
|
+
| 退订后的缓存 | 最后一份快照仍可读,但 `activity` 变为 `"inactive"` |
|
|
580
|
+
|
|
581
|
+
## 当前限制
|
|
582
|
+
|
|
583
|
+
- 运行时真正支持的市场数据交易所只有 **Binance**(`okx`、`bybit`、`gate` 仅类型定义)
|
|
584
|
+
- 真实落地的数据链路只有 Binance **L1 Book**
|
|
585
|
+
- `fundingRate` 接口已暴露,但当前是占位快照
|
|
586
|
+
- `account` / `order` 当前是占位实现,不是完整真实私有流
|
|
587
|
+
- `CreateClientOptions` 中 `sandbox`、`logger`、`logLevel` 仍是预留位
|
|
588
|
+
|
|
589
|
+
## 仓库内开发
|
|
18
590
|
|
|
19
591
|
```bash
|
|
20
|
-
bun
|
|
592
|
+
bun install
|
|
593
|
+
bun run lint
|
|
594
|
+
bun run type-check
|
|
595
|
+
bun test
|
|
21
596
|
```
|
|
22
597
|
|
|
23
|
-
|
|
598
|
+
更完整的公开接口设计说明见 [docs/sdk-public-api.md](./docs/sdk-public-api.md)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -21,5 +21,9 @@
|
|
|
21
21
|
"@biomejs/biome": "^2.4.10",
|
|
22
22
|
"@types/bun": "latest",
|
|
23
23
|
"typescript": "^6.0.2"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@mindfoldhq/trellis": "^0.4.0",
|
|
27
|
+
"bignumber.js": "^11.0.0"
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
1
2
|
import type { MarketDefinition, MarketType } from "../../types/index.ts";
|
|
2
3
|
|
|
3
4
|
type FetchLike = typeof fetch;
|
|
@@ -131,6 +132,7 @@ function normalizeSpotSymbol(
|
|
|
131
132
|
getFilter(symbol.filters, "MIN_NOTIONAL");
|
|
132
133
|
const priceStep = normalizeStep(priceFilter?.tickSize);
|
|
133
134
|
const amountStep = normalizeStep(lotSizeFilter?.stepSize);
|
|
135
|
+
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
134
136
|
|
|
135
137
|
return {
|
|
136
138
|
exchange: "binance",
|
|
@@ -144,10 +146,12 @@ function normalizeSpotSymbol(
|
|
|
144
146
|
contract: false,
|
|
145
147
|
pricePrecision: precisionFromStep(priceStep),
|
|
146
148
|
amountPrecision: precisionFromStep(amountStep),
|
|
147
|
-
priceStep,
|
|
148
|
-
amountStep,
|
|
149
|
-
minAmount: lotSizeFilter?.minQty
|
|
150
|
-
|
|
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,
|
|
151
155
|
raw: toRecord(symbol),
|
|
152
156
|
};
|
|
153
157
|
}
|
|
@@ -173,6 +177,7 @@ function normalizeDerivativesSymbol(
|
|
|
173
177
|
: family === "usdm"
|
|
174
178
|
? "1"
|
|
175
179
|
: undefined;
|
|
180
|
+
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
176
181
|
|
|
177
182
|
return {
|
|
178
183
|
exchange: "binance",
|
|
@@ -193,13 +198,15 @@ function normalizeDerivativesSymbol(
|
|
|
193
198
|
contract: true,
|
|
194
199
|
linear: family === "usdm",
|
|
195
200
|
inverse: family === "coinm",
|
|
196
|
-
contractSize,
|
|
201
|
+
contractSize: contractSize ? new BigNumber(contractSize) : undefined,
|
|
197
202
|
pricePrecision: precisionFromStep(priceStep),
|
|
198
203
|
amountPrecision: precisionFromStep(amountStep),
|
|
199
|
-
priceStep,
|
|
200
|
-
amountStep,
|
|
201
|
-
minAmount: lotSizeFilter?.minQty
|
|
202
|
-
|
|
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,
|
|
203
210
|
expiry: type === "future" ? symbol.deliveryDate : undefined,
|
|
204
211
|
raw: toRecord(symbol),
|
|
205
212
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
1
2
|
import type {
|
|
2
3
|
L1BookStreamCallbacks,
|
|
3
4
|
L1BookStreamOptions,
|
|
@@ -208,13 +209,23 @@ export class MarketManagerImpl
|
|
|
208
209
|
this.updateActivity(record);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
getMarket(symbol: string): MarketDefinition | undefined {
|
|
212
|
-
const market = this.definitions.get(symbol);
|
|
212
|
+
getMarket(exchange: Exchange, symbol: string): MarketDefinition | undefined {
|
|
213
|
+
const market = this.definitions.get(marketKey({ exchange, symbol }));
|
|
213
214
|
return market ? cloneMarketDefinition(market) : undefined;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
findMarkets(symbol: string): MarketDefinition[] {
|
|
217
218
|
return [...this.definitions.values()]
|
|
219
|
+
.filter((market) => market.symbol === symbol)
|
|
220
|
+
.map((market) => cloneMarketDefinition(market));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
listMarkets(exchange?: Exchange): MarketDefinition[] {
|
|
224
|
+
const values = [...this.definitions.values()];
|
|
225
|
+
const filtered = exchange
|
|
226
|
+
? values.filter((market) => market.exchange === exchange)
|
|
227
|
+
: values;
|
|
228
|
+
return filtered
|
|
218
229
|
.sort((left, right) => left.symbol.localeCompare(right.symbol))
|
|
219
230
|
.map((market) => cloneMarketDefinition(market));
|
|
220
231
|
}
|
|
@@ -319,7 +330,7 @@ export class MarketManagerImpl
|
|
|
319
330
|
this.definitions.clear();
|
|
320
331
|
|
|
321
332
|
for (const market of markets) {
|
|
322
|
-
this.definitions.set(market
|
|
333
|
+
this.definitions.set(marketKey(market), market);
|
|
323
334
|
}
|
|
324
335
|
} catch (error) {
|
|
325
336
|
const wrapped = new AcexError(
|
|
@@ -344,7 +355,7 @@ export class MarketManagerImpl
|
|
|
344
355
|
this.assertSupportedExchange(input.exchange);
|
|
345
356
|
await this.loadMarketCatalog();
|
|
346
357
|
|
|
347
|
-
const market = this.definitions.get(input
|
|
358
|
+
const market = this.definitions.get(marketKey(input));
|
|
348
359
|
if (!market) {
|
|
349
360
|
throw this.createError(
|
|
350
361
|
"MARKET_NOT_FOUND",
|
|
@@ -517,10 +528,10 @@ export class MarketManagerImpl
|
|
|
517
528
|
return {
|
|
518
529
|
exchange,
|
|
519
530
|
symbol,
|
|
520
|
-
bidPrice: input.bidPrice,
|
|
521
|
-
bidSize: input.bidSize,
|
|
522
|
-
askPrice: input.askPrice,
|
|
523
|
-
askSize: input.askSize,
|
|
531
|
+
bidPrice: new BigNumber(input.bidPrice),
|
|
532
|
+
bidSize: new BigNumber(input.bidSize),
|
|
533
|
+
askPrice: new BigNumber(input.askPrice),
|
|
534
|
+
askSize: new BigNumber(input.askSize),
|
|
524
535
|
exchangeTs: input.exchangeTs,
|
|
525
536
|
receivedAt: input.receivedAt,
|
|
526
537
|
updatedAt: input.receivedAt,
|
|
@@ -538,7 +549,7 @@ export class MarketManagerImpl
|
|
|
538
549
|
return {
|
|
539
550
|
exchange,
|
|
540
551
|
symbol,
|
|
541
|
-
fundingRate: previous?.fundingRate ??
|
|
552
|
+
fundingRate: previous?.fundingRate ?? new BigNumber(0),
|
|
542
553
|
nextFundingTime: previous?.nextFundingTime,
|
|
543
554
|
markPrice: previous?.markPrice,
|
|
544
555
|
indexPrice: previous?.indexPrice,
|
package/src/types/account.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type BigNumber from "bignumber.js";
|
|
1
2
|
import type {
|
|
2
3
|
Exchange,
|
|
3
4
|
PrivateRuntimeStatus,
|
|
@@ -55,9 +56,9 @@ export interface BalanceSnapshot {
|
|
|
55
56
|
accountId: string;
|
|
56
57
|
exchange: Exchange;
|
|
57
58
|
asset: string;
|
|
58
|
-
free:
|
|
59
|
-
used:
|
|
60
|
-
total:
|
|
59
|
+
free: BigNumber;
|
|
60
|
+
used: BigNumber;
|
|
61
|
+
total: BigNumber;
|
|
61
62
|
exchangeTs?: number;
|
|
62
63
|
receivedAt: number;
|
|
63
64
|
updatedAt: number;
|
|
@@ -69,12 +70,12 @@ export interface PositionSnapshot {
|
|
|
69
70
|
exchange: Exchange;
|
|
70
71
|
symbol: string;
|
|
71
72
|
side: PositionSide;
|
|
72
|
-
size:
|
|
73
|
-
entryPrice?:
|
|
74
|
-
markPrice?:
|
|
75
|
-
unrealizedPnl?:
|
|
76
|
-
leverage?:
|
|
77
|
-
liquidationPrice?:
|
|
73
|
+
size: BigNumber;
|
|
74
|
+
entryPrice?: BigNumber;
|
|
75
|
+
markPrice?: BigNumber;
|
|
76
|
+
unrealizedPnl?: BigNumber;
|
|
77
|
+
leverage?: BigNumber;
|
|
78
|
+
liquidationPrice?: BigNumber;
|
|
78
79
|
exchangeTs?: number;
|
|
79
80
|
receivedAt: number;
|
|
80
81
|
updatedAt: number;
|
|
@@ -84,10 +85,10 @@ export interface PositionSnapshot {
|
|
|
84
85
|
export interface RiskSnapshot {
|
|
85
86
|
accountId: string;
|
|
86
87
|
exchange: Exchange;
|
|
87
|
-
equity?:
|
|
88
|
-
marginRatio?:
|
|
89
|
-
initialMargin?:
|
|
90
|
-
maintenanceMargin?:
|
|
88
|
+
equity?: BigNumber;
|
|
89
|
+
marginRatio?: BigNumber;
|
|
90
|
+
initialMargin?: BigNumber;
|
|
91
|
+
maintenanceMargin?: BigNumber;
|
|
91
92
|
exchangeTs?: number;
|
|
92
93
|
receivedAt: number;
|
|
93
94
|
updatedAt: number;
|
package/src/types/market.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type BigNumber from "bignumber.js";
|
|
1
2
|
import type {
|
|
2
3
|
Exchange,
|
|
3
4
|
MarketFreshness,
|
|
@@ -18,13 +19,13 @@ export interface MarketDefinition {
|
|
|
18
19
|
contract: boolean;
|
|
19
20
|
linear?: boolean;
|
|
20
21
|
inverse?: boolean;
|
|
21
|
-
contractSize?:
|
|
22
|
+
contractSize?: BigNumber;
|
|
22
23
|
pricePrecision: number;
|
|
23
24
|
amountPrecision: number;
|
|
24
|
-
priceStep:
|
|
25
|
-
amountStep:
|
|
26
|
-
minAmount?:
|
|
27
|
-
minNotional?:
|
|
25
|
+
priceStep: BigNumber;
|
|
26
|
+
amountStep: BigNumber;
|
|
27
|
+
minAmount?: BigNumber;
|
|
28
|
+
minNotional?: BigNumber;
|
|
28
29
|
expiry?: number;
|
|
29
30
|
raw: Record<string, unknown>;
|
|
30
31
|
}
|
|
@@ -58,10 +59,10 @@ export interface MarketEventFilter {
|
|
|
58
59
|
export interface L1Book {
|
|
59
60
|
exchange: Exchange;
|
|
60
61
|
symbol: string;
|
|
61
|
-
bidPrice:
|
|
62
|
-
bidSize:
|
|
63
|
-
askPrice:
|
|
64
|
-
askSize:
|
|
62
|
+
bidPrice: BigNumber;
|
|
63
|
+
bidSize: BigNumber;
|
|
64
|
+
askPrice: BigNumber;
|
|
65
|
+
askSize: BigNumber;
|
|
65
66
|
exchangeTs?: number;
|
|
66
67
|
receivedAt: number;
|
|
67
68
|
updatedAt: number;
|
|
@@ -71,10 +72,10 @@ export interface L1Book {
|
|
|
71
72
|
export interface FundingRateSnapshot {
|
|
72
73
|
exchange: Exchange;
|
|
73
74
|
symbol: string;
|
|
74
|
-
fundingRate:
|
|
75
|
+
fundingRate: BigNumber;
|
|
75
76
|
nextFundingTime?: number;
|
|
76
|
-
markPrice?:
|
|
77
|
-
indexPrice?:
|
|
77
|
+
markPrice?: BigNumber;
|
|
78
|
+
indexPrice?: BigNumber;
|
|
78
79
|
exchangeTs?: number;
|
|
79
80
|
receivedAt: number;
|
|
80
81
|
updatedAt: number;
|
|
@@ -128,8 +129,9 @@ export interface MarketManager {
|
|
|
128
129
|
subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
|
|
129
130
|
unsubscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
|
|
130
131
|
|
|
131
|
-
getMarket(symbol: string): MarketDefinition | undefined;
|
|
132
|
-
|
|
132
|
+
getMarket(exchange: Exchange, symbol: string): MarketDefinition | undefined;
|
|
133
|
+
findMarkets(symbol: string): MarketDefinition[];
|
|
134
|
+
listMarkets(exchange?: Exchange): MarketDefinition[];
|
|
133
135
|
getL1Book(key: MarketKeyInput): L1Book | undefined;
|
|
134
136
|
getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined;
|
|
135
137
|
getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined;
|
package/src/types/order.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type BigNumber from "bignumber.js";
|
|
1
2
|
import type { PositionSide } from "./account.ts";
|
|
2
3
|
import type {
|
|
3
4
|
Exchange,
|
|
@@ -69,14 +70,14 @@ export interface OrderSnapshot {
|
|
|
69
70
|
side: OrderSide;
|
|
70
71
|
type: string;
|
|
71
72
|
status: OrderStatus;
|
|
72
|
-
price?:
|
|
73
|
-
triggerPrice?:
|
|
74
|
-
amount:
|
|
75
|
-
filled:
|
|
76
|
-
remaining?:
|
|
73
|
+
price?: BigNumber;
|
|
74
|
+
triggerPrice?: BigNumber;
|
|
75
|
+
amount: BigNumber;
|
|
76
|
+
filled: BigNumber;
|
|
77
|
+
remaining?: BigNumber;
|
|
77
78
|
reduceOnly?: boolean;
|
|
78
79
|
positionSide?: PositionSide;
|
|
79
|
-
avgFillPrice?:
|
|
80
|
+
avgFillPrice?: BigNumber;
|
|
80
81
|
exchangeTs?: number;
|
|
81
82
|
receivedAt: number;
|
|
82
83
|
updatedAt: number;
|