@imbingox/acex 0.4.0-beta.13 → 0.4.0-beta.15

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 ADDED
@@ -0,0 +1,182 @@
1
+ # @imbingox/acex
2
+
3
+ ## 0.4.0-beta.15
4
+
5
+ ### Minor Changes
6
+
7
+ - 3f6dcb8: 新增 `AcexError.details.venueError.reason`、订单命令错误的 `details.orderState`,并导出 `isOrderStateUnknown()`,方便调用方用稳定语义区分交易所拒单、限流、余额不足和订单状态未知场景。
8
+
9
+ ## 0.4.0-beta.14
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
15
+ ## 0.4.0-beta.13
16
+
17
+ ### Patch Changes
18
+
19
+ - 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.
20
+
21
+ ## 0.4.0-beta.12
22
+
23
+ ### Patch Changes
24
+
25
+ - 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.
26
+
27
+ ## 0.4.0-beta.11
28
+
29
+ ### Patch Changes
30
+
31
+ - acbdfd8: OrderManager 内部订单主键改为 SDK 生成的 `localOrderId`,并维护 venue `orderId` / `clientOrderId` 反向索引与下单 pending claim,避免 REST 返回前早到的 WS 更新双建订单。公开 API 与类型不变。
32
+
33
+ 行为变化:调用 `createOrder()` 未传 `clientOrderId` 时,SDK 现在会生成合规的 `acex-*` client id 并作为 Binance `newClientOrderId` 发送,返回的 `snapshot.clientOrderId` 也会是该生成值,而不再依赖 Binance 自动生成。
34
+
35
+ ## 0.4.0-beta.10
36
+
37
+ ### Patch Changes
38
+
39
+ - 89f846e: OrderManager 内部订单存储改为 open / closed 分层(按 symbol 嵌套)+ 复合身份索引,终态订单不再无界累积:closed 订单按 symbol 保留最近 N 个(新增可选 `CreateClientOptions.order.maxClosedOrdersPerSymbol`,默认 500,超限按 FIFO 批量裁剪),`getOpenOrders()` 查询不再随历史订单数量增长而变慢。`getOrder()` 对外行为保持不变(仍可只按 `orderId` 或 `clientOrderId` 查询、可省略 `symbol`),`clientOrderId` 多命中时返回最新一笔。
40
+
41
+ ## 0.4.0-beta.9
42
+
43
+ ### Patch Changes
44
+
45
+ - 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.
46
+
47
+ ## 0.4.0-beta.8
48
+
49
+ ### Minor Changes
50
+
51
+ - d874b29: 公开 `AcexError.details` 与 `AcexError.cause`,让调用方在捕获订单、市场目录、server time、market stream 首包超时、account/order bootstrap 等失败时,既能继续使用稳定的 `error.code` 分支,也能读取交易所结构化拒绝原因(`details.venueError.code/message`)和已脱敏的 transport 诊断信息(`details.transport`)。
52
+
53
+ ## 0.4.0-beta.7
54
+
55
+ ### Minor Changes
56
+
57
+ - 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.
58
+
59
+ ## 0.4.0-beta.6
60
+
61
+ ### Minor Changes
62
+
63
+ - f65bab7: 新增 `client.market.reloadMarkets(venue?)` 主动刷新市场目录能力,并公开 `MarketCatalogReloadSummary` 返回每个 venue 的新增、移除、总数和失败摘要。刷新失败会保留旧目录并在对应 summary 中返回错误,方便长运行进程在交易所新增 symbol 后无需重启即可加载新目录。
64
+
65
+ ## 0.4.0-beta.5
66
+
67
+ ### Patch Changes
68
+
69
+ - 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、公共类型与运行时行为均不变,为后续接入新交易所做准备。
70
+
71
+ ## 0.4.0-beta.4
72
+
73
+ ### Minor Changes
74
+
75
+ - 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.
76
+
77
+ ## 0.4.0-beta.3
78
+
79
+ ### Minor Changes
80
+
81
+ - 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.
82
+
83
+ ## 0.4.0-beta.2
84
+
85
+ ### Patch Changes
86
+
87
+ - d9bacb6: 对外错误信息不再泄漏签名与密钥。请求失败时,错误的 `message` 与 URL 会对 `signature`、API key、`listenKey`、`token`、`passphrase` 等敏感 query 参数及对应的 JSON body 字段做脱敏(替换为 `[REDACTED]`),私有订阅 bootstrap 失败路径同样会对透传的错误信息脱敏。此前这些敏感值可能随错误信息进入日志。属向后兼容的行为修复,不改变公共类型与 API 形状。
88
+
89
+ ## 0.4.0-beta.1
90
+
91
+ ### Minor Changes
92
+
93
+ - adc9274: 公共 snapshot / market 数值字段(包括 `L1Book`、`FundingRateSnapshot`、`OrderSnapshot`、`BalanceSnapshot`、`PositionSnapshot`、`RiskSnapshot`、`MarketDefinition` 及 lending facets)由 `BigNumber` 改为 canonical 十进制 string。
94
+
95
+ 这是破坏性 public contract 变更:`snapshot.bidPrice.minus(...)`、`.multipliedBy(...)` 等链式调用不再可用,消费者需要改为 `new BigNumber(field)` 自行解析后运算(SDK 仍保留 `export { BigNumber }`)。不要用 `parseFloat()` 解析这些字段,否则会退回 JS 浮点精度。输入侧 `DecimalInput` 不变,仍接受 string / number / `BigNumber`。
96
+
97
+ ## 0.3.1-beta.0
98
+
99
+ ### Patch Changes
100
+
101
+ - 19f60bc: Binance 行情订阅现在复用 WebSocket 连接:同一 connectionKey / base URL 下多个 symbol 复用物理连接(例如 USDM L1 与 funding 因 base URL 不同会分开),通过 JSON `SUBSCRIBE`/`UNSUBSCRIBE` 动态增删订阅,断线重连后自动重放,单连接订阅数达上限会自动开新连接。行情层改为按 venue 分派 adapter,为接入更多交易所打基础。公开 API 不变。
102
+
103
+ ## 0.3.0
104
+
105
+ ### Minor Changes
106
+
107
+ - 14d25cb: 重命名账户风险权益字段并拆分净值与风控口径。`RiskSnapshot.equity` 替换为 `netEquity` / `riskEquity`,`actualLeverage` 替换为 `riskLeverage`;Binance 使用 `actualEquity` / `accountEquity` 分别映射净权益和风控折算权益,Juplend 使用清算阈值折算权益填充 `riskEquity`。
108
+ - 50e4e09: 通过周期性 REST polling 刷新 Binance 账户风险和 mark-to-market 仓位字段。`RiskSnapshot` 现在暴露风控口径的 `riskLeverage`,Binance 账户运行时配置新增 `account.binance.riskPollIntervalMs`。
109
+ - 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.
110
+ - 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.
111
+ - 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.
112
+ - 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.
113
+ - ea9a4a7: Add top-level venue capability queries for SDK runtime support by venue.
114
+
115
+ ### Patch Changes
116
+
117
+ - 46d1291: Include `docs/api.md` in the published npm package.
118
+
119
+ ## 0.3.0-beta.6
120
+
121
+ ### Minor Changes
122
+
123
+ - 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.
124
+
125
+ ## 0.3.0-beta.5
126
+
127
+ ### Minor Changes
128
+
129
+ - 14d25cb: 重命名账户风险权益字段并拆分净值与风控口径。`RiskSnapshot.equity` 替换为 `netEquity` / `riskEquity`,`actualLeverage` 替换为 `riskLeverage`;Binance 使用 `actualEquity` / `accountEquity` 分别映射净权益和风控折算权益,Juplend 使用清算阈值折算权益填充 `riskEquity`。
130
+
131
+ ## 0.3.0-beta.4
132
+
133
+ ### Minor Changes
134
+
135
+ - 50e4e09: 通过周期性 REST polling 刷新 Binance 账户风险和 mark-to-market 仓位字段。`RiskSnapshot` 现在暴露 `actualLeverage`,Binance 账户运行时配置新增 `account.binance.riskPollIntervalMs`。
136
+
137
+ ## 0.3.0-beta.3
138
+
139
+ ### Minor Changes
140
+
141
+ - ea9a4a7: Add top-level venue capability queries for SDK runtime support by venue.
142
+
143
+ ### Patch Changes
144
+
145
+ - 46d1291: Include `docs/api.md` in the published npm package.
146
+
147
+ ## 0.3.0-beta.2
148
+
149
+ ### Minor Changes
150
+
151
+ - 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.
152
+
153
+ ## 0.3.0-beta.1
154
+
155
+ ### Minor Changes
156
+
157
+ - 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.
158
+
159
+ ## 0.3.0-beta.0
160
+
161
+ ### Minor Changes
162
+
163
+ - 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.
164
+
165
+ ## 0.2.0
166
+
167
+ ### Minor Changes
168
+
169
+ - 5dcc3c1: Add Binance funding rate market data stream with per-stream market data status.
170
+ - baeab15: Add Binance PAPI private account and order support, including the first `createOrder`, `cancelOrder`, and `cancelAllOrders` APIs.
171
+
172
+ ## 0.1.0-beta.4
173
+
174
+ ### Minor Changes
175
+
176
+ - 5dcc3c1: Add Binance funding rate market data stream with per-stream market data status.
177
+
178
+ ## 0.1.0-beta.3
179
+
180
+ ### Minor Changes
181
+
182
+ - baeab15: Add Binance PAPI private account and order support, including the first `createOrder`, `cancelOrder`, and `cancelAllOrders` APIs.
package/docs/api.md CHANGED
@@ -918,20 +918,43 @@ type OrderEvent =
918
918
  可预期错误统一抛 `AcexError`:
