@imbingox/acex 0.4.0-beta.15 → 0.4.0-beta.16
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 +6 -3
- package/package.json +1 -1
- package/src/client/context.ts +22 -0
- package/src/client/private-subscription-coordinator.ts +113 -5
- package/src/client/runtime.ts +1 -0
- package/src/managers/order/data-status.ts +9 -0
- package/src/managers/order/model.ts +3 -0
- package/src/managers/order/snapshot.ts +1 -0
- package/src/managers/order-manager.ts +202 -15
- package/src/types/order.ts +2 -1
- package/src/types/shared.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @imbingox/acex
|
|
2
2
|
|
|
3
|
+
## 0.4.0-beta.16
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bdaf9ea: 订单生命周期增加 confirmed-missing 收尾与 pending claim TTL:`OrderStatus` 新增 `unknown` 终态,open 订单在 reconcile 单笔回查连续确认不存在后会移入 closed;`CreateClientOptions.order` 新增 `missingOrderEvictionThreshold` 与 `pendingClaimTtlMs`,用于配置幽灵 open 订单驱逐阈值和 `createOrder` timeout claim 回查 TTL。
|
|
8
|
+
|
|
3
9
|
## 0.4.0-beta.15
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/docs/api.md
CHANGED
|
@@ -442,7 +442,7 @@ interface OrderManager {
|
|
|
442
442
|
|
|
443
443
|
### 7.3 本地缓存与查询
|
|
444
444
|
|
|
445
|
-
- OrderManager 内部按 open / closed 分层缓存订单。**closed(filled / canceled / rejected / expired)订单按 symbol 各保留最近 N 个**,`N = CreateClientOptions.order.maxClosedOrdersPerSymbol`(默认 500,非正或非整数回退默认),超限按 FIFO 裁剪最旧;**open 订单不受此上限限制**。`getOpenOrders()` 查询复杂度与历史终态订单数量无关。
|
|
445
|
+
- OrderManager 内部按 open / closed 分层缓存订单。**closed(filled / canceled / rejected / expired / unknown)订单按 symbol 各保留最近 N 个**,`N = CreateClientOptions.order.maxClosedOrdersPerSymbol`(默认 500,非正或非整数回退默认),超限按 FIFO 裁剪最旧;**open 订单不受此上限限制**。`getOpenOrders()` 查询复杂度与历史终态订单数量无关。
|
|
446
446
|
- `getOrder(input)` 需带 `orderId` 或 `clientOrderId`(否则返回 `undefined`),`symbol` 可选:
|
|
447
447
|
- **精确查单推荐传 `symbol + orderId`**(O(1) 精确索引、唯一命中)。
|
|
448
448
|
- 仅 `clientOrderId` 查询可命中 open 与未被裁剪的 closed;当 `clientOrderId` 唯一(你自定义的或 SDK 生成的 `acex-*`)时可精确命中,但同一 `clientOrderId` 命中多笔时返回**最新一笔**(精确定位历史某一笔请用 `symbol + orderId`)。
|
|
@@ -450,7 +450,7 @@ interface OrderManager {
|
|
|
450
450
|
- 同时给 `orderId` 与 `clientOrderId` 时,两者都匹配才命中。
|
|
451
451
|
- 已超出保留上限被裁剪的 closed 订单将查不到(返回 `undefined`)。
|
|
452
452
|
|
|
453
|
-
Order 事件用于消费订单状态变化和 open orders 快照校准。Binance private reconcile 会先用 `/papi/v1/um/openOrders` 校验当前 open set;本地 open order 从 open set 消失时,SDK 会优先查询单笔订单终态并发布 `order.filled` / `order.canceled`
|
|
453
|
+
Order 事件用于消费订单状态变化和 open orders 快照校准。Binance private reconcile 会先用 `/papi/v1/um/openOrders` 校验当前 open set;本地 open order 从 open set 消失时,SDK 会优先查询单笔订单终态并发布 `order.filled` / `order.canceled` 等事件。若单笔查询连续确认订单不存在(默认 3 次,`CreateClientOptions.order.missingOrderEvictionThreshold` 可配置),SDK 会把该订单终态化为 `status: "unknown"`、移出 open 缓存并发布一次 runtime error;网络/超时/限流等 transport 错误不会计入该阈值。`createOrder()` 超时保留的 pending claim 会在 reconcile 周期里按 `CreateClientOptions.order.pendingClaimTtlMs`(默认 90s)过期回查:查到订单则正常入库,确认不存在则清理 claim 并发布 runtime error;无 `fetchOrder` 能力的 venue 会保守保留 claim。
|
|
454
454
|
|
|
455
455
|
```ts
|
|
456
456
|
for await (const event of client.order.events.updates({
|
|
@@ -499,7 +499,8 @@ type OrderStatus =
|
|
|
499
499
|
| "filled"
|
|
500
500
|
| "canceled"
|
|
501
501
|
| "rejected"
|
|
502
|
-
| "expired"
|
|
502
|
+
| "expired"
|
|
503
|
+
| "unknown";
|
|
503
504
|
|
|
504
505
|
type PrivateRuntimeReason =
|
|
505
506
|
| "credentials_missing"
|
|
@@ -597,6 +598,8 @@ interface CreateClientOptions {
|
|
|
597
598
|
};
|
|
598
599
|
order?: {
|
|
599
600
|
maxClosedOrdersPerSymbol?: number;
|
|
601
|
+
missingOrderEvictionThreshold?: number;
|
|
602
|
+
pendingClaimTtlMs?: number;
|
|
600
603
|
};
|
|
601
604
|
}
|
|
602
605
|
|
package/package.json
CHANGED
package/src/client/context.ts
CHANGED
|
@@ -73,6 +73,13 @@ export interface PrivateSubscriptionState {
|
|
|
73
73
|
lastReadyAt?: number;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export interface ExpiredPendingOrderClaim {
|
|
77
|
+
venueClientOrderId: string;
|
|
78
|
+
localOrderId: string;
|
|
79
|
+
symbol: string;
|
|
80
|
+
claimedAt: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
export interface PrivateAccountDataConsumer {
|
|
77
84
|
onPrivateAccountPending(accountId: string, venue: Venue): void;
|
|
78
85
|
onPrivateAccountBootstrap(
|
|
@@ -119,7 +126,22 @@ export interface PrivateOrderDataConsumer {
|
|
|
119
126
|
update: RawOrderUpdate,
|
|
120
127
|
options?: { requestStartedAt?: number; preserveStatus?: boolean },
|
|
121
128
|
): void;
|
|
129
|
+
onPrivateOrderConfirmedMissing(
|
|
130
|
+
accountId: string,
|
|
131
|
+
venue: Venue,
|
|
132
|
+
order: OrderSnapshot,
|
|
133
|
+
): void;
|
|
122
134
|
getPrivateOpenOrders(accountId: string): OrderSnapshot[];
|
|
135
|
+
getExpiredPrivateOrderClaims(
|
|
136
|
+
accountId: string,
|
|
137
|
+
now: number,
|
|
138
|
+
ttlMs: number,
|
|
139
|
+
): ExpiredPendingOrderClaim[];
|
|
140
|
+
onPrivateOrderClaimNotFound(
|
|
141
|
+
accountId: string,
|
|
142
|
+
venue: Venue,
|
|
143
|
+
claim: ExpiredPendingOrderClaim,
|
|
144
|
+
): void;
|
|
123
145
|
onPrivateOrderStreamState(
|
|
124
146
|
accountId: string,
|
|
125
147
|
venue: Venue,
|
|
@@ -12,12 +12,14 @@ import {
|
|
|
12
12
|
import { isTransportError } from "../internal/http-client.ts";
|
|
13
13
|
import type {
|
|
14
14
|
AccountRuntimeOptions,
|
|
15
|
+
OrderRuntimeOptions,
|
|
15
16
|
OrderSnapshot,
|
|
16
17
|
PrivateRuntimeReason,
|
|
17
18
|
Venue,
|
|
18
19
|
} from "../types/index.ts";
|
|
19
20
|
import type {
|
|
20
21
|
ClientContext,
|
|
22
|
+
ExpiredPendingOrderClaim,
|
|
21
23
|
PrivateAccountDataConsumer,
|
|
22
24
|
PrivateOrderDataConsumer,
|
|
23
25
|
RegisteredAccountRecord,
|
|
@@ -50,6 +52,7 @@ const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
|
|
|
50
52
|
const DEFAULT_PRIVATE_STREAM_STALE_AFTER_MS = 65 * 60_000;
|
|
51
53
|
const DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS = 5_000;
|
|
52
54
|
const DEFAULT_BINANCE_PRIVATE_RECONCILE_INTERVAL_MS = 60_000;
|
|
55
|
+
const DEFAULT_PENDING_CLAIM_TTL_MS = 90_000;
|
|
53
56
|
const MAX_ORDER_TERMINAL_BACKFILLS_PER_RECONCILE = 20;
|
|
54
57
|
const MAX_ORDER_TERMINAL_BACKFILL_CONCURRENCY = 4;
|
|
55
58
|
|
|
@@ -94,6 +97,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
94
97
|
private readonly binancePrivateStreamStaleAfterMs: number;
|
|
95
98
|
private readonly binanceRiskPollIntervalMs: number;
|
|
96
99
|
private readonly binancePrivateReconcileIntervalMs: number | undefined;
|
|
100
|
+
private readonly pendingClaimTtlMs: number;
|
|
97
101
|
private readonly records = new Map<string, PrivateSubscriptionRecord>();
|
|
98
102
|
|
|
99
103
|
constructor(
|
|
@@ -102,6 +106,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
102
106
|
accountConsumer: PrivateAccountDataConsumer,
|
|
103
107
|
orderConsumer: PrivateOrderDataConsumer,
|
|
104
108
|
options: AccountRuntimeOptions = {},
|
|
109
|
+
orderOptions: OrderRuntimeOptions = {},
|
|
105
110
|
) {
|
|
106
111
|
this.context = context;
|
|
107
112
|
this.adapters = new Map(
|
|
@@ -130,6 +135,10 @@ export class PrivateSubscriptionCoordinator {
|
|
|
130
135
|
options.binance?.privateReconcileIntervalMs,
|
|
131
136
|
DEFAULT_BINANCE_PRIVATE_RECONCILE_INTERVAL_MS,
|
|
132
137
|
);
|
|
138
|
+
this.pendingClaimTtlMs = normalizePositiveInterval(
|
|
139
|
+
orderOptions.pendingClaimTtlMs,
|
|
140
|
+
DEFAULT_PENDING_CLAIM_TTL_MS,
|
|
141
|
+
);
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
async subscribeAccountFeed(accountId: string): Promise<void> {
|
|
@@ -940,6 +949,12 @@ export class PrivateSubscriptionCoordinator {
|
|
|
940
949
|
orderGeneration,
|
|
941
950
|
disappeared,
|
|
942
951
|
);
|
|
952
|
+
await this.reconcileExpiredPendingOrderClaims(
|
|
953
|
+
record,
|
|
954
|
+
account,
|
|
955
|
+
generation,
|
|
956
|
+
orderGeneration,
|
|
957
|
+
);
|
|
943
958
|
} catch (error) {
|
|
944
959
|
if (
|
|
945
960
|
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
@@ -1054,11 +1069,104 @@ export class PrivateSubscriptionCoordinator {
|
|
|
1054
1069
|
}
|
|
1055
1070
|
|
|
1056
1071
|
if (!update) {
|
|
1057
|
-
this.
|
|
1058
|
-
record,
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1072
|
+
this.orderConsumer.onPrivateOrderConfirmedMissing(
|
|
1073
|
+
record.accountId,
|
|
1074
|
+
record.venue,
|
|
1075
|
+
order,
|
|
1076
|
+
);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
this.orderConsumer.onPrivateOrderUpdate(
|
|
1081
|
+
record.accountId,
|
|
1082
|
+
record.venue,
|
|
1083
|
+
update,
|
|
1084
|
+
{
|
|
1085
|
+
requestStartedAt,
|
|
1086
|
+
},
|
|
1087
|
+
);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (
|
|
1090
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1091
|
+
) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
this.handleOrderReconcileError(record, error);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private async reconcileExpiredPendingOrderClaims(
|
|
1100
|
+
record: PrivateSubscriptionRecord,
|
|
1101
|
+
account: RegisteredAccountRecord,
|
|
1102
|
+
generation: number,
|
|
1103
|
+
orderGeneration: number,
|
|
1104
|
+
): Promise<void> {
|
|
1105
|
+
const adapter = this.getAdapter(record.venue);
|
|
1106
|
+
if (
|
|
1107
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1108
|
+
) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (!adapter.fetchOrder) {
|
|
1113
|
+
// Pending createOrder claims can only be retired after the venue confirms
|
|
1114
|
+
// the clientOrderId is absent. Adapters without fetchOrder keep claims.
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const expiredClaims = this.orderConsumer.getExpiredPrivateOrderClaims(
|
|
1119
|
+
record.accountId,
|
|
1120
|
+
this.context.now(),
|
|
1121
|
+
this.pendingClaimTtlMs,
|
|
1122
|
+
);
|
|
1123
|
+
for (const claim of expiredClaims) {
|
|
1124
|
+
await this.reconcileExpiredPendingOrderClaim(
|
|
1125
|
+
record,
|
|
1126
|
+
account,
|
|
1127
|
+
generation,
|
|
1128
|
+
orderGeneration,
|
|
1129
|
+
claim,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private async reconcileExpiredPendingOrderClaim(
|
|
1135
|
+
record: PrivateSubscriptionRecord,
|
|
1136
|
+
account: RegisteredAccountRecord,
|
|
1137
|
+
generation: number,
|
|
1138
|
+
orderGeneration: number,
|
|
1139
|
+
claim: ExpiredPendingOrderClaim,
|
|
1140
|
+
): Promise<void> {
|
|
1141
|
+
const adapter = this.getAdapter(record.venue);
|
|
1142
|
+
if (
|
|
1143
|
+
!adapter.fetchOrder ||
|
|
1144
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1145
|
+
) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const requestStartedAt = this.context.now();
|
|
1150
|
+
try {
|
|
1151
|
+
const update = await adapter.fetchOrder(
|
|
1152
|
+
account.credentials ?? {},
|
|
1153
|
+
{
|
|
1154
|
+
symbol: claim.symbol,
|
|
1155
|
+
clientOrderId: claim.venueClientOrderId,
|
|
1156
|
+
},
|
|
1157
|
+
{ ...account.options, accountId: account.accountId },
|
|
1158
|
+
);
|
|
1159
|
+
if (
|
|
1160
|
+
!this.shouldContinueOrderBootstrap(record, generation, orderGeneration)
|
|
1161
|
+
) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (!update) {
|
|
1166
|
+
this.orderConsumer.onPrivateOrderClaimNotFound(
|
|
1167
|
+
record.accountId,
|
|
1168
|
+
record.venue,
|
|
1169
|
+
claim,
|
|
1062
1170
|
);
|
|
1063
1171
|
return;
|
|
1064
1172
|
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -143,6 +143,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
143
143
|
this.accountManager as PrivateAccountDataConsumer,
|
|
144
144
|
this.orderManager as PrivateOrderDataConsumer,
|
|
145
145
|
options.account,
|
|
146
|
+
options.order,
|
|
146
147
|
);
|
|
147
148
|
|
|
148
149
|
this.market = this.marketManager;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OrderDataStatus, Venue } from "../../types/index.ts";
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL = 500;
|
|
4
|
+
export const DEFAULT_MISSING_ORDER_EVICTION_THRESHOLD = 3;
|
|
4
5
|
|
|
5
6
|
export function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
6
7
|
return { ...status };
|
|
@@ -28,6 +29,14 @@ export function normalizeMaxClosedOrdersPerSymbol(
|
|
|
28
29
|
: DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export function normalizeMissingOrderEvictionThreshold(
|
|
33
|
+
value: number | undefined,
|
|
34
|
+
): number {
|
|
35
|
+
return value !== undefined && Number.isInteger(value) && value > 0
|
|
36
|
+
? value
|
|
37
|
+
: DEFAULT_MISSING_ORDER_EVICTION_THRESHOLD;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
export function successfulStatus(
|
|
32
41
|
status: OrderDataStatus,
|
|
33
42
|
options: {
|
|
@@ -15,6 +15,7 @@ export interface OrderRecord {
|
|
|
15
15
|
orderIdOnlyIndex: Map<string, Set<string>>;
|
|
16
16
|
clientOrderIdIndex: Map<string, Set<string>>;
|
|
17
17
|
pendingClientOrderIdIndex: Map<string, PendingOrderClaim>;
|
|
18
|
+
missingOrderConfirmations: Map<string, number>;
|
|
18
19
|
status: OrderDataStatus;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -29,8 +30,10 @@ export interface OrderLocation {
|
|
|
29
30
|
export interface PendingOrderClaim {
|
|
30
31
|
localOrderId: string;
|
|
31
32
|
symbol: string;
|
|
33
|
+
claimedAt: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface OrderManagerOptions {
|
|
35
37
|
maxClosedOrdersPerSymbol?: number;
|
|
38
|
+
missingOrderEvictionThreshold?: number;
|
|
36
39
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import type {
|
|
6
6
|
AccountAwareManager,
|
|
7
7
|
ClientContext,
|
|
8
|
+
ExpiredPendingOrderClaim,
|
|
8
9
|
HealthReporter,
|
|
9
10
|
ManagerLifecycle,
|
|
10
11
|
PrivateOrderDataConsumer,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
cloneOrderStatus,
|
|
44
45
|
createOrderDataStatus,
|
|
45
46
|
normalizeMaxClosedOrdersPerSymbol,
|
|
47
|
+
normalizeMissingOrderEvictionThreshold,
|
|
46
48
|
successfulStatus,
|
|
47
49
|
} from "./order/data-status.ts";
|
|
48
50
|
import {
|
|
@@ -61,6 +63,7 @@ import { createSnapshot, isOpenOrder } from "./order/snapshot.ts";
|
|
|
61
63
|
import {
|
|
62
64
|
getAllSnapshots,
|
|
63
65
|
getExistingSnapshot,
|
|
66
|
+
getExistingSnapshotLocation,
|
|
64
67
|
getLocalOrderIdForVenueOrderId,
|
|
65
68
|
getLocationByLocalOrderId,
|
|
66
69
|
getOpenOrderSnapshots,
|
|
@@ -96,6 +99,7 @@ export class OrderManagerImpl
|
|
|
96
99
|
|
|
97
100
|
private readonly context: ClientContext;
|
|
98
101
|
private readonly maxClosedOrdersPerSymbol: number;
|
|
102
|
+
private readonly missingOrderEvictionThreshold: number;
|
|
99
103
|
private readonly orderBus = new AsyncEventBus<OrderEvent>();
|
|
100
104
|
private readonly orderStatusBus =
|
|
101
105
|
new AsyncEventBus<OrderStatusChangedEvent>();
|
|
@@ -107,6 +111,9 @@ export class OrderManagerImpl
|
|
|
107
111
|
this.maxClosedOrdersPerSymbol = normalizeMaxClosedOrdersPerSymbol(
|
|
108
112
|
options.maxClosedOrdersPerSymbol,
|
|
109
113
|
);
|
|
114
|
+
this.missingOrderEvictionThreshold = normalizeMissingOrderEvictionThreshold(
|
|
115
|
+
options.missingOrderEvictionThreshold,
|
|
116
|
+
);
|
|
110
117
|
|
|
111
118
|
this.events = {
|
|
112
119
|
status: (filter) =>
|
|
@@ -494,6 +501,9 @@ export class OrderManagerImpl
|
|
|
494
501
|
openSetKeys.add(lookupKey);
|
|
495
502
|
}
|
|
496
503
|
const current = getExistingSnapshot(record, update);
|
|
504
|
+
if (current) {
|
|
505
|
+
this.clearMissingOrderConfirmationsForUpdate(record, current);
|
|
506
|
+
}
|
|
497
507
|
const nextSnapshot = this.applyUpdateToRecord(
|
|
498
508
|
record,
|
|
499
509
|
accountId,
|
|
@@ -565,6 +575,36 @@ export class OrderManagerImpl
|
|
|
565
575
|
return this.getOpenOrders(accountId);
|
|
566
576
|
}
|
|
567
577
|
|
|
578
|
+
getExpiredPrivateOrderClaims(
|
|
579
|
+
accountId: string,
|
|
580
|
+
now: number,
|
|
581
|
+
ttlMs: number,
|
|
582
|
+
): ExpiredPendingOrderClaim[] {
|
|
583
|
+
const record = this.records.get(accountId);
|
|
584
|
+
if (!record || ttlMs <= 0) {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const expired: ExpiredPendingOrderClaim[] = [];
|
|
589
|
+
for (const [
|
|
590
|
+
venueClientOrderId,
|
|
591
|
+
claim,
|
|
592
|
+
] of record.pendingClientOrderIdIndex) {
|
|
593
|
+
if (now - claim.claimedAt < ttlMs) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
expired.push({
|
|
598
|
+
venueClientOrderId,
|
|
599
|
+
localOrderId: claim.localOrderId,
|
|
600
|
+
symbol: claim.symbol,
|
|
601
|
+
claimedAt: claim.claimedAt,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return expired;
|
|
606
|
+
}
|
|
607
|
+
|
|
568
608
|
onPrivateOrderUpdate(
|
|
569
609
|
accountId: string,
|
|
570
610
|
venue: Venue,
|
|
@@ -590,32 +630,129 @@ export class OrderManagerImpl
|
|
|
590
630
|
return;
|
|
591
631
|
}
|
|
592
632
|
|
|
593
|
-
|
|
594
|
-
snapshot.status === "filled"
|
|
595
|
-
? "order.filled"
|
|
596
|
-
: snapshot.status === "rejected"
|
|
597
|
-
? "order.rejected"
|
|
598
|
-
: snapshot.status === "canceled" || snapshot.status === "expired"
|
|
599
|
-
? "order.canceled"
|
|
600
|
-
: "order.updated";
|
|
633
|
+
this.publishOrderEvent(accountId, venue, snapshot);
|
|
601
634
|
|
|
602
|
-
|
|
603
|
-
|
|
635
|
+
record.status = successfulStatus(record.status, {
|
|
636
|
+
preserveStatus: options.preserveStatus,
|
|
637
|
+
lastReceivedAt: snapshot.receivedAt,
|
|
638
|
+
lastReadyAt: snapshot.updatedAt,
|
|
639
|
+
});
|
|
640
|
+
this.publishStatus(record);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
onPrivateOrderConfirmedMissing(
|
|
644
|
+
accountId: string,
|
|
645
|
+
venue: Venue,
|
|
646
|
+
order: OrderSnapshot,
|
|
647
|
+
): void {
|
|
648
|
+
const record = this.getOrCreateRecord(accountId, venue);
|
|
649
|
+
if (!record.subscribed) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const location = getExistingSnapshotLocation(record, order);
|
|
654
|
+
if (!location || location.table !== "open") {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const current = getSnapshotAtLocation(record, location);
|
|
659
|
+
if (!current || !isOpenOrder(current)) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const confirmations =
|
|
664
|
+
(record.missingOrderConfirmations.get(location.localOrderId) ?? 0) + 1;
|
|
665
|
+
if (confirmations < this.missingOrderEvictionThreshold) {
|
|
666
|
+
record.missingOrderConfirmations.set(
|
|
667
|
+
location.localOrderId,
|
|
668
|
+
confirmations,
|
|
669
|
+
);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const receivedAt = this.context.now();
|
|
674
|
+
const snapshot = createSnapshot(
|
|
604
675
|
accountId,
|
|
605
676
|
venue,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
677
|
+
{
|
|
678
|
+
orderId: current.orderId,
|
|
679
|
+
clientOrderId: current.clientOrderId,
|
|
680
|
+
symbol: current.symbol,
|
|
681
|
+
side: current.side,
|
|
682
|
+
type: current.type,
|
|
683
|
+
status: "unknown",
|
|
684
|
+
price: current.price,
|
|
685
|
+
triggerPrice: current.triggerPrice,
|
|
686
|
+
amount: current.amount,
|
|
687
|
+
filled: current.filled,
|
|
688
|
+
remaining: current.remaining,
|
|
689
|
+
reduceOnly: current.reduceOnly,
|
|
690
|
+
positionSide: current.positionSide,
|
|
691
|
+
avgFillPrice: current.avgFillPrice,
|
|
692
|
+
receivedAt,
|
|
693
|
+
},
|
|
694
|
+
current,
|
|
695
|
+
);
|
|
610
696
|
|
|
697
|
+
if (
|
|
698
|
+
!this.writeSnapshot(record, location.localOrderId, snapshot, location)
|
|
699
|
+
) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
this.context.publishRuntimeError(
|
|
704
|
+
"order",
|
|
705
|
+
new Error(
|
|
706
|
+
`Evicted ${venue} open order after ${confirmations} confirmed missing checks`,
|
|
707
|
+
),
|
|
708
|
+
{
|
|
709
|
+
accountId,
|
|
710
|
+
venue,
|
|
711
|
+
symbol: current.symbol,
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
this.publishOrderEvent(accountId, venue, snapshot);
|
|
611
715
|
record.status = successfulStatus(record.status, {
|
|
612
|
-
preserveStatus: options.preserveStatus,
|
|
613
716
|
lastReceivedAt: snapshot.receivedAt,
|
|
614
717
|
lastReadyAt: snapshot.updatedAt,
|
|
615
718
|
});
|
|
616
719
|
this.publishStatus(record);
|
|
617
720
|
}
|
|
618
721
|
|
|
722
|
+
onPrivateOrderClaimNotFound(
|
|
723
|
+
accountId: string,
|
|
724
|
+
venue: Venue,
|
|
725
|
+
claim: ExpiredPendingOrderClaim,
|
|
726
|
+
): void {
|
|
727
|
+
const record = this.records.get(accountId);
|
|
728
|
+
if (!record) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const pending = record.pendingClientOrderIdIndex.get(
|
|
733
|
+
claim.venueClientOrderId,
|
|
734
|
+
);
|
|
735
|
+
if (
|
|
736
|
+
pending?.localOrderId !== claim.localOrderId ||
|
|
737
|
+
pending.symbol !== claim.symbol
|
|
738
|
+
) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
record.pendingClientOrderIdIndex.delete(claim.venueClientOrderId);
|
|
743
|
+
this.context.publishRuntimeError(
|
|
744
|
+
"order",
|
|
745
|
+
new Error(
|
|
746
|
+
`createOrder timed out and the order was not found on the venue: ${claim.venueClientOrderId}`,
|
|
747
|
+
),
|
|
748
|
+
{
|
|
749
|
+
accountId,
|
|
750
|
+
venue,
|
|
751
|
+
symbol: claim.symbol,
|
|
752
|
+
},
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
619
756
|
onPrivateOrderStreamState(
|
|
620
757
|
accountId: string,
|
|
621
758
|
venue: Venue,
|
|
@@ -670,6 +807,7 @@ export class OrderManagerImpl
|
|
|
670
807
|
orderIdOnlyIndex: new Map(),
|
|
671
808
|
clientOrderIdIndex: new Map(),
|
|
672
809
|
pendingClientOrderIdIndex: new Map(),
|
|
810
|
+
missingOrderConfirmations: new Map(),
|
|
673
811
|
status: createOrderDataStatus(accountId, venue, "inactive"),
|
|
674
812
|
};
|
|
675
813
|
|
|
@@ -718,9 +856,51 @@ export class OrderManagerImpl
|
|
|
718
856
|
|
|
719
857
|
this.warnSystemClientOrderIdOnlyClaim(record, snapshot);
|
|
720
858
|
this.warnProvisionalTerminalOrder(record, snapshot);
|
|
859
|
+
this.clearMissingOrderConfirmations(record, localOrderId);
|
|
721
860
|
return true;
|
|
722
861
|
}
|
|
723
862
|
|
|
863
|
+
private clearMissingOrderConfirmations(
|
|
864
|
+
record: OrderRecord,
|
|
865
|
+
localOrderId: string,
|
|
866
|
+
): void {
|
|
867
|
+
record.missingOrderConfirmations.delete(localOrderId);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private clearMissingOrderConfirmationsForUpdate(
|
|
871
|
+
record: OrderRecord,
|
|
872
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
873
|
+
): void {
|
|
874
|
+
const location = getExistingSnapshotLocation(record, update);
|
|
875
|
+
if (location) {
|
|
876
|
+
this.clearMissingOrderConfirmations(record, location.localOrderId);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private publishOrderEvent(
|
|
881
|
+
accountId: string,
|
|
882
|
+
venue: Venue,
|
|
883
|
+
snapshot: OrderSnapshot,
|
|
884
|
+
): void {
|
|
885
|
+
const eventType =
|
|
886
|
+
snapshot.status === "filled"
|
|
887
|
+
? "order.filled"
|
|
888
|
+
: snapshot.status === "rejected"
|
|
889
|
+
? "order.rejected"
|
|
890
|
+
: isOpenOrder(snapshot)
|
|
891
|
+
? "order.updated"
|
|
892
|
+
: "order.canceled";
|
|
893
|
+
|
|
894
|
+
this.orderBus.publish({
|
|
895
|
+
type: eventType,
|
|
896
|
+
accountId,
|
|
897
|
+
venue,
|
|
898
|
+
symbol: snapshot.symbol,
|
|
899
|
+
snapshot,
|
|
900
|
+
ts: this.context.now(),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
724
904
|
private warnDroppedUnkeyedTerminalOrder(
|
|
725
905
|
record: OrderRecord,
|
|
726
906
|
snapshot: OrderSnapshot,
|
|
@@ -800,6 +980,9 @@ export class OrderManagerImpl
|
|
|
800
980
|
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
801
981
|
): OrderSnapshot | undefined {
|
|
802
982
|
const resolution = this.resolveLocalOrderIdForUpdate(record, update);
|
|
983
|
+
if (resolution.localOrderId) {
|
|
984
|
+
this.clearMissingOrderConfirmations(record, resolution.localOrderId);
|
|
985
|
+
}
|
|
803
986
|
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
804
987
|
const previousLocation = getLocationByLocalOrderId(record, localOrderId);
|
|
805
988
|
const previous = previousLocation
|
|
@@ -874,6 +1057,7 @@ export class OrderManagerImpl
|
|
|
874
1057
|
record.pendingClientOrderIdIndex.set(venueClientOrderId, {
|
|
875
1058
|
localOrderId,
|
|
876
1059
|
symbol,
|
|
1060
|
+
claimedAt: this.context.now(),
|
|
877
1061
|
});
|
|
878
1062
|
}
|
|
879
1063
|
|
|
@@ -955,6 +1139,9 @@ export class OrderManagerImpl
|
|
|
955
1139
|
update,
|
|
956
1140
|
options.localOrderId,
|
|
957
1141
|
);
|
|
1142
|
+
if (resolution.localOrderId) {
|
|
1143
|
+
this.clearMissingOrderConfirmations(record, resolution.localOrderId);
|
|
1144
|
+
}
|
|
958
1145
|
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
959
1146
|
const previousLocation = getLocationByLocalOrderId(record, localOrderId);
|
|
960
1147
|
const previous = previousLocation
|
package/src/types/order.ts
CHANGED
package/src/types/shared.ts
CHANGED
|
@@ -107,6 +107,8 @@ export interface AccountRuntimeOptions {
|
|
|
107
107
|
|
|
108
108
|
export interface OrderRuntimeOptions {
|
|
109
109
|
maxClosedOrdersPerSymbol?: number;
|
|
110
|
+
missingOrderEvictionThreshold?: number;
|
|
111
|
+
pendingClaimTtlMs?: number;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
export interface CreateClientOptions {
|