@imbingox/acex 0.4.0-beta.14 → 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 +6 -0
- package/docs/api.md +26 -3
- package/package.json +1 -1
- package/src/adapters/binance/error-codes.ts +53 -0
- package/src/adapters/binance/private-adapter.ts +6 -2
- package/src/adapters/types.ts +2 -0
- package/src/client/context.ts +5 -0
- package/src/client/runtime.ts +8 -0
- package/src/errors.ts +17 -1
- package/src/index.ts +2 -1
- package/src/managers/order-manager.ts +107 -12
package/CHANGELOG.md
CHANGED
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.
|
|
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
|
@@ -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
|
|
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>,
|
package/src/adapters/types.ts
CHANGED
|
@@ -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>,
|
package/src/client/context.ts
CHANGED
|
@@ -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;
|
package/src/client/runtime.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
}
|