919
919
 
920
920
  ```ts
921
- import { AcexError } from "@imbingox/acex";
921
+ import { AcexError, isOrderStateUnknown } from "@imbingox/acex";
922
922
 
923
923
  try {
924
- await client.market.subscribeL1Book({ venue: "binance", symbol: "X/Y:Z" });
924
+ await client.order.createOrder({
925
+ accountId: "main-binance",
926
+ symbol: "BTC/USDT:USDT",
927
+ side: "buy",
928
+ type: "limit",
929
+ price: "101000",
930
+ amount: "0.01",
931
+ postOnly: true,
932
+ });
925
933
  } catch (error) {
926
934
  if (error instanceof AcexError) {
927
935
  console.log(error.code);
928
936
  console.log(error.details?.venueError?.code);
937
+ console.log(error.details?.venueError?.reason);
938
+ console.log(error.details?.orderState);
929
939
  console.log(error.details?.transport?.status);
940
+ console.log(isOrderStateUnknown(error));
930
941
  }
931
942
  }
932
943
  ```
933
944
 
934
- `details.venueError` 是读取交易所结构化拒绝原因的首选字段;`details.transport` 保存已脱敏的 HTTP / transport 诊断信息;`cause` 保留底层错误链。
945
+ `details.venueError` 是读取交易所结构化拒绝原因的首选字段;`details.venueError.reason` 是 SDK 归一后的稳定原因,原始 `code/message` 会继续保留。`details.orderState` 只在订单命令错误中填写:`not_placed` 表示 SDK 判定订单未落地,`unknown` 表示请求可能已经到达交易所,应由调用方后续查询或对账确认。`details.transport` 保存已脱敏的 HTTP / transport 诊断信息;`cause` 保留底层错误链。
946
+
947
+ 归一错误原因:
948
+
949
+ | `VenueErrorReason` | 典型含义 |
950
+ |---|---|
951
+ | `insufficient_balance` | 余额或保证金不足 |
952
+ | `would_take` | Post Only / maker-only 订单会吃单而被拒 |
953
+ | `order_not_found` | 订单不存在、已不在可撤订单簿或超过交易所可查询范围 |
954
+ | `filter_violation` | 价格、数量、精度、最小名义金额或订单数量限制不满足 |
955
+ | `rate_limited` | 请求权重、订单频率或账户排队被限流 |
956
+ | `timestamp_out_of_sync` | 请求时间戳或 `recvWindow` 与交易所时间不匹配 |
957
+ | `unknown` | 交易所原始码未归入稳定语义,调用方仍可读取原始 `code/message` |
935
958
 
