@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.11

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