@imbingox/acex 0.4.0-beta.12 → 0.4.0-beta.14
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/CHANGELOG.md +176 -0
- package/docs/api.md +4 -2
- package/package.json +7 -2
- package/src/adapters/binance/private-adapter.ts +210 -37
- package/src/adapters/types.ts +2 -0
- package/src/client/private-subscription-coordinator.ts +31 -0
- package/src/internal/watermark.ts +11 -0
- package/src/managers/order/data-status.ts +61 -0
- package/src/managers/order/identity.ts +77 -0
- package/src/managers/order/model.ts +36 -0
- package/src/managers/order/snapshot.ts +87 -0
- package/src/managers/order/store.ts +486 -0
- package/src/managers/order-manager.ts +168 -720
- package/src/types/shared.ts +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @imbingox/acex
|
|
2
|
+
|
|
3
|
+
## 0.4.0-beta.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a57b1a0: Include `README.md` and `CHANGELOG.md` in the published npm package so downstream consumers can inspect package usage and release notes from the installed tarball.
|
|
8
|
+
|
|
9
|
+
## 0.4.0-beta.13
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3581ced: Binance private user streams now recover from `listenKeyExpired`, listenKey keepalive failure, and private stream message watchdog timeout by rotating the listenKey and rebuilding the WebSocket, then triggering the existing account/order reconcile path. Added optional `account.binance.privateStreamStaleAfterMs` tuning and a live order smoke entry for listenKey invalidation recovery.
|
|
14
|
+
|
|
15
|
+
## 0.4.0-beta.12
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- e98dba3: Fix Binance `cancelAllOrders` parsing of the PAPI `{code,msg}` response as an order array, which previously always threw against the live API after the venue had already canceled the orders. The adapter now pre-fetches symbol open orders and returns them as canceled snapshots after the cancel-all response succeeds.
|
|
20
|
+
|
|
21
|
+
## 0.4.0-beta.11
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- acbdfd8: OrderManager 内部订单主键改为 SDK 生成的 `localOrderId`,并维护 venue `orderId` / `clientOrderId` 反向索引与下单 pending claim,避免 REST 返回前早到的 WS 更新双建订单。公开 API 与类型不变。
|
|
26
|
+
|
|
27
|
+
行为变化:调用 `createOrder()` 未传 `clientOrderId` 时,SDK 现在会生成合规的 `acex-*` client id 并作为 Binance `newClientOrderId` 发送,返回的 `snapshot.clientOrderId` 也会是该生成值,而不再依赖 Binance 自动生成。
|
|
28
|
+
|
|
29
|
+
## 0.4.0-beta.10
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 89f846e: OrderManager 内部订单存储改为 open / closed 分层(按 symbol 嵌套)+ 复合身份索引,终态订单不再无界累积:closed 订单按 symbol 保留最近 N 个(新增可选 `CreateClientOptions.order.maxClosedOrdersPerSymbol`,默认 500,超限按 FIFO 批量裁剪),`getOpenOrders()` 查询不再随历史订单数量增长而变慢。`getOrder()` 对外行为保持不变(仍可只按 `orderId` 或 `clientOrderId` 查询、可省略 `symbol`),`clientOrderId` 多命中时返回最新一笔。
|
|
34
|
+
|
|
35
|
+
## 0.4.0-beta.9
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 153e2d8: Binance public market catalog now treats `TRADIFI_PERPETUAL` USDⓈ-M symbols as perpetual swaps, so TradFi Perps such as `AAPLUSDT` normalize to `AAPL/USDT:USDT` and support the existing L1 book and funding-rate public WebSocket subscriptions.
|
|
40
|
+
|
|
41
|
+
## 0.4.0-beta.8
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- d874b29: 公开 `AcexError.details` 与 `AcexError.cause`,让调用方在捕获订单、市场目录、server time、market stream 首包超时、account/order bootstrap 等失败时,既能继续使用稳定的 `error.code` 分支,也能读取交易所结构化拒绝原因(`details.venueError.code/message`)和已脱敏的 transport 诊断信息(`details.transport`)。
|
|
46
|
+
|
|
47
|
+
## 0.4.0-beta.7
|
|
48
|
+
|
|
49
|
+
### Minor Changes
|
|
50
|
+
|
|
51
|
+
- dac87aa: Add `client.market.fetchServerTime(venue)` with Binance USDM server-time support, RTT measurement, estimated clock offset, venue capability reporting, and a structured failure code.
|
|
52
|
+
|
|
53
|
+
## 0.4.0-beta.6
|
|
54
|
+
|
|
55
|
+
### Minor Changes
|
|
56
|
+
|
|
57
|
+
- f65bab7: 新增 `client.market.reloadMarkets(venue?)` 主动刷新市场目录能力,并公开 `MarketCatalogReloadSummary` 返回每个 venue 的新增、移除、总数和失败摘要。刷新失败会保留旧目录并在对应 summary 中返回错误,方便长运行进程在交易所新增 symbol 后无需重启即可加载新目录。
|
|
58
|
+
|
|
59
|
+
## 0.4.0-beta.5
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- e61f10f: private 编排层改为按 adapter capability 分派,移除残留的 venue 字面量:下单命令是否支持按 `orderCapabilities.supported`、订单订阅按 `orderCapabilities.updates`、private credentials 是否必需按 `accountCapabilities.credentialsRequired`、account stream 启动顺序按 `accountCapabilities.updates`(polling 先 bootstrap、websocket 先建流)、REST account refresh polling 按 adapter 是否实现可选的 `refreshAccount()` 判别。juplend 轮询间隔从内部 `PrivateStreamOptions` 收口进 adapter 构造。公开 API、公共类型与运行时行为均不变,为后续接入新交易所做准备。
|
|
64
|
+
|
|
65
|
+
## 0.4.0-beta.4
|
|
66
|
+
|
|
67
|
+
### Minor Changes
|
|
68
|
+
|
|
69
|
+
- 0d99377: Add a public `RateLimiter` seam via `CreateClientOptions.rateLimiter`. The default reactive limiter tracks venue-provided REST usage metadata and honors `Retry-After` after 429/418 responses without proactively throttling normal requests or replaying non-idempotent order commands.
|
|
70
|
+
|
|
71
|
+
## 0.4.0-beta.3
|
|
72
|
+
|
|
73
|
+
### Minor Changes
|
|
74
|
+
|
|
75
|
+
- c3c9460: Add an injectable request signing clock via `CreateClientOptions.clock` and the public `TimeProvider` type. The default remains the local system clock; this does not add server-time calibration.
|
|
76
|
+
|
|
77
|
+
## 0.4.0-beta.2
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- d9bacb6: 对外错误信息不再泄漏签名与密钥。请求失败时,错误的 `message` 与 URL 会对 `signature`、API key、`listenKey`、`token`、`passphrase` 等敏感 query 参数及对应的 JSON body 字段做脱敏(替换为 `[REDACTED]`),私有订阅 bootstrap 失败路径同样会对透传的错误信息脱敏。此前这些敏感值可能随错误信息进入日志。属向后兼容的行为修复,不改变公共类型与 API 形状。
|
|
82
|
+
|
|
83
|
+
## 0.4.0-beta.1
|
|
84
|
+
|
|
85
|
+
### Minor Changes
|
|
86
|
+
|
|
87
|
+
- adc9274: 公共 snapshot / market 数值字段(包括 `L1Book`、`FundingRateSnapshot`、`OrderSnapshot`、`BalanceSnapshot`、`PositionSnapshot`、`RiskSnapshot`、`MarketDefinition` 及 lending facets)由 `BigNumber` 改为 canonical 十进制 string。
|
|
88
|
+
|
|
89
|
+
这是破坏性 public contract 变更:`snapshot.bidPrice.minus(...)`、`.multipliedBy(...)` 等链式调用不再可用,消费者需要改为 `new BigNumber(field)` 自行解析后运算(SDK 仍保留 `export { BigNumber }`)。不要用 `parseFloat()` 解析这些字段,否则会退回 JS 浮点精度。输入侧 `DecimalInput` 不变,仍接受 string / number / `BigNumber`。
|
|
90
|
+
|
|
91
|
+
## 0.3.1-beta.0
|
|
92
|
+
|
|
93
|
+
### Patch Changes
|
|
94
|
+
|
|
95
|
+
- 19f60bc: Binance 行情订阅现在复用 WebSocket 连接:同一 connectionKey / base URL 下多个 symbol 复用物理连接(例如 USDM L1 与 funding 因 base URL 不同会分开),通过 JSON `SUBSCRIBE`/`UNSUBSCRIBE` 动态增删订阅,断线重连后自动重放,单连接订阅数达上限会自动开新连接。行情层改为按 venue 分派 adapter,为接入更多交易所打基础。公开 API 不变。
|
|
96
|
+
|
|
97
|
+
## 0.3.0
|
|
98
|
+
|
|
99
|
+
### Minor Changes
|
|
100
|
+
|
|
101
|
+
- 14d25cb: 重命名账户风险权益字段并拆分净值与风控口径。`RiskSnapshot.equity` 替换为 `netEquity` / `riskEquity`,`actualLeverage` 替换为 `riskLeverage`;Binance 使用 `actualEquity` / `accountEquity` 分别映射净权益和风控折算权益,Juplend 使用清算阈值折算权益填充 `riskEquity`。
|
|
102
|
+
- 50e4e09: 通过周期性 REST polling 刷新 Binance 账户风险和 mark-to-market 仓位字段。`RiskSnapshot` 现在暴露风控口径的 `riskLeverage`,Binance 账户运行时配置新增 `account.binance.riskPollIntervalMs`。
|
|
103
|
+
- 680e315: Add strict-symbol market data aggregation APIs for markets, L1 books, and funding rates. Also update Binance USDⓈ-M funding mark price streams to use the current market websocket endpoint and default 3s `markPrice` stream.
|
|
104
|
+
- 68356a0: Replace Juplend's portfolio-backed lending account implementation with native `@jup-ag/lend-read` reads. Juplend accounts no longer require credentials, can be loaded by `walletAddress` or direct `vaultId + positionId`, support optional RPC and Jup API enrichment via `SOL_HELIUS_RPC` / `account.juplend.rpcUrl` and `JUP_API` / `account.juplend.jupApiKey`, and now report more accurate lending balances, debt, collateral, and risk data from native vault sources.
|
|
105
|
+
- c411b69: Add venue-based account registration and Juplend read-only lending account support. `Exchange` is renamed to `Venue`, account risk now uses unified `riskRatio`, and `RegisterAccountInput` is venue-specific so Juplend requires `credentials.apiKey` plus `options.walletAddress` with optional `positionId` filtering. Juplend account polling exposes lending balance/risk facets, replaces full snapshots to clear closed positions, and includes live smoke coverage.
|
|
106
|
+
- 9dad2f0: Add post-only limit order support and market order input normalization. Binance PAPI UM limit orders now map `postOnly: true` to `timeInForce=GTX`, and callers can normalize price and amount strings with `market.normalizeOrderInput()` before placing orders.
|
|
107
|
+
- ea9a4a7: Add top-level venue capability queries for SDK runtime support by venue.
|
|
108
|
+
|
|
109
|
+
### Patch Changes
|
|
110
|
+
|
|
111
|
+
- 46d1291: Include `docs/api.md` in the published npm package.
|
|
112
|
+
|
|
113
|
+
## 0.3.0-beta.6
|
|
114
|
+
|
|
115
|
+
### Minor Changes
|
|
116
|
+
|
|
117
|
+
- 68356a0: Replace Juplend's portfolio-backed lending account implementation with native `@jup-ag/lend-read` reads. Juplend accounts no longer require credentials, can be loaded by `walletAddress` or direct `vaultId + positionId`, support optional RPC and Jup API enrichment via `SOL_HELIUS_RPC` / `account.juplend.rpcUrl` and `JUP_API` / `account.juplend.jupApiKey`, and now report more accurate lending balances, debt, collateral, and risk data from native vault sources.
|
|
118
|
+
|
|
119
|
+
## 0.3.0-beta.5
|
|
120
|
+
|
|
121
|
+
### Minor Changes
|
|
122
|
+
|
|
123
|
+
- 14d25cb: 重命名账户风险权益字段并拆分净值与风控口径。`RiskSnapshot.equity` 替换为 `netEquity` / `riskEquity`,`actualLeverage` 替换为 `riskLeverage`;Binance 使用 `actualEquity` / `accountEquity` 分别映射净权益和风控折算权益,Juplend 使用清算阈值折算权益填充 `riskEquity`。
|
|
124
|
+
|
|
125
|
+
## 0.3.0-beta.4
|
|
126
|
+
|
|
127
|
+
### Minor Changes
|
|
128
|
+
|
|
129
|
+
- 50e4e09: 通过周期性 REST polling 刷新 Binance 账户风险和 mark-to-market 仓位字段。`RiskSnapshot` 现在暴露 `actualLeverage`,Binance 账户运行时配置新增 `account.binance.riskPollIntervalMs`。
|
|
130
|
+
|
|
131
|
+
## 0.3.0-beta.3
|
|
132
|
+
|
|
133
|
+
### Minor Changes
|
|
134
|
+
|
|
135
|
+
- ea9a4a7: Add top-level venue capability queries for SDK runtime support by venue.
|
|
136
|
+
|
|
137
|
+
### Patch Changes
|
|
138
|
+
|
|
139
|
+
- 46d1291: Include `docs/api.md` in the published npm package.
|
|
140
|
+
|
|
141
|
+
## 0.3.0-beta.2
|
|
142
|
+
|
|
143
|
+
### Minor Changes
|
|
144
|
+
|
|
145
|
+
- c411b69: Add venue-based account registration and Juplend read-only lending account support. `Exchange` is renamed to `Venue`, account risk now uses unified `riskRatio`, and `RegisterAccountInput` is venue-specific so Juplend requires `credentials.apiKey` plus `options.walletAddress` with optional `positionId` filtering. Juplend account polling exposes lending balance/risk facets, replaces full snapshots to clear closed positions, and includes live smoke coverage.
|
|
146
|
+
|
|
147
|
+
## 0.3.0-beta.1
|
|
148
|
+
|
|
149
|
+
### Minor Changes
|
|
150
|
+
|
|
151
|
+
- 9dad2f0: Add post-only limit order support and market order input normalization. Binance PAPI UM limit orders now map `postOnly: true` to `timeInForce=GTX`, and callers can normalize price and amount strings with `market.normalizeOrderInput()` before placing orders.
|
|
152
|
+
|
|
153
|
+
## 0.3.0-beta.0
|
|
154
|
+
|
|
155
|
+
### Minor Changes
|
|
156
|
+
|
|
157
|
+
- 680e315: Add strict-symbol market data aggregation APIs for markets, L1 books, and funding rates. Also update Binance USDⓈ-M funding mark price streams to use the current market websocket endpoint and default 3s `markPrice` stream.
|
|
158
|
+
|
|
159
|
+
## 0.2.0
|
|
160
|
+
|
|
161
|
+
### Minor Changes
|
|
162
|
+
|
|
163
|
+
- 5dcc3c1: Add Binance funding rate market data stream with per-stream market data status.
|
|
164
|
+
- baeab15: Add Binance PAPI private account and order support, including the first `createOrder`, `cancelOrder`, and `cancelAllOrders` APIs.
|
|
165
|
+
|
|
166
|
+
## 0.1.0-beta.4
|
|
167
|
+
|
|
168
|
+
### Minor Changes
|
|
169
|
+
|
|
170
|
+
- 5dcc3c1: Add Binance funding rate market data stream with per-stream market data status.
|
|
171
|
+
|
|
172
|
+
## 0.1.0-beta.3
|
|
173
|
+
|
|
174
|
+
### Minor Changes
|
|
175
|
+
|
|
176
|
+
- baeab15: Add Binance PAPI private account and order support, including the first `createOrder`, `cancelOrder`, and `cancelAllOrders` APIs.
|
package/docs/api.md
CHANGED
|
@@ -100,7 +100,7 @@ const risk = client.account.getRiskSnapshot("main-binance");
|
|
|
100
100
|
const openOrders = client.order.getOpenOrders("main-binance");
|
|
101
101
|
```
|
|
102
102
|
|
|
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
|
|
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 是保守值。读取余额、仓位或风险数据时必须订阅 `client.account.subscribeAccount()`;`client.order.subscribeOrders()` 只维护订单缓存,即使底层复用同一条 private WS,也不会维护 account 仓位缓存。
|
|
104
104
|
|
|
105
105
|
### 2.4 注册 Juplend 只读账户
|
|
106
106
|
|
|
@@ -242,6 +242,7 @@ const client = createClient({
|
|
|
242
242
|
binance: {
|
|
243
243
|
riskPollIntervalMs: 5_000,
|
|
244
244
|
privateReconcileIntervalMs: 60_000,
|
|
245
|
+
privateStreamStaleAfterMs: 65 * 60_000,
|
|
245
246
|
},
|
|
246
247
|
juplend: {
|
|
247
248
|
pollIntervalMs: 30_000,
|
|
@@ -392,7 +393,7 @@ interface AccountManager {
|
|
|
392
393
|
|
|
393
394
|
`AccountSnapshot.balances` 是 `Record<string, BalanceSnapshot>`,数组视图用 `getBalances()`。
|
|
394
395
|
|
|
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
|
+
Binance account update 是 REST bootstrap + WS 增量 + REST risk refresh + private reconcile 的组合。WS `ACCOUNT_UPDATE` 会更新发生变化的余额和仓位;`/papi/v1/account` + `/papi/v1/um/positionRisk` refresh 用于校准风险字段和 mark-to-market 仓位字段。risk refresh 是增量语义,不会因 REST 缺失项删除本地 position;private reconcile 是全量校准语义,会清理 REST 全量余额/仓位中缺失或归零的本地记录。Juplend 每次 poll 都是全量快照,成功 poll 会替换 balances / positions / risk,用于清理已关闭或不再匹配的 position。
|
|
396
397
|
|
|
397
398
|
Account 事件用于消费余额、仓位、风险或全量快照替换:
|
|
398
399
|
|
|
@@ -586,6 +587,7 @@ interface CreateClientOptions {
|
|
|
586
587
|
binance?: {
|
|
587
588
|
riskPollIntervalMs?: number;
|
|
588
589
|
privateReconcileIntervalMs?: number;
|
|
590
|
+
privateStreamStaleAfterMs?: number;
|
|
589
591
|
};
|
|
590
592
|
juplend?: {
|
|
591
593
|
pollIntervalMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.14",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"index.ts",
|
|
19
19
|
"src/",
|
|
20
|
-
"docs/api.md"
|
|
20
|
+
"docs/api.md",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
23
25
|
"changeset": "changeset",
|
|
@@ -25,7 +27,9 @@
|
|
|
25
27
|
"changeset:pre:exit": "changeset pre exit && if [ -f .changeset/pre.json ]; then biome check --write .changeset/pre.json; fi",
|
|
26
28
|
"lint": "biome check .",
|
|
27
29
|
"lint:fix": "biome check --write .",
|
|
30
|
+
"pack:check": "bun run scripts/check-npm-pack.ts",
|
|
28
31
|
"release": "changeset publish",
|
|
32
|
+
"release:notes": "bun run scripts/extract-changelog-section.ts",
|
|
29
33
|
"type-check": "tsc --noEmit",
|
|
30
34
|
"test": "bun test --max-concurrency=1 tests/unit tests/integration",
|
|
31
35
|
"test:live:account": "bun run scripts/live-account-smoke.ts",
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
"test:live:order": "bun run scripts/live-order-smoke.ts",
|
|
40
44
|
"test:live:order:smoke": "bun run scripts/live-order-smoke.ts --duration 10",
|
|
41
45
|
"test:live:order:soak": "bun run scripts/live-order-smoke.ts --duration 60 --disconnect-after 5",
|
|
46
|
+
"test:live:order:listen-key": "bun run scripts/live-order-smoke.ts --duration 60 --expire-listen-key-after 5",
|
|
42
47
|
"version-packages": "changeset version && files=\"package.json\"; if [ -f .changeset/pre.json ]; then files=\"$files .changeset/pre.json\"; fi; if [ -f CHANGELOG.md ]; then files=\"$files CHANGELOG.md\"; fi; biome check --write $files",
|
|
43
48
|
"test:unit": "bun test tests/unit",
|
|
44
49
|
"test:integration": "bun test --max-concurrency=1 tests/integration",
|
|
@@ -155,9 +155,16 @@ interface BinanceOrderTradeUpdateMessage {
|
|
|
155
155
|
o?: BinanceOrderTradeUpdatePayload;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
interface BinanceListenKeyExpiredMessage {
|
|
159
|
+
e?: string;
|
|
160
|
+
E?: number;
|
|
161
|
+
listenKey?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
158
164
|
type BinancePrivateMessage =
|
|
159
165
|
| BinanceAccountUpdateMessage
|
|
160
|
-
| BinanceOrderTradeUpdateMessage
|
|
166
|
+
| BinanceOrderTradeUpdateMessage
|
|
167
|
+
| BinanceListenKeyExpiredMessage;
|
|
161
168
|
|
|
162
169
|
const BINANCE_PAPI_REST_BASE_URL = "https://papi.binance.com";
|
|
163
170
|
const BINANCE_PAPI_WS_BASE_URL = "wss://fstream.binance.com/pm/ws";
|
|
@@ -225,6 +232,10 @@ function getStringOption(
|
|
|
225
232
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
function toError(value: unknown, fallback: string): Error {
|
|
236
|
+
return value instanceof Error ? value : new Error(fallback);
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
function signQuery(query: string, secret: string): string {
|
|
229
240
|
return createHmac("sha256", secret).update(query).digest("hex");
|
|
230
241
|
}
|
|
@@ -534,7 +545,9 @@ function mapAccountUpdatePosition(
|
|
|
534
545
|
|
|
535
546
|
function parsePrivateMessage(data: string): BinancePrivateMessage | undefined {
|
|
536
547
|
const parsed = JSON.parse(data) as BinancePrivateMessage;
|
|
537
|
-
return parsed.e === "ACCOUNT_UPDATE" ||
|
|
548
|
+
return parsed.e === "ACCOUNT_UPDATE" ||
|
|
549
|
+
parsed.e === "ORDER_TRADE_UPDATE" ||
|
|
550
|
+
parsed.e === "listenKeyExpired"
|
|
538
551
|
? parsed
|
|
539
552
|
: undefined;
|
|
540
553
|
}
|
|
@@ -545,6 +558,12 @@ function isAccountUpdateMessage(
|
|
|
545
558
|
return message.e === "ACCOUNT_UPDATE";
|
|
546
559
|
}
|
|
547
560
|
|
|
561
|
+
function isListenKeyExpiredMessage(
|
|
562
|
+
message: BinancePrivateMessage,
|
|
563
|
+
): message is BinanceListenKeyExpiredMessage {
|
|
564
|
+
return message.e === "listenKeyExpired";
|
|
565
|
+
}
|
|
566
|
+
|
|
548
567
|
function mapAccountUpdate(
|
|
549
568
|
message: BinanceAccountUpdateMessage,
|
|
550
569
|
receivedAt: number,
|
|
@@ -920,75 +939,158 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
920
939
|
options: PrivateStreamOptions,
|
|
921
940
|
accountOptions?: Record<string, unknown>,
|
|
922
941
|
): StreamHandle {
|
|
942
|
+
interface PrivateStreamSession {
|
|
943
|
+
readonly listenKey: string;
|
|
944
|
+
websocket?: StreamHandle;
|
|
945
|
+
keepAliveTimer?: TimerHandle;
|
|
946
|
+
stopped: boolean;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
type RecoveryReason =
|
|
950
|
+
| "heartbeat_timeout"
|
|
951
|
+
| "keepalive_failed"
|
|
952
|
+
| "listen_key_expired";
|
|
953
|
+
|
|
923
954
|
let closed = false;
|
|
924
|
-
let
|
|
925
|
-
let
|
|
926
|
-
let
|
|
955
|
+
let activeSession: PrivateStreamSession | undefined;
|
|
956
|
+
let recoveryInFlight: Promise<void> | undefined;
|
|
957
|
+
let recoveryRetryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
927
958
|
let openedOnce = false;
|
|
928
959
|
|
|
929
|
-
const
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
960
|
+
const clearRecoveryRetry = () => {
|
|
961
|
+
if (recoveryRetryTimer) {
|
|
962
|
+
clearTimeout(recoveryRetryTimer);
|
|
963
|
+
recoveryRetryTimer = undefined;
|
|
933
964
|
}
|
|
934
965
|
};
|
|
935
966
|
|
|
936
|
-
const closeListenKey = () => {
|
|
937
|
-
|
|
967
|
+
const closeListenKey = (listenKey: string) => {
|
|
968
|
+
void this.closeUserDataStream(
|
|
969
|
+
credentials,
|
|
970
|
+
listenKey,
|
|
971
|
+
accountOptions,
|
|
972
|
+
).catch((error) => {
|
|
973
|
+
if (!closed) {
|
|
974
|
+
callbacks.onError(
|
|
975
|
+
toError(error, "Failed to close Binance PAPI listenKey"),
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const closeSession = (
|
|
982
|
+
session: PrivateStreamSession | undefined,
|
|
983
|
+
shouldCloseListenKey: boolean,
|
|
984
|
+
) => {
|
|
985
|
+
if (!session || session.stopped) {
|
|
938
986
|
return;
|
|
939
987
|
}
|
|
940
988
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
);
|
|
989
|
+
session.stopped = true;
|
|
990
|
+
if (session.keepAliveTimer) {
|
|
991
|
+
clearInterval(session.keepAliveTimer);
|
|
992
|
+
session.keepAliveTimer = undefined;
|
|
993
|
+
}
|
|
994
|
+
session.websocket?.close();
|
|
995
|
+
session.websocket = undefined;
|
|
996
|
+
if (shouldCloseListenKey) {
|
|
997
|
+
closeListenKey(session.listenKey);
|
|
998
|
+
}
|
|
952
999
|
};
|
|
953
1000
|
|
|
954
|
-
const
|
|
955
|
-
listenKey = await this.startUserDataStream(credentials, accountOptions);
|
|
1001
|
+
const activateSession = (nextSession: PrivateStreamSession) => {
|
|
956
1002
|
if (closed) {
|
|
957
|
-
|
|
1003
|
+
closeSession(nextSession, true);
|
|
958
1004
|
return;
|
|
959
1005
|
}
|
|
960
1006
|
|
|
961
|
-
|
|
962
|
-
|
|
1007
|
+
const previousSession = activeSession;
|
|
1008
|
+
activeSession = nextSession;
|
|
1009
|
+
closeSession(previousSession, true);
|
|
1010
|
+
|
|
1011
|
+
if (openedOnce) {
|
|
1012
|
+
callbacks.onReconnected();
|
|
1013
|
+
} else {
|
|
1014
|
+
openedOnce = true;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const scheduleRecoveryRetry = (reason: RecoveryReason) => {
|
|
1019
|
+
if (closed || recoveryRetryTimer) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
recoveryRetryTimer = setTimeout(() => {
|
|
1024
|
+
recoveryRetryTimer = undefined;
|
|
1025
|
+
recoverPrivateStream(reason);
|
|
1026
|
+
}, options.reconnectDelayMs);
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const createSession = async (): Promise<
|
|
1030
|
+
PrivateStreamSession | undefined
|
|
1031
|
+
> => {
|
|
1032
|
+
const listenKey = await this.startUserDataStream(
|
|
1033
|
+
credentials,
|
|
1034
|
+
accountOptions,
|
|
1035
|
+
);
|
|
1036
|
+
if (closed) {
|
|
1037
|
+
closeListenKey(listenKey);
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const nextSession: PrivateStreamSession = {
|
|
1042
|
+
listenKey,
|
|
1043
|
+
stopped: false,
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
nextSession.keepAliveTimer = setInterval(() => {
|
|
1047
|
+
if (closed || activeSession !== nextSession) {
|
|
963
1048
|
return;
|
|
964
1049
|
}
|
|
965
1050
|
|
|
966
1051
|
void this.keepAliveUserDataStream(
|
|
967
1052
|
credentials,
|
|
968
|
-
listenKey,
|
|
1053
|
+
nextSession.listenKey,
|
|
969
1054
|
accountOptions,
|
|
970
1055
|
).catch((error) => {
|
|
1056
|
+
if (closed || activeSession !== nextSession) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
971
1060
|
callbacks.onError(
|
|
972
|
-
error
|
|
973
|
-
? error
|
|
974
|
-
: new Error("Failed to keep Binance PAPI listenKey alive"),
|
|
1061
|
+
toError(error, "Failed to keep Binance PAPI listenKey alive"),
|
|
975
1062
|
);
|
|
1063
|
+
recoverPrivateStream("keepalive_failed");
|
|
976
1064
|
});
|
|
977
1065
|
}, options.listenKeyKeepAliveMs);
|
|
978
1066
|
|
|
979
|
-
websocket = createManagedWebSocket<BinancePrivateMessage>({
|
|
1067
|
+
nextSession.websocket = createManagedWebSocket<BinancePrivateMessage>({
|
|
980
1068
|
url: `${BINANCE_PAPI_WS_BASE_URL}/${listenKey}`,
|
|
981
1069
|
initialMessageTimeoutMs: options.openTimeoutMs,
|
|
982
1070
|
readyWhen: "open",
|
|
983
1071
|
now: options.now,
|
|
984
1072
|
parseMessage: parsePrivateMessage,
|
|
985
1073
|
onOpen() {
|
|
1074
|
+
if (closed || activeSession !== nextSession) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
986
1078
|
if (openedOnce) {
|
|
987
1079
|
callbacks.onReconnected();
|
|
1080
|
+
} else {
|
|
1081
|
+
openedOnce = true;
|
|
988
1082
|
}
|
|
989
|
-
openedOnce = true;
|
|
990
1083
|
},
|
|
991
1084
|
onMessage(message, receivedAt) {
|
|
1085
|
+
if (closed || activeSession !== nextSession) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (isListenKeyExpiredMessage(message)) {
|
|
1090
|
+
recoverPrivateStream("listen_key_expired");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
992
1094
|
if (isAccountUpdateMessage(message)) {
|
|
993
1095
|
callbacks.onAccountUpdate(mapAccountUpdate(message, receivedAt));
|
|
994
1096
|
return;
|
|
@@ -1000,13 +1102,31 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
1000
1102
|
}
|
|
1001
1103
|
},
|
|
1002
1104
|
onUnexpectedClose() {
|
|
1105
|
+
if (closed || activeSession !== nextSession) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1003
1109
|
callbacks.onDisconnected();
|
|
1004
1110
|
},
|
|
1005
1111
|
onError() {
|
|
1112
|
+
if (closed || activeSession !== nextSession) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1006
1116
|
callbacks.onError(
|
|
1007
1117
|
new Error("WebSocket error for Binance PAPI private stream"),
|
|
1008
1118
|
);
|
|
1009
1119
|
},
|
|
1120
|
+
messageWatchdog: {
|
|
1121
|
+
staleAfterMs: options.staleAfterMs,
|
|
1122
|
+
onStale() {
|
|
1123
|
+
if (closed || activeSession !== nextSession) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
recoverPrivateStream("heartbeat_timeout");
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1010
1130
|
reconnect: {
|
|
1011
1131
|
initialDelayMs: options.reconnectDelayMs,
|
|
1012
1132
|
maxDelayMs: options.reconnectMaxDelayMs,
|
|
@@ -1014,7 +1134,60 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
1014
1134
|
},
|
|
1015
1135
|
});
|
|
1016
1136
|
|
|
1017
|
-
|
|
1137
|
+
try {
|
|
1138
|
+
await nextSession.websocket.ready;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
closeSession(nextSession, true);
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return nextSession;
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const recoverPrivateStream = (reason: RecoveryReason) => {
|
|
1148
|
+
if (closed || recoveryInFlight) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
clearRecoveryRetry();
|
|
1153
|
+
if (reason === "heartbeat_timeout") {
|
|
1154
|
+
callbacks.onFreshnessChange("stale", "heartbeat_timeout");
|
|
1155
|
+
} else {
|
|
1156
|
+
callbacks.onDisconnected();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const recovery = (async () => {
|
|
1160
|
+
const previousSession = activeSession;
|
|
1161
|
+
activeSession = undefined;
|
|
1162
|
+
closeSession(previousSession, true);
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
const nextSession = await createSession();
|
|
1166
|
+
if (nextSession) {
|
|
1167
|
+
activateSession(nextSession);
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (!closed) {
|
|
1171
|
+
callbacks.onError(
|
|
1172
|
+
toError(error, "Failed to rebuild Binance PAPI private stream"),
|
|
1173
|
+
);
|
|
1174
|
+
scheduleRecoveryRetry(reason);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
})().finally(() => {
|
|
1178
|
+
if (recoveryInFlight === recovery) {
|
|
1179
|
+
recoveryInFlight = undefined;
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
recoveryInFlight = recovery;
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const ready = (async () => {
|
|
1187
|
+
const initialSession = await createSession();
|
|
1188
|
+
if (initialSession) {
|
|
1189
|
+
activateSession(initialSession);
|
|
1190
|
+
}
|
|
1018
1191
|
})();
|
|
1019
1192
|
|
|
1020
1193
|
return {
|
|
@@ -1025,9 +1198,9 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
1025
1198
|
}
|
|
1026
1199
|
|
|
1027
1200
|
closed = true;
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1201
|
+
clearRecoveryRetry();
|
|
1202
|
+
closeSession(activeSession, true);
|
|
1203
|
+
activeSession = undefined;
|
|
1031
1204
|
},
|
|
1032
1205
|
};
|
|
1033
1206
|
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -212,6 +212,7 @@ export interface PrivateStreamCallbacks {
|
|
|
212
212
|
onAccountSnapshot(snapshot: RawAccountBootstrap): void;
|
|
213
213
|
onAccountUpdate(update: RawAccountUpdate): void;
|
|
214
214
|
onOrderUpdate(update: RawOrderUpdate): void;
|
|
215
|
+
onFreshnessChange(freshness: "stale", reason: "heartbeat_timeout"): void;
|
|
215
216
|
onDisconnected(): void;
|
|
216
217
|
onReconnected(): void;
|
|
217
218
|
onError(error: Error): void;
|
|
@@ -222,6 +223,7 @@ export interface PrivateStreamOptions {
|
|
|
222
223
|
reconnectDelayMs: number;
|
|
223
224
|
reconnectMaxDelayMs: number;
|
|
224
225
|
listenKeyKeepAliveMs: number;
|
|
226
|
+
staleAfterMs: number;
|
|
225
227
|
now?: () => number;
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -47,6 +47,7 @@ const DEFAULT_STREAM_OPEN_TIMEOUT_MS = 15_000;
|
|
|
47
47
|
const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
|
|
48
48
|
const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
|
|
49
49
|
const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
|
|
50
|
+
const DEFAULT_PRIVATE_STREAM_STALE_AFTER_MS = 65 * 60_000;
|
|
50
51
|
const DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS = 5_000;
|
|
51
52
|
const DEFAULT_BINANCE_PRIVATE_RECONCILE_INTERVAL_MS = 60_000;
|
|
52
53
|
const MAX_ORDER_TERMINAL_BACKFILLS_PER_RECONCILE = 20;
|
|
@@ -90,6 +91,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
90
91
|
private readonly streamReconnectDelayMs: number;
|
|
91
92
|
private readonly streamReconnectMaxDelayMs: number;
|
|
92
93
|
private readonly listenKeyKeepAliveMs: number;
|
|
94
|
+
private readonly binancePrivateStreamStaleAfterMs: number;
|
|
93
95
|
private readonly binanceRiskPollIntervalMs: number;
|
|
94
96
|
private readonly binancePrivateReconcileIntervalMs: number | undefined;
|
|
95
97
|
private readonly records = new Map<string, PrivateSubscriptionRecord>();
|
|
@@ -116,6 +118,10 @@ export class PrivateSubscriptionCoordinator {
|
|
|
116
118
|
DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
|
|
117
119
|
this.listenKeyKeepAliveMs =
|
|
118
120
|
options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
|
|
121
|
+
this.binancePrivateStreamStaleAfterMs = normalizePositiveInterval(
|
|
122
|
+
options.binance?.privateStreamStaleAfterMs,
|
|
123
|
+
DEFAULT_PRIVATE_STREAM_STALE_AFTER_MS,
|
|
124
|
+
);
|
|
119
125
|
this.binanceRiskPollIntervalMs = normalizePositiveInterval(
|
|
120
126
|
options.binance?.riskPollIntervalMs,
|
|
121
127
|
DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS,
|
|
@@ -1180,6 +1186,30 @@ export class PrivateSubscriptionCoordinator {
|
|
|
1180
1186
|
update,
|
|
1181
1187
|
);
|
|
1182
1188
|
},
|
|
1189
|
+
onFreshnessChange: (_freshness, reason) => {
|
|
1190
|
+
if (record.accountSubscribed) {
|
|
1191
|
+
this.accountConsumer.onPrivateAccountStreamState(
|
|
1192
|
+
record.accountId,
|
|
1193
|
+
record.venue,
|
|
1194
|
+
{
|
|
1195
|
+
runtimeStatus: "reconnecting",
|
|
1196
|
+
ready: record.accountReady,
|
|
1197
|
+
reason,
|
|
1198
|
+
},
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
if (record.ordersSubscribed) {
|
|
1202
|
+
this.orderConsumer.onPrivateOrderStreamState(
|
|
1203
|
+
record.accountId,
|
|
1204
|
+
record.venue,
|
|
1205
|
+
{
|
|
1206
|
+
runtimeStatus: "reconnecting",
|
|
1207
|
+
ready: record.orderReady,
|
|
1208
|
+
reason,
|
|
1209
|
+
},
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1183
1213
|
onDisconnected: () => {
|
|
1184
1214
|
if (record.accountSubscribed) {
|
|
1185
1215
|
this.accountConsumer.onPrivateAccountStreamState(
|
|
@@ -1236,6 +1266,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
1236
1266
|
reconnectDelayMs: this.streamReconnectDelayMs,
|
|
1237
1267
|
reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
|
|
1238
1268
|
listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
|
|
1269
|
+
staleAfterMs: this.binancePrivateStreamStaleAfterMs,
|
|
1239
1270
|
now: () => this.context.now(),
|
|
1240
1271
|
},
|
|
1241
1272
|
{ ...account.options, accountId: account.accountId },
|