936
959
  完整错误码:
937
960
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.4.0-beta.13",
3
+ "version": "0.4.0-beta.15",
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",
@@ -0,0 +1,53 @@
1
+ import type { VenueErrorReason } from "../../errors.ts";
2
+
3
+ const BINANCE_RATE_LIMITED_CODES = new Set([
4
+ "-1003",
5
+ "-1008",
6
+ "-1015",
7
+ "-5041",
8
+ ]);
9
+ const BINANCE_TIMESTAMP_OUT_OF_SYNC_CODES = new Set(["-1021", "-5028"]);
10
+ const BINANCE_ORDER_NOT_FOUND_CODES = new Set(["-2011", "-2013"]);
11
+ const BINANCE_INSUFFICIENT_BALANCE_CODES = new Set(["-2018", "-2019"]);
12
+ const BINANCE_WOULD_TAKE_CODES = new Set(["-5022"]);
13
+ const BINANCE_FILTER_VIOLATION_CODES = new Set([
14
+ "-4131",
15
+ "-2025",
16
+ "-1111",
17
+ "-4002",
18
+ "-4004",
19
+ "-4005",
20
+ "-4013",
21
+ "-4014",
22
+ "-4016",
23
+ "-4023",
24
+ "-4024",
25
+ "-4029",
26
+ "-4030",
27
+ "-4164",
28
+ "-4183",
29
+ "-4184",
30
+ ]);
31
+
32
+ export function normalizeBinanceErrorCode(code: string): VenueErrorReason {
33
+ if (BINANCE_RATE_LIMITED_CODES.has(code)) {
34
+ return "rate_limited";
35
+ }
36
+ if (BINANCE_TIMESTAMP_OUT_OF_SYNC_CODES.has(code)) {
37
+ return "timestamp_out_of_sync";
38
+ }
39
+ if (BINANCE_ORDER_NOT_FOUND_CODES.has(code)) {
40
+ return "order_not_found";
41
+ }
42
+ if (BINANCE_INSUFFICIENT_BALANCE_CODES.has(code)) {
43
+ return "insufficient_balance";
44
+ }
45
+ if (BINANCE_WOULD_TAKE_CODES.has(code)) {
46
+ return "would_take";
47
+ }
48
+ if (BINANCE_FILTER_VIOLATION_CODES.has(code)) {
49
+ return "filter_violation";
50
+ }
51
+
52
+ return "unknown";
53
+ }
@@ -33,6 +33,7 @@ import type {
33
33
  RawRiskUpdate,
34
34
  StreamHandle,
35
35
  } from "../types.ts";
