@imbingox/acex 0.3.0-beta.1 → 0.3.0-beta.3

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/docs/api.md ADDED
@@ -0,0 +1,1457 @@
1
+ # acex 使用手册
2
+
3
+ 本手册是 `@imbingox/acex` 的对外参考文档。安装与项目定位见 [README](../README.md)。
4
+
5
+ ## 目录
6
+
7
+ 1. [关于 acex](#1-关于-acex)
8
+ 2. [快速上手](#2-快速上手)
9
+ 3. [核心概念](#3-核心概念)
10
+ 4. [Client 生命周期](#4-client-生命周期)
11
+ 5. [MarketManager](#5-marketmanager)
12
+ 6. [AccountManager](#6-accountmanager)
13
+ 7. [OrderManager](#7-ordermanager)
14
+ 8. [健康与错误事件](#8-健康与错误事件)
15
+ 9. [数据类型参考](#9-数据类型参考)
16
+ 10. [错误处理](#10-错误处理)
17
+ 11. [当前限制](#11-当前限制)
18
+
19
+ ## 1. 关于 acex
20
+
21
+ `acex` 是一个面向交易场景的 **状态型** SDK:调用方只持有一个 `AcexClient`,通过统一的 `market` / `account` / `order` manager 读取最新快照、消费增量事件、观察健康状态,并执行下单/撤单命令。SDK 内部维护本地缓存、ready barrier 和 websocket 生命周期,调用方不需要自己做这些事。
22
+
23
+ SDK 的心智模型是一组三元语义:
24
+
25
+ | 动作 | 语义 |
26
+ |---|---|
27
+ | `subscribe*()` | 让 SDK 开始持续维护这份数据。`await` 返回时,对应 `get*()` 已可用 |
28
+ | `get*()` | 读取本地快照。不走网络、不阻塞 |
29
+ | `events.*()` | 订阅增量事件流。只消费,不会隐式触发 `subscribe` |
30
+
31
+ 当前 MVP 阶段覆盖:Binance 现货与 USDⓈ-M / COIN-M 合约的 L1 Book、Binance 永续合约 Funding Rate,Binance PAPI UM 私有链路的账户与订单,Juplend 只读借贷账户视图,以及第一版下单/撤单命令。详见 [§11 当前限制](#11-当前限制)。
32
+
33
+ ## 2. 快速上手
34
+
35
+ ```bash
36
+ bun add @imbingox/acex
37
+ ```
38
+
39
+ ```ts
40
+ import { createClient } from "@imbingox/acex";
41
+
42
+ const client = createClient();
43
+ await client.start();
44
+
45
+ await client.market.subscribeL1Book({
46
+ venue: "binance",
47
+ symbol: "BTC/USDT:USDT",
48
+ });
49
+
50
+ const book = client.market.getL1Book({
51
+ venue: "binance",
52
+ symbol: "BTC/USDT:USDT",
53
+ });
54
+ console.log(`bid=${book?.bidPrice.toFixed()} ask=${book?.askPrice.toFixed()}`);
55
+
56
+ for await (const event of client.market.events.l1BookUpdates({
57
+ venue: "binance",
58
+ symbol: "BTC/USDT:USDT",
59
+ })) {
60
+ console.log(event.snapshot.bidPrice.toFixed());
61
+ break;
62
+ }
63
+
64
+ await client.stop();
65
+ ```
66
+
67
+ 同一个 client 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户。`account.juplend.pollIntervalMs` 只是 Juplend polling 配置,不会把 client 限定为 Juplend 专用:
68
+
69
+ ```ts
70
+ const client = createClient({
71
+ account: {
72
+ juplend: {
73
+ pollIntervalMs: 30_000,
74
+ },
75
+ },
76
+ });
77
+ await client.start();
78
+
79
+ await client.registerAccount({
80
+ accountId: "main-binance",
81
+ venue: "binance",
82
+ credentials: {
83
+ apiKey: process.env.BINANCE_PAPI_API_KEY,
84
+ secret: process.env.BINANCE_PAPI_SECRET,
85
+ },
86
+ });
87
+
88
+ await client.registerAccount({
89
+ accountId: "jup-loop-a",
90
+ venue: "juplend",
91
+ credentials: { apiKey: process.env.JUPITER_API_KEY! },
92
+ options: {
93
+ walletAddress: "<solana-wallet-address>",
94
+ positionId: "<optional-nft-position-id>",
95
+ },
96
+ });
97
+
98
+ await client.account.subscribeAccount({ accountId: "main-binance" });
99
+ await client.account.subscribeAccount({ accountId: "jup-loop-a" });
100
+
101
+ const binanceRisk = client.account.getRiskSnapshot("main-binance");
102
+ const juplendRisk = client.account.getRiskSnapshot("jup-loop-a");
103
+
104
+ console.log(binanceRisk?.riskRatio?.toFixed());
105
+ console.log(juplendRisk?.riskRatio?.toFixed());
106
+ ```
107
+
108
+ 需要账户或订单能力时,在 `start()` 前后任意时刻 `registerAccount()`:
109
+
110
+ ```ts
111
+ const client = createClient();
112
+ await client.start();
113
+
114
+ await client.registerAccount({
115
+ accountId: "main-binance",
116
+ venue: "binance",
117
+ credentials: {
118
+ apiKey: process.env.BINANCE_PAPI_API_KEY,
119
+ secret: process.env.BINANCE_PAPI_SECRET,
120
+ },
121
+ });
122
+
123
+ await client.account.subscribeAccount({ accountId: "main-binance" });
124
+ await client.order.subscribeOrders({ accountId: "main-binance" });
125
+
126
+ await client.stop();
127
+ ```
128
+
129
+ ## 3. 核心概念
130
+
131
+ ### 3.1 状态型 SDK
132
+
133
+ SDK 本地维护最新快照。读快照用 `get*()`,**不会** 触发网络请求。这意味着:
134
+
135
+ - `get*()` 返回 `undefined` 表示「从未订阅」或「首次快照还没到」
136
+ - 跨 symbol 做决策(套利、对冲)时,在事件回调里用 `get*()` 拿各 symbol 最新值,比读 `event.snapshot` 更一致
137
+
138
+ ### 3.2 subscribe 是 ready barrier
139
+
140
+ ```ts
141
+ await client.market.subscribeL1Book({ venue, symbol });
142
+ // await 返回之后,getL1Book({ venue, symbol }) 一定已经有值
143
+ ```
144
+
145
+ `subscribe*()` 会等首条可用快照到达后才 resolve。超时则抛出 `MARKET_STREAM_TIMEOUT`。默认超时 15s,可通过 `CreateClientOptions.market.l1InitialMessageTimeoutMs` 调整。
146
+
147
+ ### 3.2.1 subscribe / unsubscribe 行为
148
+
149
+ - 同一个 `(venue, symbol)` 反复调用 `subscribe*()` 是幂等的:已存在的流不会重复创建,只会继续等待原有流进入 ready
150
+ - `unsubscribe*()` 可以和 `subscribe*()` 动态交替调用;退订后该 stream 停止维护,但 `get*()` 仍可读到最后一份快照
151
+ - `unsubscribe*()` 对未订阅目标是安全的 no-op
152
+ - 退订后再次 `subscribe*()` 会重新恢复该 stream 的维护与更新
153
+ - `subscribeL1Book()` 和 `subscribeFundingRate()` 彼此独立;退订其中一个不会影响另一个
154
+
155
+ ### 3.3 event vs snapshot
156
+
157
+ `event.snapshot` 是事件发生那一刻的快照。由于事件是异步消费的,你在 `for await` 处理时,SDK 内部状态可能已经更新到下一版。因此:
158
+
159
+ - 单 symbol 场景,直接用 `event.snapshot` 即可
160
+ - 跨 symbol 决策场景,把事件当触发器,用 `get*()` 读所有 symbol 的当下值
161
+
162
+ ### 3.4 activity vs freshness vs runtime status
163
+
164
+ | 字段 | 语义 | 出现在 |
165
+ |---|---|---|
166
+ | `activity` | `"active"` 表示 SDK 仍在维护;`"inactive"` 表示已退订或未订阅 | 所有 `*DataStatus` |
167
+ | `freshness` | market 数据的新鲜度:`"fresh"` / `"stale"` / `"reconciling"` | `MarketDataStatus` |
168
+ | `runtimeStatus` | 私有链路运行态:`"bootstrap_pending"` / `"healthy"` / `"degraded"` / `"reconnecting"` / `"reconciling"` / `"stopped"` | `AccountDataStatus`、`OrderDataStatus` |
169
+
170
+ 退订后 `activity` 变为 `"inactive"`,但最后一份快照仍可读——不要把它当实时值。
171
+
172
+ ### 3.5 BigNumber 约定
173
+
174
+ 输出侧的价格、数量、金额统一是 `BigNumber`(来自 [bignumber.js](https://github.com/MikeMcl/bignumber.js),SDK 已 re-export):
175
+
176
+ ```ts
177
+ import { BigNumber } from "@imbingox/acex";
178
+
179
+ const book = client.market.getL1Book({ venue, symbol });
180
+ const spread = book!.askPrice.minus(book!.bidPrice); // BigNumber
181
+ console.log(spread.toFixed());
182
+ ```
183
+
184
+ **输入侧不对称**:`createOrder()` 的 `price` / `amount` 仍接受 decimal string。这是为了让调用方直接从交易所精度(`MarketDefinition.priceStep` / `amountStep`)做字符串格式化,不必先转 BigNumber 再转字符串。
185
+
186
+ ## 4. Client 生命周期
187
+
188
+ ### 4.1 `createClient(options?)`
189
+
190
+ ```ts
191
+ function createClient(options?: CreateClientOptions): AcexClient;
192
+ ```
193
+
194
+ 只创建对象,不建立任何网络连接。`CreateClientOptions` 见 [§9 数据类型参考](#9-数据类型参考)。
195
+
196
+ 运行时真正生效的配置当前是 `market.*` 与 `account.*`:
197
+
198
+ ```ts
199
+ const client = createClient({
200
+ market: {
201
+ l1InitialMessageTimeoutMs: 15_000,
202
+ l1StaleAfterMs: 15_000,
203
+ l1ReconnectDelayMs: 1_000,
204
+ l1ReconnectMaxDelayMs: 10_000,
205
+ },
206
+ account: {
207
+ streamOpenTimeoutMs: 15_000,
208
+ streamReconnectDelayMs: 1_000,
209
+ streamReconnectMaxDelayMs: 10_000,
210
+ listenKeyKeepAliveMs: 30 * 60_000,
211
+ juplend: {
212
+ pollIntervalMs: 30_000,
213
+ },
214
+ },
215
+ });
216
+ ```
217
+
218
+ `sandbox`、`logger`、`logLevel` 是预留位,当前不生效。
219
+
220
+ ### 4.2 `start()` / `stop()`
221
+
222
+ ```ts
223
+ await client.start();
224
+ // ...
225
+ await client.stop();
226
+ await client.stop({ graceful: true, timeoutMs: 5_000 });
227
+ ```
228
+
229
+ Client 状态机:`idle → starting → running → stopping → stopped`,可通过 `client.getStatus()` 读取。`start()` / `stop()` 都幂等。
230
+
231
+ 在 `start()` 之前调 `subscribe*()` 会直接失败,抛 `CLIENT_NOT_STARTED`。
232
+
233
+ ### 4.3 Venue capabilities
234
+
235
+ ```ts
236
+ const binance = client.getVenueCapabilities("binance");
237
+ const allVenues = client.listVenueCapabilities();
238
+ ```
239
+
240
+ `getVenueCapabilities()` / `listVenueCapabilities()` 不需要 `start()`,也不会建立网络连接。返回值表达的是 **当前 SDK runtime 已实现能力**,不是交易所官网完整能力,也不会检查 API key 是否开通交易权限。
241
+
242
+ 典型用途是在 UI 或策略启动前预检查 venue 能力:
243
+
244
+ ```ts
245
+ if (!client.getVenueCapabilities("juplend").order.supported) {
246
+ // Juplend 是只读 lending 账户,不能通过 OrderManager 下单
247
+ }
248
+ ```
249
+
250
+ 当前 venue 级能力摘要:
251
+
252
+ | Venue | runtimeStatus | readOnly | Market | Account | Order |
253
+ |---|---|---:|---|---|---|
254
+ | `binance` | `available` | false | catalog / L1 支持;funding rate 为 `market_dependent` | WebSocket 私有账户流 | 支持 `limit` / `market` 下单、撤单、按 symbol 全撤 |
255
+ | `juplend` | `available` | true | 不支持 | polling 只读 lending 账户 | 不支持,`reason = "read_only"` |
256
+ | `okx` / `bybit` / `gate` | `type_only` | false | 当前未实现 | 当前未实现 | 当前未实现,`reason = "not_implemented"` |
257
+
258
+ 第一版只提供 venue 级查询,不提供 symbol/market 级 capability。像 funding rate 这类依赖 market type 的能力会用 `market_dependent` 表达,具体 symbol 仍以实际 `subscribeFundingRate()` / `MARKET_FUNDING_RATE_UNSUPPORTED` 为准。
259
+
260
+ Binance 的 `order.supported = true` 只表示当前 SDK 有 Binance 订单命令链路;第一版命令固定走 Binance PAPI UM,主要面向 USDⓈ-M symbol,不代表 Binance spot、COIN-M 或交割合约都可通过 `OrderManager` 下单。需要按具体 symbol 预检时,应等后续 market-level capability。
261
+
262
+ ### 4.4 账户注册
263
+
264
+ ```ts
265
+ await client.registerAccount({
266
+ accountId: "main-binance",
267
+ venue: "binance",
268
+ credentials: { apiKey, secret },
269
+ });
270
+
271
+ await client.updateAccountCredentials("main-binance", { apiKey, secret });
272
+
273
+ await client.removeAccount("main-binance");
274
+ ```
275
+
276
+ `RegisterAccountInput` 是按 `venue` 区分的 union。不同 venue 的初始化参数不同,TypeScript 会在注册时提示/校验:
277
+
278
+ ```ts
279
+ await client.registerAccount({
280
+ accountId: "main-binance",
281
+ venue: "binance",
282
+ credentials: { apiKey, secret },
283
+ options: {
284
+ recvWindow: 5000,
285
+ timestamp: Date.now(),
286
+ },
287
+ });
288
+
289
+ await client.registerAccount({
290
+ accountId: "jup-loop-a",
291
+ venue: "juplend",
292
+ credentials: { apiKey: jupiterApiKey },
293
+ options: {
294
+ walletAddress,
295
+ positionId: "101", // 可选;不传则聚合该钱包全部 Juplend positions
296
+ },
297
+ });
298
+ ```
299
+
300
+ 约束:
301
+
302
+ - `accountId` 在单个 `AcexClient` 实例内全局唯一。重复注册抛 `ACCOUNT_ALREADY_EXISTS`
303
+ - 凭证校验发生在 `subscribeAccount()` / `subscribeOrders()` 时,不是注册时
304
+ - `updateAccountCredentials()` 可以在私有订阅活跃时调用,SDK 会按需重建私有链路
305
+ - `removeAccount()` 比 `unsubscribeAccount()` 更彻底:账户配置、凭证、账户级缓存都会清理
306
+ - Juplend 的 `accountId` 是自定义逻辑账户名;Solana 钱包地址必须放在 `options.walletAddress`,可用 `options.positionId` 把同钱包下单个 NFT position 映射为独立账户
307
+
308
+ ### 4.5 `getStatus()` / `getHealth()`
309
+
310
+ ```ts
311
+ client.getStatus(); // ClientStatus
312
+ client.getHealth(); // ClientHealthSnapshot(聚合所有 market/account/order 状态)
313
+ ```
314
+
315
+ ## 5. MarketManager
316
+
317
+ ```ts
318
+ interface MarketManager {
319
+ readonly events: MarketEventStreams;
320
+
321
+ loadMarkets(): Promise<void>;
322
+ listMarkets(venue?: Venue): MarketDefinition[];
323
+ getMarket(venue: Venue, symbol: string): MarketDefinition | undefined;
324
+ getMarkets(symbol: string): MarketDefinition[];
325
+ normalizeOrderInput(input: NormalizeOrderInputInput): NormalizedOrderInput;
326
+
327
+ subscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
328
+ unsubscribeL1Book(input: SubscribeL1BookInput): Promise<void>;
329
+ getL1Book(key: MarketKeyInput): L1Book | undefined;
330
+ getL1Books(symbol: string): L1Book[];
331
+
332
+ subscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
333
+ unsubscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
334
+ getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined;
335
+ getFundingRates(symbol: string): FundingRateSnapshot[];
336
+
337
+ getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined;
338
+ }
339
+ ```
340
+
341
+ ### 5.1 Market catalog
342
+
343
+ ```ts
344
+ await client.market.loadMarkets();
345
+
346
+ const all = client.market.listMarkets();
347
+ const binanceOnly = client.market.listMarkets("binance");
348
+
349
+ const btcPerp = client.market.getMarket("binance", "BTC/USDT:USDT");
350
+ const allBtcPerp = client.market.getMarkets("BTC/USDT:USDT");
351
+ ```
352
+
353
+ `getMarkets(symbol)` 严格按完整统一 symbol 匹配。
354
+
355
+ `MarketDefinition` 见 [§9](#9-数据类型参考)。价格/数量相关字段(`priceStep`、`amountStep`、`contractSize`、`minAmount`、`minNotional`)都是 `BigNumber`。
356
+
357
+ 归一化下单价格和数量:
358
+
359
+ ```ts
360
+ await client.market.loadMarkets();
361
+
362
+ const normalized = client.market.normalizeOrderInput({
363
+ venue: "binance",
364
+ symbol: "BTC/USDT:USDT",
365
+ price: "101000.123456789",
366
+ amount: "0.010987654321",
367
+ });
368
+
369
+ if (normalized.accepted) {
370
+ await client.order.createOrder({
371
+ accountId: "main-binance",
372
+ symbol: "BTC/USDT:USDT",
373
+ side: "buy",
374
+ type: "limit",
375
+ price: normalized.price,
376
+ amount: normalized.amount,
377
+ });
378
+ }
379
+ ```
380
+
381
+ `normalizeOrderInput()` 会按该 market 的 `priceStep` / `amountStep` 向下取整,返回 decimal string,避免浮点科学计数法;并基于归一化后的值检查 `minAmount` / `minNotional`。
382
+
383
+ 如果归一化后的结果不满足最小下单条件,接口不会抛错,而是返回 `accepted: false` 和 `rejectReason`,调用方应避免继续下单:
384
+
385
+ ```ts
386
+ const normalized = client.market.normalizeOrderInput({
387
+ venue: "binance",
388
+ symbol: "BTC/USDT:USDT",
389
+ price: "1000.09",
390
+ amount: "0.0049",
391
+ });
392
+
393
+ // 示例返回:
394
+ // {
395
+ // price: "1000",
396
+ // amount: "0.004",
397
+ // rawPrice: "1000.09",
398
+ // rawAmount: "0.0049",
399
+ // adjusted: true,
400
+ // accepted: false,
401
+ // rejectReason: "notional_below_min",
402
+ // priceStep: "0.1",
403
+ // amountStep: "0.001",
404
+ // minAmount: "0.001",
405
+ // minNotional: "5"
406
+ // }
407
+ ```
408
+
409
+ `rejectReason` 当前可能是:`price_not_positive`、`amount_not_positive`、`amount_below_min`、`notional_below_min`。
410
+
411
+ **统一 symbol 约定:**
412
+
413
+ | 格式 | 含义 | 示例 |
414
+ |---|---|---|
415
+ | `BASE/QUOTE` | 现货 | `BTC/USDT` |
416
+ | `BASE/QUOTE:SETTLE` | USDⓈ-M 永续 | `BTC/USDT:USDT` |
417
+ | `BASE/USD:BASE` | COIN-M 永续 | `BTC/USD:BTC` |
418
+ | `BASE/USD:BASE-YYYYMMDD` | COIN-M 交割 | `BTC/USD:BTC-20250627` |
419
+
420
+ `subscribeL1Book()` 内部会自动确保 catalog 已加载,所以不必手动先 `loadMarkets()`;只在需要枚举或读取精度字段时主动调用。
421
+
422
+ ### 5.2 L1 Book
423
+
424
+ ```ts
425
+ await client.market.subscribeL1Book({
426
+ venue: "binance",
427
+ symbol: "BTC/USDT:USDT",
428
+ });
429
+
430
+ const book = client.market.getL1Book({
431
+ venue: "binance",
432
+ symbol: "BTC/USDT:USDT",
433
+ });
434
+
435
+ if (book) {
436
+ const spread = book.askPrice.minus(book.bidPrice);
437
+ console.log(`spread=${spread.toFixed()}`);
438
+ }
439
+ ```
440
+
441
+ 消费增量事件:
442
+
443
+ ```ts
444
+ for await (const event of client.market.events.l1BookUpdates({
445
+ venue: "binance",
446
+ symbol: "BTC/USDT:USDT",
447
+ })) {
448
+ console.log(event.snapshot.bidPrice.toFixed());
449
+ }
450
+ ```
451
+
452
+ 不传 filter 会拿到所有 symbol 的更新:
453
+
454
+ ```ts
455
+ for await (const event of client.market.events.l1BookUpdates()) {
456
+ console.log(event.venue, event.symbol);
457
+ }
458
+ ```
459
+
460
+ **事件当触发器模式**(跨 symbol 决策推荐):
461
+
462
+ ```ts
463
+ const pairs = [
464
+ { venue: "binance", symbol: "BTC/USDT:USDT" },
465
+ { venue: "binance", symbol: "BTC/USD:BTC" },
466
+ ];
467
+
468
+ for (const pair of pairs) await client.market.subscribeL1Book(pair);
469
+
470
+ for await (const _ of client.market.events.l1BookUpdates()) {
471
+ const books = pairs.map((p) => ({ ...p, book: client.market.getL1Book(p) }));
472
+ if (books.some((b) => !b.book)) continue;
473
+ // 用 books 里的最新值做决策
474
+ }
475
+ ```
476
+
477
+ 退订:
478
+
479
+ ```ts
480
+ await client.market.unsubscribeL1Book({
481
+ venue: "binance",
482
+ symbol: "BTC/USDT:USDT",
483
+ });
484
+ ```
485
+
486
+ 退订后最后一份快照仍可读,但 `getMarketStatus().activity` 变为 `"inactive"`。
487
+
488
+ L1 Book 支持动态重复 `subscribe` / `unsubscribe`;对同一个 market 重复 `subscribeL1Book()` 不会重复开流,退订后再次订阅会恢复维护。
489
+
490
+ ### 5.3 Funding Rate
491
+
492
+ Funding Rate 当前通过 Binance mark price websocket 实时更新,仅支持永续合约(`MarketDefinition.type === "swap"`)。订阅 spot 或交割合约会抛出 `MARKET_FUNDING_RATE_UNSUPPORTED`。
493
+
494
+ ```ts
495
+ await client.market.subscribeFundingRate({
496
+ venue: "binance",
497
+ symbol: "BTC/USDT:USDT",
498
+ });
499
+
500
+ const funding = client.market.getFundingRate({
501
+ venue: "binance",
502
+ symbol: "BTC/USDT:USDT",
503
+ });
504
+
505
+ if (funding) {
506
+ console.log(funding.fundingRate.toFixed());
507
+ console.log(funding.markPrice?.toFixed());
508
+ console.log(funding.indexPrice?.toFixed());
509
+ console.log(funding.nextFundingTime);
510
+ console.log(funding.status.freshness);
511
+ }
512
+ ```
513
+
514
+ 消费增量事件:
515
+
516
+ ```ts
517
+ for await (const event of client.market.events.fundingRateUpdates({
518
+ venue: "binance",
519
+ symbol: "BTC/USDT:USDT",
520
+ })) {
521
+ console.log(event.snapshot.fundingRate.toFixed());
522
+ }
523
+ ```
524
+
525
+ 字段映射来自 Binance mark price stream:`r` → `fundingRate`,`p` → `markPrice`,`i` → `indexPrice`,`T` → `nextFundingTime`,`E` → `exchangeTs`。
526
+
527
+ Funding Rate 也支持动态重复 `subscribe` / `unsubscribe`;对同一个 market 重复 `subscribeFundingRate()` 不会重复开流,退订后再次订阅会恢复维护。
528
+
529
+ ### 5.4 订阅状态
530
+
531
+ ```ts
532
+ const status = client.market.getMarketStatus({
533
+ venue: "binance",
534
+ symbol: "BTC/USDT:USDT",
535
+ });
536
+
537
+ if (status) {
538
+ status.activity; // "active" | "inactive"
539
+ status.ready; // 首次 ready 是否完成
540
+ status.freshness; // "fresh" | "stale" | "reconciling"
541
+ status.lastReceivedAt; // 最后收到数据的时间
542
+ status.reason; // "ws_disconnected" | "heartbeat_timeout" | "reconciling"
543
+ }
544
+ ```
545
+
546
+ `getMarketStatus()` 是 `(venue, symbol)` 级聚合状态:同一个 market 下任意 active stream(例如 L1 Book 或 Funding Rate)变 stale,聚合状态也会 stale。更精确的下游判断应优先读取快照自带的 stream 级状态:
547
+
548
+ ```ts
549
+ const book = client.market.getL1Book({ venue, symbol });
550
+ book?.status.freshness; // 只代表 L1 Book stream
551
+
552
+ const funding = client.market.getFundingRate({ venue, symbol });
553
+ funding?.status.freshness; // 只代表 Funding Rate stream
554
+ ```
555
+
556
+ `freshness` 区分两种异常:
557
+
558
+ - `ws_disconnected`:底层连接已断
559
+ - `heartbeat_timeout`:连接仍在但长时间没收到消息
560
+
561
+ 自动重连由 SDK 负责,调用方不需要手工处理。
562
+
563
+ ## 6. AccountManager
564
+
565
+ ```ts
566
+ interface AccountManager {
567
+ readonly events: AccountEventStreams;
568
+
569
+ subscribeAccount(input: SubscribeAccountInput): Promise<void>;
570
+ unsubscribeAccount(input: UnsubscribeAccountInput): Promise<void>;
571
+
572
+ getAccountSnapshot(accountId: string): AccountSnapshot | undefined;
573
+ getBalances(accountId: string): BalanceSnapshot[];
574
+ getBalance(accountId: string, asset: string): BalanceSnapshot | undefined;
575
+ getPositions(accountId: string, symbol?: string): PositionSnapshot[];
576
+ getPosition(input: PositionKeyInput): PositionSnapshot | undefined;
577
+ getRiskSnapshot(accountId: string): RiskSnapshot | undefined;
578
+ getAccountStatus(accountId: string): AccountDataStatus | undefined;
579
+ }
580
+ ```
581
+
582
+ ### 6.1 订阅与退订
583
+
584
+ ```ts
585
+ await client.account.subscribeAccount({ accountId: "main-binance" });
586
+ await client.account.unsubscribeAccount({ accountId: "main-binance" });
587
+ ```
588
+
589
+ - 调用前需要先 `registerAccount()`
590
+ - 凭证不足抛 `CREDENTIALS_MISSING`
591
+ - bootstrap 失败抛 `ACCOUNT_BOOTSTRAP_FAILED`
592
+
593
+ ### 6.2 读快照
594
+
595
+ ```ts
596
+ const snapshot = client.account.getAccountSnapshot("main-binance");
597
+ // AccountSnapshot.balances 是 Record<string, BalanceSnapshot>(按 asset 索引)
598
+
599
+ const balances = client.account.getBalances("main-binance");
600
+ // BalanceSnapshot[](数组视图)
601
+
602
+ const usdt = client.account.getBalance("main-binance", "USDT");
603
+ // BalanceSnapshot | undefined
604
+
605
+ const positions = client.account.getPositions("main-binance");
606
+ const btcPosition = client.account.getPosition({
607
+ accountId: "main-binance",
608
+ symbol: "BTC/USDT:USDT",
609
+ side: "long", // 双向持仓时必传;单向持仓可省略
610
+ });
611
+
612
+ const risk = client.account.getRiskSnapshot("main-binance");
613
+ ```
614
+
615
+ 所有数量字段(`free` / `used` / `total` / `size` / `entryPrice` / `equity` / ...)都是 `BigNumber`。
616
+
617
+ > **注意**:`AccountSnapshot.balances` 是 `Record<string, BalanceSnapshot>`,不是数组;需要数组视图用 `getBalances()`。
618
+
619
+ ### 6.3 事件
620
+
621
+ ```ts
622
+ for await (const event of client.account.events.updates({
623
+ accountId: "main-binance",
624
+ })) {
625
+ switch (event.type) {
626
+ case "balance.updated":
627
+ console.log(event.asset, event.snapshot.free.toFixed());
628
+ break;
629
+ case "position.updated":
630
+ console.log(event.symbol, event.snapshot.size.toFixed());
631
+ break;
632
+ case "risk.updated":
633
+ console.log(event.snapshot.riskRatio?.toFixed());
634
+ break;
635
+ case "account.snapshot_replaced":
636
+ // 私有链路重连/重对账后的全量替换
637
+ break;
638
+ }
639
+ }
640
+ ```
641
+
642
+ ### 6.4 订阅状态
643
+
644
+ ```ts
645
+ const status = client.account.getAccountStatus("main-binance");
646
+ status?.runtimeStatus;
647
+ // "bootstrap_pending" | "healthy" | "degraded" | "reconnecting" | "reconciling" | "stopped"
648
+ status?.reason;
649
+ // "credentials_missing" | "auth_failed" | "http_failed" | "rate_limited" | "ws_disconnected" | "heartbeat_timeout" | "reconciling"
650
+ ```
651
+
652
+ ## 7. OrderManager
653
+
654
+ ```ts
655
+ interface OrderManager {
656
+ readonly events: OrderEventStreams;
657
+
658
+ subscribeOrders(input: SubscribeOrdersInput): Promise<void>;
659
+ unsubscribeOrders(input: UnsubscribeOrdersInput): Promise<void>;
660
+
661
+ createOrder(input: CreateOrderInput): Promise<OrderSnapshot>;
662
+ cancelOrder(input: CancelOrderInput): Promise<OrderSnapshot>;
663
+ cancelAllOrders(input: CancelAllOrdersInput): Promise<OrderSnapshot[]>;
664
+
665
+ getOrder(input: GetOrderInput): OrderSnapshot | undefined;
666
+ getOpenOrders(accountId: string, symbol?: string): OrderSnapshot[];
667
+ getOrderStatus(accountId: string): OrderDataStatus | undefined;
668
+ }
669
+ ```
670
+
671
+ ### 7.1 订阅订单流
672
+
673
+ ```ts
674
+ await client.order.subscribeOrders({ accountId: "main-binance" });
675
+ await client.order.unsubscribeOrders({ accountId: "main-binance" });
676
+ ```
677
+
678
+ - 需要先 `registerAccount()`
679
+ - 凭证不足抛 `CREDENTIALS_MISSING`
680
+ - bootstrap(open orders 拉取)失败抛 `ORDER_BOOTSTRAP_FAILED`
681
+
682
+ ### 7.2 读快照
683
+
684
+ ```ts
685
+ const openOrders = client.order.getOpenOrders("main-binance");
686
+ const btcOrders = client.order.getOpenOrders("main-binance", "BTC/USDT:USDT");
687
+
688
+ const order = client.order.getOrder({
689
+ accountId: "main-binance",
690
+ orderId: "12345",
691
+ // 或 clientOrderId: "my-order-1"
692
+ });
693
+
694
+ const status = client.order.getOrderStatus("main-binance");
695
+ ```
696
+
697
+ ### 7.3 下单
698
+
699
+ `createOrder()` 第一版支持 `limit` / `market` 两种类型。`price` / `amount` 是 decimal string。
700
+
701
+ ```ts
702
+ const limit = await client.order.createOrder({
703
+ accountId: "main-binance",
704
+ symbol: "BTC/USDT:USDT",
705
+ side: "buy",
706
+ type: "limit",
707
+ price: "71830.6",
708
+ amount: "0.001",
709
+ clientOrderId: "my-order-1", // 可选
710
+ reduceOnly: false, // 可选
711
+ postOnly: true, // 可选:仅限 limit,Binance 映射为 GTX
712
+ });
713
+
714
+ const market = await client.order.createOrder({
715
+ accountId: "main-binance",
716
+ symbol: "BTC/USDT:USDT",
717
+ side: "sell",
718
+ type: "market",
719
+ amount: "0.001",
720
+ });
721
+ ```
722
+
723
+ **双向持仓模式(hedge mode)必须显式传 `positionSide`**:
724
+
725
+ ```ts
726
+ const hedge = await client.order.createOrder({
727
+ accountId: "main-binance",
728
+ symbol: "BTC/USDT:USDT",
729
+ side: "buy",
730
+ type: "limit",
731
+ price: "71900.9",
732
+ amount: "0.001",
733
+ positionSide: "long", // "long" | "short"
734
+ });
735
+ ```
736
+
737
+ 单向持仓模式可以省略 `positionSide`,返回的 snapshot 通常归一成 `"net"`。
738
+
739
+ `postOnly` 仅对 `limit` 单有效;当前 Binance PAPI UM adapter 会把普通 limit 单映射为 `timeInForce=GTC`,把 `postOnly: true` 映射为 `timeInForce=GTX`。
740
+
741
+ 失败时抛 `ORDER_CREATE_FAILED`;输入本身不合法(比如 limit 单缺 price)抛 `ORDER_INPUT_INVALID`。
742
+
743
+ ### 7.4 撤单
744
+
745
+ ```ts
746
+ const canceled = await client.order.cancelOrder({
747
+ accountId: "main-binance",
748
+ symbol: "BTC/USDT:USDT",
749
+ orderId: "12345",
750
+ // 或 clientOrderId: "my-order-1"
751
+ });
752
+
753
+ const batch = await client.order.cancelAllOrders({
754
+ accountId: "main-binance",
755
+ symbol: "BTC/USDT:USDT", // 当前必填,不支持账户级全撤
756
+ });
757
+ ```
758
+
759
+ `cancelOrder()` 要求 `orderId` / `clientOrderId` 至少一个,否则本地校验失败抛 `ORDER_INPUT_INVALID`。命令失败抛 `ORDER_CANCEL_FAILED` / `ORDER_CANCEL_ALL_FAILED`。
760
+
761
+ ### 7.5 命令结果 vs 事件流
762
+
763
+ - `createOrder()` / `cancelOrder()` resolve 的是 **REST 成功后标准化的 `OrderSnapshot`**
764
+ - `events.updates()` 是订单的 **后续生命周期变化流**,不是唯一 ack 来源
765
+
766
+ 也就是说,命令 resolve 不代表订单已终结。想追踪完整生命周期(部分成交 → 完全成交 / 撤销 / 拒绝)要同时消费事件。
767
+
768
+ ### 7.6 事件
769
+
770
+ ```ts
771
+ for await (const event of client.order.events.updates({
772
+ accountId: "main-binance",
773
+ })) {
774
+ switch (event.type) {
775
+ case "order.updated":
776
+ console.log("更新", event.snapshot.status, event.snapshot.filled.toFixed());
777
+ break;
778
+ case "order.filled":
779
+ console.log("全部成交", event.snapshot.avgFillPrice?.toFixed());
780
+ break;
781
+ case "order.canceled":
782
+ console.log("已撤单");
783
+ break;
784
+ case "order.rejected":
785
+ console.log("被拒绝");
786
+ break;
787
+ case "order.snapshot_replaced":
788
+ // 私有链路重连/重对账后的全量订单集合替换
789
+ break;
790
+ }
791
+ }
792
+ ```
793
+
794
+ ### 7.7 Binance PAPI UM 精度约束
795
+
796
+ `price` / `amount` 必须满足 `MarketDefinition.priceStep`、`amountStep`、`minAmount`、`minNotional`。**SDK 第一版不自动纠偏**——调用方自己用这些字段做字符串格式化。违反约束的订单会被交易所拒绝,SDK 对外表现为对应命令失败。
797
+
798
+ ## 8. 健康与错误事件
799
+
800
+ ### 8.1 `getHealth()`
801
+
802
+ ```ts
803
+ const health = client.getHealth();
804
+ // {
805
+ // clientStatus: "running",
806
+ // markets: MarketDataStatus[],
807
+ // accounts: AccountDataStatus[],
808
+ // orders: OrderDataStatus[],
809
+ // updatedAt: 1710000000000,
810
+ // }
811
+ ```
812
+
813
+ ### 8.2 `events.health()`
814
+
815
+ ```ts
816
+ for await (const event of client.events.health()) {
817
+ switch (event.type) {
818
+ case "client.status_changed":
819
+ console.log("client", event.status);
820
+ break;
821
+ case "market.status_changed":
822
+ console.log("market", event.venue, event.symbol, event.status.activity);
823
+ break;
824
+ case "account.status_changed":
825
+ console.log("account", event.accountId, event.status.runtimeStatus);
826
+ break;
827
+ case "order.status_changed":
828
+ console.log("order", event.accountId, event.status.runtimeStatus);
829
+ break;
830
+ }
831
+ }
832
+ ```
833
+
834
+ 可以用 `HealthEventFilter` 过滤:
835
+
836
+ ```ts
837
+ // 只看 market 范围
838
+ for await (const e of client.events.health({ scope: "market" })) { /* ... */ }
839
+
840
+ // 只看某个 venue
841
+ for await (const e of client.events.health({ venue: "binance" })) { /* ... */ }
842
+
843
+ // 只看某个 account
844
+ for await (const e of client.events.health({ accountId: "main-binance" })) { /* ... */ }
845
+ ```
846
+
847
+ ### 8.3 `events.errors()`
848
+
849
+ ```ts
850
+ for await (const err of client.events.errors()) {
851
+ console.error(`[${err.source}] ${err.error.message}`, {
852
+ venue: err.venue,
853
+ accountId: err.accountId,
854
+ symbol: err.symbol,
855
+ });
856
+ }
857
+ ```
858
+
859
+ `AcexInternalError.source` 枚举:`"client" | "market" | "account" | "order" | "adapter" | "runtime"`。适合桥接到日志或告警系统。
860
+
861
+ ## 9. 数据类型参考
862
+
863
+ 所有 public 类型都从包顶层 import:
864
+
865
+ ```ts
866
+ import type {
867
+ Venue, ClientStatus, CreateClientOptions, VenueCapabilities,
868
+ VenueRuntimeStatus, VenueCapabilitySupport, VenueCapabilityReason,
869
+ FundingRateCapability, PrivateUpdateCapability,
870
+ CancelAllOrdersCapability, PositionSideCapability,
871
+ OrderTimeInForceCapability,
872
+ AccountCredentials,
873
+ MarketDefinition, L1Book, FundingRateSnapshot, MarketDataStatus,
874
+ MarketDataStreamStatus,
875
+ BalanceSnapshot, PositionSnapshot, RiskSnapshot, AccountSnapshot,
876
+ AccountDataStatus, CreateOrderInput, CancelOrderInput, CancelAllOrdersInput,
877
+ OrderSnapshot, OrderDataStatus, OrderSide, OrderStatus, PositionSide,
878
+ MarketEvent, AccountEvent, OrderEvent, HealthEvent,
879
+ AcexInternalError,
880
+ } from "@imbingox/acex";
881
+ import { BigNumber, AcexError } from "@imbingox/acex";
882
+ ```
883
+
884
+ ### 9.1 基础
885
+
886
+ ```ts
887
+ const SUPPORTED_VENUES = ["binance", "okx", "bybit", "gate", "juplend"] as const;
888
+ type Venue = (typeof SUPPORTED_VENUES)[number];
889
+
890
+ type ClientStatus = "idle" | "starting" | "running" | "stopping" | "stopped";
891
+ type VenueRuntimeStatus = "available" | "type_only" | "reserved";
892
+ type VenueCapabilitySupport = "supported" | "unsupported";
893
+ type VenueCapabilityReason =
894
+ | "not_implemented" | "read_only"
895
+ | "market_type_unsupported" | "sdk_reserved";
896
+
897
+ type SubscriptionActivity = "active" | "inactive";
898
+ type MarketFreshness = "fresh" | "stale" | "reconciling";
899
+ type PrivateRuntimeStatus =
900
+ | "bootstrap_pending" | "healthy" | "degraded"
901
+ | "reconnecting" | "reconciling" | "stopped";
902
+ type PrivateRuntimeReason =
903
+ | "credentials_missing" | "auth_failed" | "http_failed" | "rate_limited"
904
+ | "ws_disconnected" | "heartbeat_timeout" | "reconciling";
905
+ type PrivateRuntimeReason =
906
+ | "credentials_missing" | "auth_failed"
907
+ | "ws_disconnected" | "heartbeat_timeout" | "reconciling";
908
+
909
+ type OrderSide = "buy" | "sell";
910
+ type OrderStatus =
911
+ | "open" | "partially_filled" | "filled"
912
+ | "canceled" | "rejected" | "expired";
913
+ type PositionSide = "long" | "short" | "net";
914
+ type CreateOrderType = "limit" | "market";
915
+ type MarketType = "spot" | "swap" | "future";
916
+ ```
917
+
918
+ ### 9.2 Client 配置
919
+
920
+ ```ts
921
+ interface MarketRuntimeOptions {
922
+ l1InitialMessageTimeoutMs?: number; // 默认 15_000
923
+ l1StaleAfterMs?: number; // 默认 15_000
924
+ l1ReconnectDelayMs?: number; // 默认 1_000
925
+ l1ReconnectMaxDelayMs?: number; // 默认 10_000
926
+ }
927
+
928
+ interface AccountRuntimeOptions {
929
+ streamOpenTimeoutMs?: number;
930
+ streamReconnectDelayMs?: number;
931
+ streamReconnectMaxDelayMs?: number;
932
+ listenKeyKeepAliveMs?: number;
933
+ juplend?: {
934
+ pollIntervalMs?: number;
935
+ };
936
+ }
937
+
938
+ interface CreateClientOptions {
939
+ sandbox?: boolean; // 预留,当前不生效
940
+ logger?: Logger; // 预留
941
+ logLevel?: LogLevel; // 预留
942
+ market?: MarketRuntimeOptions;
943
+ account?: AccountRuntimeOptions;
944
+ }
945
+
946
+ interface AccountCredentials {
947
+ apiKey?: string;
948
+ secret?: string;
949
+ password?: string;
950
+ extra?: Record<string, string>;
951
+ }
952
+
953
+ interface BinanceAccountOptions {
954
+ timestamp?: number;
955
+ recvWindow?: number;
956
+ }
957
+
958
+ interface JuplendAccountCredentials {
959
+ apiKey: string;
960
+ }
961
+
962
+ interface JuplendAccountOptions {
963
+ walletAddress: string;
964
+ positionId?: string;
965
+ }
966
+
967
+ type RegisterAccountInput =
968
+ | {
969
+ accountId: string;
970
+ venue: "binance" | "okx" | "bybit" | "gate";
971
+ credentials?: AccountCredentials;
972
+ options?: BinanceAccountOptions;
973
+ }
974
+ | {
975
+ accountId: string;
976
+ venue: "juplend";
977
+ credentials: JuplendAccountCredentials;
978
+ options: JuplendAccountOptions;
979
+ };
980
+
981
+ interface RegisterAccountResult {
982
+ accountId: string;
983
+ venue: Venue;
984
+ }
985
+
986
+ interface StopOptions {
987
+ graceful?: boolean;
988
+ timeoutMs?: number;
989
+ }
990
+ ```
991
+
992
+ ### 9.2.1 Venue Capabilities
993
+
994
+ ```ts
995
+ type FundingRateCapability =
996
+ | VenueCapabilitySupport
997
+ | "market_dependent";
998
+
999
+ type PrivateUpdateCapability =
1000
+ | "websocket"
1001
+ | "polling"
1002
+ | "unsupported";
1003
+
1004
+ type CancelAllOrdersCapability =
1005
+ | "symbol"
1006
+ | "account"
1007
+ | "unsupported";
1008
+
1009
+ type PositionSideCapability =
1010
+ | "optional"
1011
+ | "required_for_hedge"
1012
+ | "unsupported";
1013
+
1014
+ type OrderTimeInForceCapability = "gtc" | "post_only";
1015
+
1016
+ interface VenueCapabilities {
1017
+ venue: Venue;
1018
+ runtimeStatus: VenueRuntimeStatus;
1019
+ readOnly: boolean;
1020
+ notes: string[];
1021
+ market: {
1022
+ catalog: VenueCapabilitySupport;
1023
+ l1Book: VenueCapabilitySupport;
1024
+ fundingRate: FundingRateCapability;
1025
+ marketTypes: MarketType[];
1026
+ };
1027
+ account: {
1028
+ register: VenueCapabilitySupport;
1029
+ snapshot: VenueCapabilitySupport;
1030
+ updates: PrivateUpdateCapability;
1031
+ balances: VenueCapabilitySupport;
1032
+ positions: VenueCapabilitySupport;
1033
+ risk: VenueCapabilitySupport;
1034
+ lending: VenueCapabilitySupport;
1035
+ credentialsRequired: boolean;
1036
+ };
1037
+ order: {
1038
+ supported: boolean;
1039
+ openOrders: VenueCapabilitySupport;
1040
+ updates: PrivateUpdateCapability;
1041
+ create: VenueCapabilitySupport;
1042
+ cancel: VenueCapabilitySupport;
1043
+ cancelAll: CancelAllOrdersCapability;
1044
+ orderTypes: CreateOrderType[];
1045
+ timeInForce: OrderTimeInForceCapability[];
1046
+ postOnly: boolean;
1047
+ reduceOnly: boolean;
1048
+ positionSide: PositionSideCapability;
1049
+ clientOrderId: boolean;
1050
+ reason?: VenueCapabilityReason;
1051
+ };
1052
+ }
1053
+ ```
1054
+
1055
+ ### 9.3 Market
1056
+
1057
+ ```ts
1058
+ interface MarketDefinition {
1059
+ venue: Venue;
1060
+ symbol: string; // 统一 symbol
1061
+ id: string; // 交易所原始 symbol
1062
+ type: MarketType;
1063
+ base: string;
1064
+ quote: string;
1065
+ settle?: string;
1066
+ active: boolean;
1067
+ contract: boolean;
1068
+ linear?: boolean;
1069
+ inverse?: boolean;
1070
+ contractSize?: BigNumber;
1071
+ pricePrecision: number;
1072
+ amountPrecision: number;
1073
+ priceStep: BigNumber;
1074
+ amountStep: BigNumber;
1075
+ minAmount?: BigNumber;
1076
+ minNotional?: BigNumber;
1077
+ expiry?: number;
1078
+ raw: Record<string, unknown>;
1079
+ }
1080
+
1081
+ interface MarketKeyInput {
1082
+ venue: Venue;
1083
+ symbol: string;
1084
+ }
1085
+
1086
+ type DecimalInput = string | number | BigNumber;
1087
+
1088
+ type NormalizeOrderInputRejectReason =
1089
+ | "price_not_positive"
1090
+ | "amount_not_positive"
1091
+ | "amount_below_min"
1092
+ | "notional_below_min";
1093
+
1094
+ interface NormalizeOrderInputInput extends MarketKeyInput {
1095
+ price: DecimalInput;
1096
+ amount: DecimalInput;
1097
+ }
1098
+
1099
+ interface NormalizedOrderInput {
1100
+ price: string;
1101
+ amount: string;
1102
+ rawPrice: string;
1103
+ rawAmount: string;
1104
+ adjusted: boolean;
1105
+ accepted: boolean;
1106
+ rejectReason?: NormalizeOrderInputRejectReason;
1107
+ priceStep: string;
1108
+ amountStep: string;
1109
+ minAmount?: string;
1110
+ minNotional?: string;
1111
+ }
1112
+
1113
+ interface SubscribeL1BookInput extends MarketKeyInput {}
1114
+
1115
+ interface SubscribeFundingRateInput extends MarketKeyInput {}
1116
+
1117
+ interface MarketEventFilter {
1118
+ venue?: Venue;
1119
+ symbol?: string;
1120
+ }
1121
+
1122
+ interface L1Book {
1123
+ venue: Venue;
1124
+ symbol: string;
1125
+ bidPrice: BigNumber;
1126
+ bidSize: BigNumber;
1127
+ askPrice: BigNumber;
1128
+ askSize: BigNumber;
1129
+ exchangeTs?: number;
1130
+ receivedAt: number;
1131
+ updatedAt: number;
1132
+ version: number;
1133
+ status: MarketDataStreamStatus;
1134
+ }
1135
+
1136
+ interface FundingRateSnapshot {
1137
+ venue: Venue;
1138
+ symbol: string;
1139
+ fundingRate: BigNumber;
1140
+ nextFundingTime?: number;
1141
+ markPrice?: BigNumber;
1142
+ indexPrice?: BigNumber;
1143
+ exchangeTs?: number;
1144
+ receivedAt: number;
1145
+ updatedAt: number;
1146
+ version: number;
1147
+ status: MarketDataStreamStatus;
1148
+ }
1149
+
1150
+ interface MarketDataStreamStatus {
1151
+ activity: SubscriptionActivity;
1152
+ ready: boolean;
1153
+ freshness?: MarketFreshness;
1154
+ lastReceivedAt?: number;
1155
+ lastReadyAt?: number;
1156
+ inactiveSince?: number;
1157
+ reason?: "ws_disconnected" | "heartbeat_timeout" | "reconciling";
1158
+ }
1159
+
1160
+ interface MarketDataStatus {
1161
+ venue: Venue;
1162
+ symbol: string;
1163
+ activity: SubscriptionActivity;
1164
+ ready: boolean;
1165
+ freshness?: MarketFreshness;
1166
+ lastReceivedAt?: number;
1167
+ lastReadyAt?: number;
1168
+ inactiveSince?: number;
1169
+ reason?: "ws_disconnected" | "heartbeat_timeout" | "reconciling";
1170
+ }
1171
+ ```
1172
+
1173
+ ### 9.4 Account
1174
+
1175
+ ```ts
1176
+ interface BalanceSnapshot {
1177
+ accountId: string;
1178
+ venue: Venue;
1179
+ asset: string;
1180
+ free: BigNumber;
1181
+ used: BigNumber;
1182
+ total: BigNumber;
1183
+ exchangeTs?: number;
1184
+ receivedAt: number;
1185
+ updatedAt: number;
1186
+ seq: number;
1187
+ lending?: LendingBalanceFacet;
1188
+ }
1189
+
1190
+ interface LendingBalanceFacet {
1191
+ supplied: BigNumber;
1192
+ borrowed: BigNumber;
1193
+ interest: BigNumber;
1194
+ netAsset: BigNumber;
1195
+ supplyAPY?: BigNumber;
1196
+ borrowAPY?: BigNumber;
1197
+ }
1198
+
1199
+ interface PositionSnapshot {
1200
+ accountId: string;
1201
+ venue: Venue;
1202
+ symbol: string;
1203
+ side: PositionSide;
1204
+ size: BigNumber;
1205
+ entryPrice?: BigNumber;
1206
+ markPrice?: BigNumber;
1207
+ unrealizedPnl?: BigNumber;
1208
+ leverage?: BigNumber;
1209
+ liquidationPrice?: BigNumber;
1210
+ exchangeTs?: number;
1211
+ receivedAt: number;
1212
+ updatedAt: number;
1213
+ seq: number;
1214
+ }
1215
+
1216
+ interface RiskSnapshot {
1217
+ accountId: string;
1218
+ venue: Venue;
1219
+ equity?: BigNumber;
1220
+ riskRatio?: BigNumber;
1221
+ initialMargin?: BigNumber;
1222
+ maintenanceMargin?: BigNumber;
1223
+ exchangeTs?: number;
1224
+ receivedAt: number;
1225
+ updatedAt: number;
1226
+ seq: number;
1227
+ lending?: LendingRiskFacet;
1228
+ }
1229
+
1230
+ interface LendingRiskFacet {
1231
+ marginLevel?: BigNumber;
1232
+ healthFactor?: BigNumber;
1233
+ ltv?: BigNumber;
1234
+ liquidationThreshold?: BigNumber;
1235
+ totalCollateralUSD?: BigNumber;
1236
+ totalDebtUSD?: BigNumber;
1237
+ }
1238
+
1239
+ interface AccountSnapshot {
1240
+ accountId: string;
1241
+ venue: Venue;
1242
+ balances: Record<string, BalanceSnapshot>; // 按 asset 索引
1243
+ positions: PositionSnapshot[];
1244
+ risk?: RiskSnapshot;
1245
+ exchangeTs?: number;
1246
+ receivedAt: number;
1247
+ updatedAt: number;
1248
+ }
1249
+
1250
+ interface AccountDataStatus {
1251
+ accountId: string;
1252
+ venue: Venue;
1253
+ activity: SubscriptionActivity;
1254
+ ready: boolean;
1255
+ runtimeStatus?: PrivateRuntimeStatus;
1256
+ lastReceivedAt?: number;
1257
+ lastReadyAt?: number;
1258
+ inactiveSince?: number;
1259
+ reason?: PrivateRuntimeReason;
1260
+ }
1261
+ ```
1262
+
1263
+ ### 9.5 Order
1264
+
1265
+ ```ts
1266
+ // limit / market 两个 variant
1267
+ type CreateOrderInput =
1268
+ | {
1269
+ accountId: string;
1270
+ symbol: string;
1271
+ side: OrderSide;
1272
+ type: "limit";
1273
+ price: string; // decimal string
1274
+ amount: string; // decimal string
1275
+ postOnly?: boolean;
1276
+ clientOrderId?: string;
1277
+ reduceOnly?: boolean;
1278
+ positionSide?: PositionSide;
1279
+ }
1280
+ | {
1281
+ accountId: string;
1282
+ symbol: string;
1283
+ side: OrderSide;
1284
+ type: "market";
1285
+ amount: string; // decimal string
1286
+ clientOrderId?: string;
1287
+ reduceOnly?: boolean;
1288
+ positionSide?: PositionSide;
1289
+ };
1290
+
1291
+ interface CancelOrderInput {
1292
+ accountId: string;
1293
+ symbol: string;
1294
+ orderId?: string;
1295
+ clientOrderId?: string;
1296
+ }
1297
+
1298
+ interface CancelAllOrdersInput {
1299
+ accountId: string;
1300
+ symbol: string;
1301
+ }
1302
+
1303
+ interface GetOrderInput {
1304
+ accountId: string;
1305
+ orderId?: string;
1306
+ clientOrderId?: string;
1307
+ }
1308
+
1309
+ interface OrderSnapshot {
1310
+ accountId: string;
1311
+ venue: Venue;
1312
+ orderId?: string;
1313
+ clientOrderId?: string;
1314
+ symbol: string;
1315
+ side: OrderSide;
1316
+ type: string; // 交易所原始 type 字符串
1317
+ status: OrderStatus;
1318
+ price?: BigNumber;
1319
+ triggerPrice?: BigNumber;
1320
+ amount: BigNumber;
1321
+ filled: BigNumber;
1322
+ remaining?: BigNumber;
1323
+ reduceOnly?: boolean;
1324
+ positionSide?: PositionSide;
1325
+ avgFillPrice?: BigNumber;
1326
+ exchangeTs?: number;
1327
+ receivedAt: number;
1328
+ updatedAt: number;
1329
+ seq: number;
1330
+ }
1331
+
1332
+ interface OrderDataStatus {
1333
+ accountId: string;
1334
+ venue: Venue;
1335
+ activity: SubscriptionActivity;
1336
+ ready: boolean;
1337
+ runtimeStatus?: PrivateRuntimeStatus;
1338
+ lastReceivedAt?: number;
1339
+ lastReadyAt?: number;
1340
+ inactiveSince?: number;
1341
+ reason?: PrivateRuntimeReason;
1342
+ }
1343
+ ```
1344
+
1345
+ ### 9.6 事件
1346
+
1347
+ ```ts
1348
+ // Market
1349
+ type MarketEvent =
1350
+ | { type: "l1_book.updated"; venue: Venue; symbol: string; snapshot: L1Book; ts: number }
1351
+ | { type: "funding_rate.updated"; venue: Venue; symbol: string; snapshot: FundingRateSnapshot; ts: number }
1352
+ | { type: "market.status_changed"; venue: Venue; symbol: string; status: MarketDataStatus; ts: number };
1353
+
1354
+ // Account
1355
+ type AccountEvent =
1356
+ | { type: "balance.updated"; accountId: string; venue: Venue; ts: number; asset: string; snapshot: BalanceSnapshot }
1357
+ | { type: "position.updated"; accountId: string; venue: Venue; ts: number; symbol: string; snapshot: PositionSnapshot }
1358
+ | { type: "risk.updated"; accountId: string; venue: Venue; ts: number; snapshot: RiskSnapshot }
1359
+ | { type: "account.snapshot_replaced"; accountId: string; venue: Venue; ts: number; snapshot: AccountSnapshot };
1360
+
1361
+ // Order
1362
+ type OrderEvent =
1363
+ | { type: "order.updated"; accountId: string; venue: Venue; symbol: string; ts: number; snapshot: OrderSnapshot }
1364
+ | { type: "order.filled"; accountId: string; venue: Venue; symbol: string; ts: number; snapshot: OrderSnapshot }
1365
+ | { type: "order.canceled"; accountId: string; venue: Venue; symbol: string; ts: number; snapshot: OrderSnapshot }
1366
+ | { type: "order.rejected"; accountId: string; venue: Venue; symbol: string; ts: number; snapshot: OrderSnapshot }
1367
+ | { type: "order.snapshot_replaced"; accountId: string; venue: Venue; ts: number; snapshot: OrderSnapshot[] };
1368
+
1369
+ // Health
1370
+ type HealthEvent =
1371
+ | { type: "client.status_changed"; status: ClientStatus; ts: number }
1372
+ | { type: "market.status_changed"; venue: Venue; symbol: string; status: MarketDataStatus; ts: number }
1373
+ | { type: "account.status_changed"; accountId: string; venue: Venue; status: AccountDataStatus; ts: number }
1374
+ | { type: "order.status_changed"; accountId: string; venue: Venue; status: OrderDataStatus; ts: number };
1375
+ ```
1376
+
1377
+ 过滤器:
1378
+
1379
+ ```ts
1380
+ interface MarketEventFilter { venue?: Venue; symbol?: string; }
1381
+ interface AccountEventFilter { accountId?: string; venue?: Venue; symbol?: string; }
1382
+ interface OrderEventFilter { accountId?: string; venue?: Venue; symbol?: string; }
1383
+ interface HealthEventFilter {
1384
+ scope?: "client" | "market" | "account" | "order";
1385
+ venue?: Venue;
1386
+ accountId?: string;
1387
+ symbol?: string;
1388
+ }
1389
+ ```
1390
+
1391
+ ### 9.7 错误
1392
+
1393
+ ```ts
1394
+ interface AcexInternalError {
1395
+ source: "client" | "market" | "account" | "order" | "adapter" | "runtime";
1396
+ venue?: Venue;
1397
+ accountId?: string;
1398
+ symbol?: string;
1399
+ error: Error;
1400
+ ts: number;
1401
+ }
1402
+
1403
+ class AcexError extends Error {
1404
+ readonly code: AcexErrorCode;
1405
+ }
1406
+ ```
1407
+
1408
+ ## 10. 错误处理
1409
+
1410
+ 可预期错误统一通过 `AcexError` 抛出,`code` 字段可用于分支判断:
1411
+
1412
+ ```ts
1413
+ import { AcexError } from "@imbingox/acex";
1414
+
1415
+ try {
1416
+ await client.market.subscribeL1Book({ venue: "binance", symbol: "X/Y:Z" });
1417
+ } catch (err) {
1418
+ if (err instanceof AcexError) {
1419
+ console.log(err.code, err.message);
1420
+ }
1421
+ }
1422
+ ```
1423
+
1424
+ 完整错误码列表:
1425
+
1426
+ | Code | 典型场景 |
1427
+ |---|---|
1428
+ | `CLIENT_NOT_STARTED` | 未 `start()` 就调用 `subscribe*()` |
1429
+ | `VENUE_NOT_SUPPORTED` | venue 当前未实现,或 Juplend 这类只读 venue 被用于下单 |
1430
+ | `MARKET_CATALOG_LOAD_FAILED` | `loadMarkets()` 拉取失败 |
1431
+ | `MARKET_NOT_FOUND` | 指定 symbol 不存在 |
1432
+ | `MARKET_INACTIVE` | 指定 symbol 在 catalog 中但不可交易 |
1433
+ | `MARKET_FUNDING_RATE_UNSUPPORTED` | 指定 market 不支持资金费率订阅 |
1434
+ | `MARKET_STREAM_TIMEOUT` | market stream 首条消息超时 |
1435
+ | `ACCOUNT_ALREADY_EXISTS` | 重复注册同一个 `accountId` |
1436
+ | `ACCOUNT_NOT_FOUND` | `accountId` 未注册或已被移除 |
1437
+ | `ACCOUNT_BOOTSTRAP_FAILED` | `subscribeAccount()` 过程中账户快照拉取失败,例如 Juplend HTTP/API 失败或缺 `options.walletAddress` |
1438
+ | `CREDENTIALS_MISSING` | 私有订阅 / 下单缺必要凭证,例如 Binance 缺 `apiKey/secret` 或 Juplend 缺 `apiKey` |
1439
+ | `ORDER_BOOTSTRAP_FAILED` | `subscribeOrders()` 过程中 open orders 拉取失败 |
1440
+ | `ORDER_INPUT_INVALID` | 下单/撤单本地输入校验失败(如缺 price、缺 id) |
1441
+ | `ORDER_CREATE_FAILED` | 交易所拒单 / REST 报错 |
1442
+ | `ORDER_CANCEL_FAILED` | 撤单失败 |
1443
+ | `ORDER_CANCEL_ALL_FAILED` | 批量撤单失败 |
1444
+
1445
+ ## 11. 当前限制
1446
+
1447
+ - **Venue**:market/order 运行时只支持 `binance`;account 运行时支持 `binance` 与只读 `juplend`。`okx` / `bybit` / `gate` 仅在 `SUPPORTED_VENUES` 类型里声明,未接入
1448
+ - **市场数据**:真实落地 Binance L1 Book(Spot + USDⓈ-M + COIN-M)和 Binance 永续 Funding Rate
1449
+ - **私有链路**:Binance PAPI UM 使用 listenKey/WebSocket;Juplend 使用 HTTP polling,只提供账户只读视图
1450
+ - **Juplend 写操作**:不支持 supply / borrow / repay / withdraw,不支持 `OrderManager` 下单撤单
1451
+ - **Juplend 数量精度**:Portfolio API 提供 USD 聚合值,token 数量由 USD / vault oracle price 反算
1452
+ - **Funding Rate**:仅永续合约支持,来自 mark price websocket;不支持现货和交割合约
1453
+ - **下单类型**:`createOrder()` 仅支持 `limit` / `market`;条件单、改单不支持
1454
+ - **撤单范围**:`cancelAllOrders()` 必须传 `symbol`,不支持账户级全撤
1455
+ - **双向持仓**:hedge mode 下 `createOrder()` 必须显式传 `positionSide`
1456
+ - **精度纠偏**:SDK 不自动按 `priceStep` / `amountStep` / `minNotional` 调整下单输入
1457
+ - **Client options**:`sandbox` / `logger` / `logLevel` 是预留位,当前不生效