36
+ import { normalizeBinanceErrorCode } from "./error-codes.ts";
36
37
  import { parseBinanceRateLimitUsage } from "./rate-limit.ts";
37
38
 
38
39
  type TimerHandle = ReturnType<typeof setInterval>;
@@ -183,7 +184,6 @@ const LISTEN_KEY_KEEPALIVE_RETRY_POLICY: HttpRetryPolicy = {
183
184
  idempotent: true,
184
185
  maxAttempts: 3,
185
186
  };
186
- const BINANCE_ORDER_NOT_FOUND_CODES = new Set(["-2011", "-2013"]);
187
187
  function getBinancePapiHttpMessages(timeoutMs: number): HttpClientMessages {
188
188
  return {
189
189
  http: ({ status, statusText, url, rawBody }) =>
@@ -628,7 +628,7 @@ function isBinanceOrderNotFound(error: unknown): boolean {
628
628
 
629
629
  try {
630
630
  const parsed = JSON.parse(rawBody) as { code?: unknown };
631
- return BINANCE_ORDER_NOT_FOUND_CODES.has(`${parsed.code}`);
631
+ return normalizeBinanceErrorCode(`${parsed.code}`) === "order_not_found";
632
632
  } catch {
633
633
  return false;
634
634
  }
@@ -676,6 +676,10 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
676
676
  } = {},
677
677
  ) {}
678
678
 
679
+ normalizeVenueErrorCode(code: string) {
680
+ return normalizeBinanceErrorCode(code);
681
+ }
682
+
679
683
  async bootstrapAccount(
680
684
  credentials: AccountCredentials,
681
685
  accountOptions?: Record<string, unknown>,
@@ -1,3 +1,4 @@
1
+ import type { VenueErrorReason } from "../errors.ts";
1
2
  import type {
2
3
  AccountCredentials,
3
4
  CreateOrderType,
@@ -233,6 +234,7 @@ export interface PrivateUserDataAdapter {
233
234
  readonly notes: string[];
234
235
  readonly accountCapabilities: VenueAccountCapabilities;
235
236
  readonly orderCapabilities: VenueOrderCapabilities;
237
+ normalizeVenueErrorCode?(code: string): VenueErrorReason;
236
238
  bootstrapAccount(
237
239
  credentials: AccountCredentials,
238
240
  accountOptions?: Record<string, unknown>,
@@ -4,6 +4,7 @@ import type {
4
4
  RawOpenOrdersSnapshot,
5
5
  RawOrderUpdate,
6
6
  } from "../adapters/types.ts";
7
+ import type { VenueErrorReason } from "../errors.ts";
7
8
  import type {
8
9
  AccountCredentials,
9
10
  AcexInternalError,
@@ -30,6 +31,10 @@ export interface ClientContext {
30
31
  assertStarted(): void;
31
32
  getRegisteredAccount(accountId: string): RegisteredAccountRecord;
32
33
  getPrivateOrderCapabilities(venue: Venue): VenueOrderCapabilities | undefined;
34
+ normalizeVenueErrorCode(
35
+ venue: Venue,
36
+ code: string,
37
+ ): VenueErrorReason | undefined;
33
38
  ensurePrivateCredentials(accountId: string): void;
34
39
  subscribePrivateAccountFeed(accountId: string): Promise<void>;
35
40
  unsubscribePrivateAccountFeed(accountId: string): void;
@@ -13,6 +13,7 @@ import {
13
13
  AcexError,
14
14
  type AcexErrorCode,
15
15
  buildAcexErrorDetails,
16
+ type VenueErrorReason,
16
17
  } from "../errors.ts";
17
18
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
18
19
  import { matchesHealthFilter } from "../internal/filters.ts";
@@ -312,6 +313,13 @@ export class AcexClientImpl implements AcexClient, ClientContext {
312
313
  return this.privateAdapters.get(venue)?.orderCapabilities;
313
314
  }
314
315
 
316
+ normalizeVenueErrorCode(
317
+ venue: Venue,
318
+ code: string,
319
+ ): VenueErrorReason | undefined {
320
+ return this.privateAdapters.get(venue)?.normalizeVenueErrorCode?.(code);
321
+ }
322
+
315
323
  ensurePrivateCredentials(accountId: string): void {
316
324
  const account = this.getRegisteredAccount(accountId);
317
325
  if (
package/src/errors.ts CHANGED
@@ -27,9 +27,19 @@ export type AcexErrorTransportKind =
27
27
  | "rate_limited"
28
28
  | "parse";
29
29
 
30
+ export type VenueErrorReason =
31
+ | "insufficient_balance"
32
+ | "would_take"
33
+ | "order_not_found"
34
+ | "filter_violation"
35
+ | "rate_limited"
36
+ | "timestamp_out_of_sync"
37
+ | "unknown";
38
+
30
39
  export interface AcexVenueErrorDetails {
31
40
  readonly code?: string;
32
41
  readonly message?: string;
42
+ readonly reason?: VenueErrorReason;
33
43
  }
34
44
 
35
45
  export interface AcexErrorTransportDetails {
@@ -49,6 +59,7 @@ export interface AcexErrorDetails {
49
59
  readonly symbol?: string;
50
60
  readonly venueError?: AcexVenueErrorDetails;
51
61
  readonly transport?: AcexErrorTransportDetails;
62
+ readonly orderState?: "not_placed" | "unknown";
52
63
  }
53
64
 
54
65
  export interface AcexErrorOptions {
@@ -91,6 +102,10 @@ export function buildAcexErrorDetails(
91
102
  return hasDetails(details) ? details : undefined;
92
103
  }
93
104
 
105
+ export function isOrderStateUnknown(error: unknown): boolean {
106
+ return error instanceof AcexError && error.details?.orderState === "unknown";
107
+ }
108
+
94
109
  export function formatAcexErrorMessage(
95
110
  message: string,
96
111
  details?: AcexErrorDetails,
@@ -166,7 +181,8 @@ function hasDetails(details: AcexErrorDetails): boolean {
166
181
  details.accountId ||
167
182
  details.symbol ||
168
183
  details.venueError ||
169
- details.transport,
184
+ details.transport ||
185
+ details.orderState,
170
186
  );
171
187
  }
172
188
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export type {
7
7
  AcexErrorTransportDetails,
8
8
  AcexErrorTransportKind,
9
9
  AcexVenueErrorDetails,
10
+ VenueErrorReason,
10
11
  } from "./errors.ts";
11
- export { AcexError } from "./errors.ts";
12
+ export { AcexError, isOrderStateUnknown } from "./errors.ts";
12
13
  export * from "./types/index.ts";
@@ -10,6 +10,7 @@ import type {
10
10
  PrivateOrderDataConsumer,
11
11
  PrivateSubscriptionState,
12
12
  } from "../client/context.ts";
13
+ import type { AcexErrorDetails, AcexErrorTransportKind } from "../errors.ts";
13
14
  import {
14
15
  AcexError,
15
16
  buildAcexErrorDetails,
@@ -74,6 +75,15 @@ import {
74
75
  setSnapshot,
75
76
  } from "./order/store.ts";
76
77
 
78
+ type OrderCommandErrorCode =
79
+ | "ORDER_CANCEL_ALL_FAILED"
80
+ | "ORDER_CANCEL_FAILED"
81
+ | "ORDER_CREATE_FAILED";
82
+
83
+ type OrderErrorCode = OrderCommandErrorCode | "ORDER_INPUT_INVALID";
84
+
85
+ type OrderCommandOrderState = NonNullable<AcexErrorDetails["orderState"]>;
86
+
77
87
  export class OrderManagerImpl
78
88
  implements
79
89
  OrderManager,
@@ -990,12 +1000,7 @@ export class OrderManagerImpl
990
1000
  }
991
1001
 
992
1002
  private createError(
993
- code:
994
- | "VENUE_NOT_SUPPORTED"
995
- | "ORDER_CANCEL_ALL_FAILED"
996
- | "ORDER_CANCEL_FAILED"
997
- | "ORDER_CREATE_FAILED"
998
- | "ORDER_INPUT_INVALID",
1003
+ code: "VENUE_NOT_SUPPORTED" | OrderCommandErrorCode | "ORDER_INPUT_INVALID",
999
1004
  message: string,
1000
1005
  metadata: {
1001
1006
  accountId: string;
@@ -1003,17 +1008,14 @@ export class OrderManagerImpl
1003
1008
  symbol?: string;
1004
1009
  },
1005
1010
  ): AcexError {
1006
- const details = buildAcexErrorDetails(metadata);
1011
+ const details = this.buildOrderErrorDetails(code, metadata);
1007
1012
  const error = new AcexError(code, message, { details });
1008
1013
  this.context.publishRuntimeError("order", error, metadata);
1009
1014
  return error;
1010
1015
  }
1011
1016
 
1012
1017
  private wrapCommandError(
1013
- code:
1014
- | "ORDER_CANCEL_ALL_FAILED"
1015
- | "ORDER_CANCEL_FAILED"
1016
- | "ORDER_CREATE_FAILED",
1018
+ code: OrderCommandErrorCode,
1017
1019
  message: string,
1018
1020
  error: unknown,
1019
1021
  metadata: {
@@ -1031,10 +1033,103 @@ export class OrderManagerImpl
1031
1033
  error instanceof Error ? error : new Error(message),
1032
1034
  metadata,
1033
1035
  );
1034
- const details = buildAcexErrorDetails(metadata, error);
1036
+ const details = this.buildOrderErrorDetails(code, metadata, error);
1035
1037
  return new AcexError(code, formatAcexErrorMessage(message, details), {
1036
1038
  cause: error,
1037
1039
  details,
1038
1040
  });
1039
1041
  }
1042
+
1043
+ private buildOrderErrorDetails(
1044
+ code: "VENUE_NOT_SUPPORTED" | OrderErrorCode,
1045
+ metadata: {
1046
+ accountId: string;
1047
+ venue: Venue;
1048
+ symbol?: string;
1049
+ },
1050
+ error?: unknown,
1051
+ ): AcexErrorDetails | undefined {
1052
+ const details = buildAcexErrorDetails(metadata, error);
1053
+ if (!details || !isOrderErrorCode(code)) {
1054
+ return details;
1055
+ }
1056
+
1057
+ const detailsWithReason = this.addVenueErrorReason(metadata.venue, details);
1058
+ return {
1059
+ ...detailsWithReason,
1060
+ orderState: getOrderState(code, detailsWithReason),
1061
+ };
1062
+ }
1063
+
1064
+ private addVenueErrorReason(
1065
+ venue: Venue,
1066
+ details: AcexErrorDetails,
1067
+ ): AcexErrorDetails {
1068
+ const venueErrorCode = details.venueError?.code;
1069
+ if (!venueErrorCode) {
1070
+ return details;
1071
+ }
1072
+
1073
+ const reason = this.context.normalizeVenueErrorCode(venue, venueErrorCode);
1074
+ if (!reason) {
1075
+ return details;
1076
+ }
1077
+
1078
+ return {
1079
+ ...details,
1080
+ venueError: {
1081
+ ...details.venueError,
1082
+ reason,
1083
+ },
1084
+ };
1085
+ }
1086
+ }
1087
+
1088
+ function isOrderErrorCode(
1089
+ code: "VENUE_NOT_SUPPORTED" | OrderErrorCode,
1090
+ ): code is OrderErrorCode {
1091
+ return (
1092
+ code === "ORDER_INPUT_INVALID" ||
1093
+ code === "ORDER_CREATE_FAILED" ||
1094
+ code === "ORDER_CANCEL_FAILED" ||
1095
+ code === "ORDER_CANCEL_ALL_FAILED"
1096
+ );
1097
+ }
1098
+
1099
+ function getOrderState(
1100
+ code: OrderErrorCode,
1101
+ details: AcexErrorDetails,
1102
+ ): OrderCommandOrderState {
1103
+ if (code === "ORDER_INPUT_INVALID") {
1104
+ return "not_placed";
1105
+ }
1106
+
1107
+ const transport = details.transport;
1108
+ if (!transport) {
1109
+ return details.venueError ? "not_placed" : "unknown";
1110
+ }
1111
+
1112
+ if (isUnknownOrderTransportKind(transport.kind)) {
1113
+ return "unknown";
1114
+ }
1115
+ if (transport.kind === "rate_limited") {
1116
+ return "not_placed";
1117
+ }
1118
+ if (transport.status !== undefined && transport.status >= 500) {
1119
+ return "unknown";
1120
+ }
1121
+ if (details.venueError) {
1122
+ return "not_placed";
1123
+ }
1124
+ if (transport.status !== undefined && transport.status < 500) {
1125
+ return "not_placed";
1126
+ }
1127
+
1128
+ return "unknown";
1129
+ }
1130
+
1131
+ function isUnknownOrderTransportKind(
1132
+ kind: AcexErrorTransportKind | undefined,
1133
+ ): boolean {
1134
+ return kind === "timeout" || kind === "network" || kind === "parse";
1040
1135
  }