@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/docs/api.md +477 -1002
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +83 -12
- package/src/adapters/binance/private-adapter.ts +302 -59
- package/src/adapters/binance/rate-limit.ts +47 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/juplend/private-adapter.ts +97 -68
- package/src/adapters/types.ts +25 -1
- package/src/client/context.ts +26 -9
- package/src/client/private-subscription-coordinator.ts +898 -63
- package/src/client/runtime.ts +49 -11
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +156 -2
- package/src/index.ts +8 -1
- package/src/internal/http-client.ts +608 -0
- package/src/internal/rate-limiter.ts +181 -0
- package/src/internal/watermark.ts +83 -0
- package/src/managers/account-manager.ts +227 -23
- package/src/managers/market-manager.ts +224 -34
- package/src/managers/order-manager.ts +991 -84
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +25 -0
- package/src/types/order.ts +1 -0
- package/src/types/shared.ts +66 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import BigNumber from "bignumber.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RawOpenOrdersSnapshot,
|
|
4
|
+
RawOrderUpdate,
|
|
5
|
+
} from "../adapters/types.ts";
|
|
3
6
|
import type {
|
|
4
7
|
AccountAwareManager,
|
|
5
8
|
ClientContext,
|
|
@@ -8,10 +11,19 @@ import type {
|
|
|
8
11
|
PrivateOrderDataConsumer,
|
|
9
12
|
PrivateSubscriptionState,
|
|
10
13
|
} from "../client/context.ts";
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
AcexError,
|
|
16
|
+
buildAcexErrorDetails,
|
|
17
|
+
formatAcexErrorMessage,
|
|
18
|
+
} from "../errors.ts";
|
|
12
19
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
13
20
|
import { toCanonical } from "../internal/decimal.ts";
|
|
14
21
|
import { matchesOrderFilter } from "../internal/filters.ts";
|
|
22
|
+
import { isTransportError } from "../internal/http-client.ts";
|
|
23
|
+
import {
|
|
24
|
+
canDeleteMissingFromSnapshot,
|
|
25
|
+
shouldApplyWatermarkedUpdate,
|
|
26
|
+
} from "../internal/watermark.ts";
|
|
15
27
|
import type {
|
|
16
28
|
CancelAllOrdersInput,
|
|
17
29
|
CancelOrderInput,
|
|
@@ -33,27 +45,164 @@ interface OrderRecord {
|
|
|
33
45
|
accountId: string;
|
|
34
46
|
venue: Venue;
|
|
35
47
|
subscribed: boolean;
|
|
36
|
-
|
|
48
|
+
openOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
49
|
+
closedOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
50
|
+
localOrderLocations: Map<string, OrderLocation>;
|
|
51
|
+
orderIdIndex: Map<string, Map<string, string>>;
|
|
52
|
+
orderIdOnlyIndex: Map<string, Set<string>>;
|
|
53
|
+
clientOrderIdIndex: Map<string, Set<string>>;
|
|
54
|
+
pendingClientOrderIdIndex: Map<string, PendingOrderClaim>;
|
|
37
55
|
status: OrderDataStatus;
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
type OrderTable = "open" | "closed";
|
|
59
|
+
|
|
60
|
+
interface OrderLocation {
|
|
61
|
+
table: OrderTable;
|
|
62
|
+
symbol: string;
|
|
63
|
+
localOrderId: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface PendingOrderClaim {
|
|
67
|
+
localOrderId: string;
|
|
68
|
+
symbol: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface OrderManagerOptions {
|
|
72
|
+
maxClosedOrdersPerSymbol?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL = 500;
|
|
76
|
+
const SDK_CLIENT_ORDER_ID_PREFIX = "acex-";
|
|
77
|
+
const VENUE_CLIENT_ORDER_ID_PATTERN = /^[.A-Z:/a-z0-9_-]{1,32}$/;
|
|
78
|
+
|
|
79
|
+
const SYSTEM_CLIENT_ORDER_ID_PATTERNS = [
|
|
80
|
+
/^adl_autoclose$/,
|
|
81
|
+
/^autoclose-/,
|
|
82
|
+
/^settlement_autoclose-/,
|
|
83
|
+
];
|
|
84
|
+
|
|
40
85
|
function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
41
86
|
return { ...status };
|
|
42
87
|
}
|
|
43
88
|
|
|
44
|
-
function
|
|
89
|
+
function normalizeMaxClosedOrdersPerSymbol(value: number | undefined): number {
|
|
90
|
+
return value !== undefined && Number.isInteger(value) && value > 0
|
|
91
|
+
? value
|
|
92
|
+
: DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getOrderLookupKeys(input: {
|
|
96
|
+
symbol: string;
|
|
45
97
|
orderId?: string;
|
|
46
98
|
clientOrderId?: string;
|
|
47
|
-
}): string
|
|
99
|
+
}): string[] {
|
|
100
|
+
const keys: string[] = [];
|
|
48
101
|
if (input.orderId) {
|
|
49
|
-
|
|
102
|
+
keys.push(`symbol:${input.symbol}:order:${input.orderId}`);
|
|
50
103
|
}
|
|
51
104
|
|
|
52
105
|
if (input.clientOrderId) {
|
|
53
|
-
|
|
106
|
+
keys.push(`symbol:${input.symbol}:client:${input.clientOrderId}`);
|
|
54
107
|
}
|
|
55
108
|
|
|
56
|
-
return
|
|
109
|
+
return keys;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function shouldMatchOrderQuery(
|
|
113
|
+
candidate: OrderSnapshot,
|
|
114
|
+
input: { symbol?: string; orderId?: string; clientOrderId?: string },
|
|
115
|
+
): boolean {
|
|
116
|
+
if (input.symbol && candidate.symbol !== input.symbol) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input.orderId && candidate.orderId !== input.orderId) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (input.clientOrderId && candidate.clientOrderId !== input.clientOrderId) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Boolean(input.orderId || input.clientOrderId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function shouldMatchStoredOrderIdentity(
|
|
132
|
+
candidate: OrderSnapshot,
|
|
133
|
+
input: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
134
|
+
): boolean {
|
|
135
|
+
if (candidate.symbol !== input.symbol) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (candidate.orderId && input.orderId) {
|
|
140
|
+
return candidate.orderId === input.orderId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// clientOrderId is only a temporary identity for an order that does not yet
|
|
144
|
+
// have an orderId. A candidate that already carries an orderId (including an
|
|
145
|
+
// old order sitting in closed that reused this clientOrderId) must not be
|
|
146
|
+
// merged by a cid-only update; otherwise the stale orderId would be
|
|
147
|
+
// carried forward and pollute closed. When the orderId is later filled in,
|
|
148
|
+
// the candidate still lacks an orderId and matches normally.
|
|
149
|
+
return Boolean(
|
|
150
|
+
input.clientOrderId &&
|
|
151
|
+
candidate.clientOrderId === input.clientOrderId &&
|
|
152
|
+
!candidate.orderId,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function successfulStatus(
|
|
157
|
+
status: OrderDataStatus,
|
|
158
|
+
options: {
|
|
159
|
+
ready?: boolean;
|
|
160
|
+
lastReceivedAt?: number;
|
|
161
|
+
lastReadyAt?: number;
|
|
162
|
+
preserveStatus?: boolean;
|
|
163
|
+
},
|
|
164
|
+
): OrderDataStatus {
|
|
165
|
+
const preservesStreamState =
|
|
166
|
+
options.preserveStatus &&
|
|
167
|
+
(status.runtimeStatus === "reconnecting" ||
|
|
168
|
+
status.reason === "ws_disconnected" ||
|
|
169
|
+
status.reason === "heartbeat_timeout");
|
|
170
|
+
const ready = options.ready ?? true;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
...status,
|
|
174
|
+
activity: "active",
|
|
175
|
+
ready,
|
|
176
|
+
runtimeStatus: preservesStreamState ? status.runtimeStatus : "healthy",
|
|
177
|
+
reason: preservesStreamState ? status.reason : undefined,
|
|
178
|
+
lastReceivedAt: options.lastReceivedAt ?? status.lastReceivedAt,
|
|
179
|
+
lastReadyAt: ready
|
|
180
|
+
? (options.lastReadyAt ??
|
|
181
|
+
(options.preserveStatus ? status.lastReadyAt : undefined) ??
|
|
182
|
+
Date.now())
|
|
183
|
+
: status.lastReadyAt,
|
|
184
|
+
inactiveSince: undefined,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isOpenOrder(snapshot: OrderSnapshot): boolean {
|
|
189
|
+
return snapshot.status === "open" || snapshot.status === "partially_filled";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function orderPriority(status: OrderSnapshot["status"]): number {
|
|
193
|
+
switch (status) {
|
|
194
|
+
case "filled":
|
|
195
|
+
return 5;
|
|
196
|
+
case "canceled":
|
|
197
|
+
case "expired":
|
|
198
|
+
return 4;
|
|
199
|
+
case "rejected":
|
|
200
|
+
return 3;
|
|
201
|
+
case "partially_filled":
|
|
202
|
+
return 2;
|
|
203
|
+
case "open":
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
57
206
|
}
|
|
58
207
|
|
|
59
208
|
export class OrderManagerImpl
|
|
@@ -67,13 +216,18 @@ export class OrderManagerImpl
|
|
|
67
216
|
readonly events: OrderEventStreams;
|
|
68
217
|
|
|
69
218
|
private readonly context: ClientContext;
|
|
219
|
+
private readonly maxClosedOrdersPerSymbol: number;
|
|
70
220
|
private readonly orderBus = new AsyncEventBus<OrderEvent>();
|
|
71
221
|
private readonly orderStatusBus =
|
|
72
222
|
new AsyncEventBus<OrderStatusChangedEvent>();
|
|
73
223
|
private readonly records = new Map<string, OrderRecord>();
|
|
224
|
+
private localOrderSequence = 0;
|
|
74
225
|
|
|
75
|
-
constructor(context: ClientContext) {
|
|
226
|
+
constructor(context: ClientContext, options: OrderManagerOptions = {}) {
|
|
76
227
|
this.context = context;
|
|
228
|
+
this.maxClosedOrdersPerSymbol = normalizeMaxClosedOrdersPerSymbol(
|
|
229
|
+
options.maxClosedOrdersPerSymbol,
|
|
230
|
+
);
|
|
77
231
|
|
|
78
232
|
this.events = {
|
|
79
233
|
status: (filter) =>
|
|
@@ -102,7 +256,10 @@ export class OrderManagerImpl
|
|
|
102
256
|
async subscribeOrders(input: SubscribeOrdersInput): Promise<void> {
|
|
103
257
|
this.context.assertStarted();
|
|
104
258
|
const account = this.context.getRegisteredAccount(input.accountId);
|
|
105
|
-
if (
|
|
259
|
+
if (
|
|
260
|
+
this.context.getPrivateOrderCapabilities(account.venue)?.updates ===
|
|
261
|
+
"unsupported"
|
|
262
|
+
) {
|
|
106
263
|
throw this.createError(
|
|
107
264
|
"VENUE_NOT_SUPPORTED",
|
|
108
265
|
`Venue does not support private order subscriptions: ${account.venue}`,
|
|
@@ -145,11 +302,45 @@ export class OrderManagerImpl
|
|
|
145
302
|
const account = this.context.getRegisteredAccount(input.accountId);
|
|
146
303
|
this.context.ensurePrivateCredentials(input.accountId);
|
|
147
304
|
this.validateCreateOrderInput(input, account.venue);
|
|
305
|
+
const record = this.getOrCreateRecord(input.accountId, account.venue);
|
|
306
|
+
const localOrderId = this.generateLocalOrderId({
|
|
307
|
+
record,
|
|
308
|
+
avoidOpenClientOrderId: input.clientOrderId === undefined,
|
|
309
|
+
});
|
|
310
|
+
const venueClientOrderId = input.clientOrderId ?? localOrderId;
|
|
311
|
+
this.addPendingClientOrderClaim(
|
|
312
|
+
record,
|
|
313
|
+
input.symbol,
|
|
314
|
+
venueClientOrderId,
|
|
315
|
+
localOrderId,
|
|
316
|
+
);
|
|
148
317
|
|
|
149
318
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
319
|
+
const commandInput: CreateOrderInput = {
|
|
320
|
+
...input,
|
|
321
|
+
clientOrderId: venueClientOrderId,
|
|
322
|
+
};
|
|
323
|
+
const update = await this.context.createOrder(commandInput);
|
|
324
|
+
const snapshot = this.applyCommandUpdate(
|
|
325
|
+
input.accountId,
|
|
326
|
+
account.venue,
|
|
327
|
+
update,
|
|
328
|
+
{ localOrderId },
|
|
329
|
+
);
|
|
330
|
+
this.clearPendingClientOrderClaim(
|
|
331
|
+
record,
|
|
332
|
+
venueClientOrderId,
|
|
333
|
+
localOrderId,
|
|
334
|
+
);
|
|
335
|
+
return snapshot;
|
|
152
336
|
} catch (error) {
|
|
337
|
+
if (!this.shouldRetainPendingClaimAfterCreateError(error)) {
|
|
338
|
+
this.clearPendingClientOrderClaim(
|
|
339
|
+
record,
|
|
340
|
+
venueClientOrderId,
|
|
341
|
+
localOrderId,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
153
344
|
throw this.wrapCommandError(
|
|
154
345
|
"ORDER_CREATE_FAILED",
|
|
155
346
|
`Failed to create order for ${input.accountId}: ${input.symbol}`,
|
|
@@ -218,17 +409,43 @@ export class OrderManagerImpl
|
|
|
218
409
|
return undefined;
|
|
219
410
|
}
|
|
220
411
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
412
|
+
if (input.symbol && input.orderId) {
|
|
413
|
+
const localOrderId = this.getLocalOrderIdForVenueOrderId(
|
|
414
|
+
record,
|
|
415
|
+
input.symbol,
|
|
416
|
+
input.orderId,
|
|
417
|
+
);
|
|
418
|
+
const snapshot = localOrderId
|
|
419
|
+
? this.getSnapshotByLocalOrderId(record, localOrderId)
|
|
420
|
+
: undefined;
|
|
421
|
+
if (!snapshot) {
|
|
422
|
+
return undefined;
|
|
224
423
|
}
|
|
225
424
|
|
|
226
425
|
if (
|
|
227
426
|
input.clientOrderId &&
|
|
228
|
-
snapshot.clientOrderId
|
|
427
|
+
snapshot.clientOrderId !== input.clientOrderId
|
|
229
428
|
) {
|
|
230
|
-
return
|
|
429
|
+
return undefined;
|
|
231
430
|
}
|
|
431
|
+
|
|
432
|
+
return snapshot;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (input.orderId) {
|
|
436
|
+
return this.selectLatestSnapshot(
|
|
437
|
+
this.getSnapshotsForOrderId(record, input.orderId).filter((snapshot) =>
|
|
438
|
+
shouldMatchOrderQuery(snapshot, input),
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (input.clientOrderId) {
|
|
444
|
+
return this.selectLatestSnapshot(
|
|
445
|
+
this.getSnapshotsForClientOrderId(record, input.clientOrderId).filter(
|
|
446
|
+
(snapshot) => shouldMatchOrderQuery(snapshot, input),
|
|
447
|
+
),
|
|
448
|
+
);
|
|
232
449
|
}
|
|
233
450
|
|
|
234
451
|
return undefined;
|
|
@@ -240,15 +457,11 @@ export class OrderManagerImpl
|
|
|
240
457
|
return [];
|
|
241
458
|
}
|
|
242
459
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
460
|
+
if (symbol) {
|
|
461
|
+
return [...(record.openOrders.get(symbol)?.values() ?? [])];
|
|
462
|
+
}
|
|
247
463
|
|
|
248
|
-
|
|
249
|
-
snapshot.status === "open" || snapshot.status === "partially_filled"
|
|
250
|
-
);
|
|
251
|
-
});
|
|
464
|
+
return this.getOpenOrderSnapshots(record);
|
|
252
465
|
}
|
|
253
466
|
|
|
254
467
|
getOrderStatus(accountId: string): OrderDataStatus | undefined {
|
|
@@ -316,7 +529,7 @@ export class OrderManagerImpl
|
|
|
316
529
|
|
|
317
530
|
record.status = {
|
|
318
531
|
...this.createStatus(accountId, venue, "active"),
|
|
319
|
-
ready: record
|
|
532
|
+
ready: this.getSnapshotCount(record) > 0,
|
|
320
533
|
runtimeStatus: "bootstrap_pending",
|
|
321
534
|
reason: undefined,
|
|
322
535
|
lastReceivedAt: record.status.lastReceivedAt,
|
|
@@ -329,40 +542,82 @@ export class OrderManagerImpl
|
|
|
329
542
|
onPrivateOrderBootstrap(
|
|
330
543
|
accountId: string,
|
|
331
544
|
venue: Venue,
|
|
332
|
-
|
|
333
|
-
|
|
545
|
+
snapshot: RawOpenOrdersSnapshot,
|
|
546
|
+
options: { requestStartedAt: number; preserveStatus?: boolean },
|
|
547
|
+
): OrderSnapshot[] {
|
|
548
|
+
return this.onPrivateOrderReconcile(accountId, venue, snapshot, options);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
onPrivateOrderReconcile(
|
|
552
|
+
accountId: string,
|
|
553
|
+
venue: Venue,
|
|
554
|
+
snapshot: RawOpenOrdersSnapshot,
|
|
555
|
+
options: { requestStartedAt: number; preserveStatus?: boolean },
|
|
556
|
+
): OrderSnapshot[] {
|
|
334
557
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
335
558
|
if (!record.subscribed) {
|
|
336
|
-
return;
|
|
559
|
+
return [];
|
|
337
560
|
}
|
|
338
561
|
|
|
339
|
-
const
|
|
340
|
-
for (const update of
|
|
341
|
-
const
|
|
562
|
+
const openSetKeys = new Set<string>();
|
|
563
|
+
for (const update of snapshot.orders) {
|
|
564
|
+
for (const lookupKey of getOrderLookupKeys(update)) {
|
|
565
|
+
openSetKeys.add(lookupKey);
|
|
566
|
+
}
|
|
567
|
+
const current = this.getExistingSnapshot(record, update);
|
|
568
|
+
const nextSnapshot = this.applyUpdateToRecord(
|
|
569
|
+
record,
|
|
342
570
|
accountId,
|
|
343
571
|
venue,
|
|
344
572
|
update,
|
|
345
|
-
|
|
573
|
+
{
|
|
574
|
+
requestStartedAt: options.requestStartedAt,
|
|
575
|
+
preserveStatus: true,
|
|
576
|
+
},
|
|
346
577
|
);
|
|
347
|
-
|
|
578
|
+
if (nextSnapshot) {
|
|
579
|
+
for (const nextLookupKey of getOrderLookupKeys(nextSnapshot)) {
|
|
580
|
+
openSetKeys.add(nextLookupKey);
|
|
581
|
+
}
|
|
582
|
+
} else if (current) {
|
|
583
|
+
for (const currentLookupKey of getOrderLookupKeys(current)) {
|
|
584
|
+
openSetKeys.add(currentLookupKey);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
348
587
|
}
|
|
349
588
|
|
|
350
|
-
record.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
589
|
+
const disappeared = this.getOpenOrderSnapshots(record).filter((order) => {
|
|
590
|
+
if (!isOpenOrder(order)) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const lookupKeys = getOrderLookupKeys(order);
|
|
595
|
+
if (
|
|
596
|
+
lookupKeys.length === 0 ||
|
|
597
|
+
lookupKeys.some((lookupKey) => openSetKeys.has(lookupKey))
|
|
598
|
+
) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return canDeleteMissingFromSnapshot(order, {
|
|
603
|
+
requestStartedAt: options.requestStartedAt,
|
|
604
|
+
snapshotExchangeTs: snapshot.snapshotExchangeTs,
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const orderedSnapshots = this.getAllSnapshots(record);
|
|
609
|
+
const latestTs = Math.max(
|
|
610
|
+
snapshot.snapshotReceivedAt,
|
|
611
|
+
orderedSnapshots.reduce(
|
|
612
|
+
(max, order) => Math.max(max, order.updatedAt),
|
|
613
|
+
0,
|
|
614
|
+
),
|
|
355
615
|
);
|
|
356
|
-
record.status = {
|
|
357
|
-
|
|
358
|
-
activity: "active",
|
|
359
|
-
ready: true,
|
|
360
|
-
runtimeStatus: "healthy",
|
|
361
|
-
reason: undefined,
|
|
616
|
+
record.status = successfulStatus(record.status, {
|
|
617
|
+
preserveStatus: options.preserveStatus,
|
|
362
618
|
lastReceivedAt: latestTs || record.status.lastReceivedAt,
|
|
363
619
|
lastReadyAt: latestTs || this.context.now(),
|
|
364
|
-
|
|
365
|
-
};
|
|
620
|
+
});
|
|
366
621
|
|
|
367
622
|
const event: OrderSnapshotReplacedEvent = {
|
|
368
623
|
type: "order.snapshot_replaced",
|
|
@@ -374,21 +629,37 @@ export class OrderManagerImpl
|
|
|
374
629
|
|
|
375
630
|
this.orderBus.publish(event);
|
|
376
631
|
this.publishStatus(record);
|
|
632
|
+
return disappeared;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
getPrivateOpenOrders(accountId: string): OrderSnapshot[] {
|
|
636
|
+
return this.getOpenOrders(accountId);
|
|
377
637
|
}
|
|
378
638
|
|
|
379
639
|
onPrivateOrderUpdate(
|
|
380
640
|
accountId: string,
|
|
381
641
|
venue: Venue,
|
|
382
642
|
update: RawOrderUpdate,
|
|
643
|
+
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
383
644
|
): void {
|
|
384
645
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
385
646
|
if (!record.subscribed) {
|
|
386
647
|
return;
|
|
387
648
|
}
|
|
388
649
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
650
|
+
const snapshot = this.applyUpdateToRecord(
|
|
651
|
+
record,
|
|
652
|
+
accountId,
|
|
653
|
+
venue,
|
|
654
|
+
update,
|
|
655
|
+
{
|
|
656
|
+
requestStartedAt: options.requestStartedAt,
|
|
657
|
+
preserveStatus: options.preserveStatus,
|
|
658
|
+
},
|
|
659
|
+
);
|
|
660
|
+
if (!snapshot) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
392
663
|
|
|
393
664
|
const eventType =
|
|
394
665
|
snapshot.status === "filled"
|
|
@@ -408,16 +679,11 @@ export class OrderManagerImpl
|
|
|
408
679
|
ts: this.context.now(),
|
|
409
680
|
});
|
|
410
681
|
|
|
411
|
-
record.status = {
|
|
412
|
-
|
|
413
|
-
activity: "active",
|
|
414
|
-
ready: true,
|
|
415
|
-
runtimeStatus: "healthy",
|
|
416
|
-
reason: undefined,
|
|
682
|
+
record.status = successfulStatus(record.status, {
|
|
683
|
+
preserveStatus: options.preserveStatus,
|
|
417
684
|
lastReceivedAt: snapshot.receivedAt,
|
|
418
685
|
lastReadyAt: snapshot.updatedAt,
|
|
419
|
-
|
|
420
|
-
};
|
|
686
|
+
});
|
|
421
687
|
this.publishStatus(record);
|
|
422
688
|
}
|
|
423
689
|
|
|
@@ -468,7 +734,13 @@ export class OrderManagerImpl
|
|
|
468
734
|
accountId,
|
|
469
735
|
venue,
|
|
470
736
|
subscribed: false,
|
|
471
|
-
|
|
737
|
+
openOrders: new Map(),
|
|
738
|
+
closedOrders: new Map(),
|
|
739
|
+
localOrderLocations: new Map(),
|
|
740
|
+
orderIdIndex: new Map(),
|
|
741
|
+
orderIdOnlyIndex: new Map(),
|
|
742
|
+
clientOrderIdIndex: new Map(),
|
|
743
|
+
pendingClientOrderIdIndex: new Map(),
|
|
472
744
|
status: this.createStatus(accountId, venue, "inactive"),
|
|
473
745
|
};
|
|
474
746
|
|
|
@@ -492,38 +764,545 @@ export class OrderManagerImpl
|
|
|
492
764
|
|
|
493
765
|
private getExistingSnapshot(
|
|
494
766
|
record: OrderRecord,
|
|
495
|
-
update: { orderId?: string; clientOrderId?: string },
|
|
767
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
496
768
|
): OrderSnapshot | undefined {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
769
|
+
const location = this.getExistingSnapshotLocation(record, update);
|
|
770
|
+
return location ? this.getSnapshotAtLocation(record, location) : undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private getExistingSnapshotLocation(
|
|
774
|
+
record: OrderRecord,
|
|
775
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
776
|
+
): OrderLocation | undefined {
|
|
777
|
+
const resolution = this.resolveLocalOrderIdForUpdate(record, update);
|
|
778
|
+
return resolution.localOrderId
|
|
779
|
+
? record.localOrderLocations.get(resolution.localOrderId)
|
|
780
|
+
: undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private resolveLocalOrderIdForUpdate(
|
|
784
|
+
record: OrderRecord,
|
|
785
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
786
|
+
preferredLocalOrderId?: string,
|
|
787
|
+
): {
|
|
788
|
+
localOrderId?: string;
|
|
789
|
+
source?: "exact" | "pending" | "provisional" | "preferred";
|
|
790
|
+
} {
|
|
791
|
+
if (update.orderId) {
|
|
792
|
+
const exact = this.getLocalOrderIdForVenueOrderId(
|
|
793
|
+
record,
|
|
794
|
+
update.symbol,
|
|
795
|
+
update.orderId,
|
|
796
|
+
);
|
|
797
|
+
if (exact) {
|
|
798
|
+
return { localOrderId: exact, source: "exact" };
|
|
500
799
|
}
|
|
800
|
+
}
|
|
501
801
|
|
|
802
|
+
if (preferredLocalOrderId) {
|
|
803
|
+
return { localOrderId: preferredLocalOrderId, source: "preferred" };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (update.clientOrderId) {
|
|
807
|
+
const pending = record.pendingClientOrderIdIndex.get(
|
|
808
|
+
update.clientOrderId,
|
|
809
|
+
);
|
|
810
|
+
if (pending?.symbol === update.symbol) {
|
|
811
|
+
return { localOrderId: pending.localOrderId, source: "pending" };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (
|
|
816
|
+
update.clientOrderId &&
|
|
817
|
+
!this.isSystemClientOrderId(update.clientOrderId)
|
|
818
|
+
) {
|
|
819
|
+
for (const localOrderId of record.clientOrderIdIndex.get(
|
|
820
|
+
update.clientOrderId,
|
|
821
|
+
) ?? []) {
|
|
822
|
+
const snapshot = this.getSnapshotByLocalOrderId(record, localOrderId);
|
|
823
|
+
if (snapshot && shouldMatchStoredOrderIdentity(snapshot, update)) {
|
|
824
|
+
return { localOrderId, source: "provisional" };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return {};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private setSnapshot(
|
|
833
|
+
record: OrderRecord,
|
|
834
|
+
localOrderId: string,
|
|
835
|
+
snapshot: OrderSnapshot,
|
|
836
|
+
previousLocation?: OrderLocation,
|
|
837
|
+
): OrderLocation | undefined {
|
|
838
|
+
if (!snapshot.orderId && !snapshot.clientOrderId) {
|
|
839
|
+
this.warnDroppedUnkeyedTerminalOrder(record, snapshot);
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const currentLocation =
|
|
844
|
+
previousLocation ?? record.localOrderLocations.get(localOrderId);
|
|
845
|
+
if (currentLocation) {
|
|
846
|
+
return this.moveSnapshot(record, currentLocation, localOrderId, snapshot);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return this.insertSnapshot(record, localOrderId, snapshot);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private insertSnapshot(
|
|
853
|
+
record: OrderRecord,
|
|
854
|
+
localOrderId: string,
|
|
855
|
+
snapshot: OrderSnapshot,
|
|
856
|
+
): OrderLocation | undefined {
|
|
857
|
+
const existingLocation = record.localOrderLocations.get(localOrderId);
|
|
858
|
+
if (existingLocation) {
|
|
859
|
+
this.deleteSnapshot(record, existingLocation);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const location: OrderLocation = {
|
|
863
|
+
table: isOpenOrder(snapshot) ? "open" : "closed",
|
|
864
|
+
symbol: snapshot.symbol,
|
|
865
|
+
localOrderId,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const table = this.getOrderTable(record, location.table);
|
|
869
|
+
const symbolOrders = this.getOrCreateSymbolOrders(table, location.symbol);
|
|
870
|
+
symbolOrders.set(localOrderId, snapshot);
|
|
871
|
+
record.localOrderLocations.set(localOrderId, location);
|
|
872
|
+
|
|
873
|
+
if (snapshot.orderId) {
|
|
874
|
+
const symbolIndex = this.getOrCreateOrderIdSymbolIndex(
|
|
875
|
+
record,
|
|
876
|
+
snapshot.symbol,
|
|
877
|
+
);
|
|
878
|
+
symbolIndex.set(snapshot.orderId, localOrderId);
|
|
879
|
+
this.addLocalOrderIdToSetIndex(
|
|
880
|
+
record.orderIdOnlyIndex,
|
|
881
|
+
snapshot.orderId,
|
|
882
|
+
localOrderId,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (snapshot.clientOrderId) {
|
|
887
|
+
this.addLocalOrderIdToSetIndex(
|
|
888
|
+
record.clientOrderIdIndex,
|
|
889
|
+
snapshot.clientOrderId,
|
|
890
|
+
localOrderId,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
this.trimClosedOrdersForSymbol(record, location);
|
|
895
|
+
this.warnSystemClientOrderIdOnlyClaim(record, snapshot);
|
|
896
|
+
this.warnProvisionalTerminalOrder(record, snapshot);
|
|
897
|
+
return location;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private deleteSnapshot(
|
|
901
|
+
record: OrderRecord,
|
|
902
|
+
location: OrderLocation,
|
|
903
|
+
): OrderSnapshot | undefined {
|
|
904
|
+
const snapshot = this.getSnapshotAtLocation(record, location);
|
|
905
|
+
if (!snapshot) {
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const table = this.getOrderTable(record, location.table);
|
|
910
|
+
const symbolOrders = table.get(location.symbol);
|
|
911
|
+
symbolOrders?.delete(location.localOrderId);
|
|
912
|
+
if (symbolOrders?.size === 0) {
|
|
913
|
+
table.delete(location.symbol);
|
|
914
|
+
}
|
|
915
|
+
record.localOrderLocations.delete(location.localOrderId);
|
|
916
|
+
|
|
917
|
+
if (snapshot.orderId) {
|
|
918
|
+
const symbolIndex = record.orderIdIndex.get(location.symbol);
|
|
502
919
|
if (
|
|
503
|
-
|
|
504
|
-
snapshot.
|
|
920
|
+
symbolIndex?.get(snapshot.orderId) &&
|
|
921
|
+
symbolIndex.get(snapshot.orderId) === location.localOrderId
|
|
505
922
|
) {
|
|
506
|
-
|
|
923
|
+
symbolIndex.delete(snapshot.orderId);
|
|
924
|
+
}
|
|
925
|
+
if (symbolIndex?.size === 0) {
|
|
926
|
+
record.orderIdIndex.delete(location.symbol);
|
|
507
927
|
}
|
|
928
|
+
this.removeLocalOrderIdFromSetIndex(
|
|
929
|
+
record.orderIdOnlyIndex,
|
|
930
|
+
snapshot.orderId,
|
|
931
|
+
location.localOrderId,
|
|
932
|
+
);
|
|
508
933
|
}
|
|
509
934
|
|
|
510
|
-
|
|
935
|
+
if (snapshot.clientOrderId) {
|
|
936
|
+
this.removeLocalOrderIdFromSetIndex(
|
|
937
|
+
record.clientOrderIdIndex,
|
|
938
|
+
snapshot.clientOrderId,
|
|
939
|
+
location.localOrderId,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return snapshot;
|
|
511
944
|
}
|
|
512
945
|
|
|
513
|
-
private
|
|
514
|
-
|
|
946
|
+
private moveSnapshot(
|
|
947
|
+
record: OrderRecord,
|
|
948
|
+
previousLocation: OrderLocation,
|
|
949
|
+
localOrderId: string,
|
|
515
950
|
snapshot: OrderSnapshot,
|
|
951
|
+
): OrderLocation | undefined {
|
|
952
|
+
this.deleteSnapshot(record, previousLocation);
|
|
953
|
+
return this.insertSnapshot(record, localOrderId, snapshot);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private trimClosedOrdersForSymbol(
|
|
957
|
+
record: OrderRecord,
|
|
958
|
+
location: OrderLocation,
|
|
516
959
|
): void {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
960
|
+
if (location.table !== "closed") {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
let symbolOrders = record.closedOrders.get(location.symbol);
|
|
965
|
+
if (!symbolOrders || symbolOrders.size <= this.maxClosedOrdersPerSymbol) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const trimBatchSize = Math.max(
|
|
970
|
+
1,
|
|
971
|
+
Math.floor(this.maxClosedOrdersPerSymbol / 10),
|
|
972
|
+
);
|
|
973
|
+
while (symbolOrders && symbolOrders.size > this.maxClosedOrdersPerSymbol) {
|
|
974
|
+
const keys = symbolOrders.keys();
|
|
975
|
+
for (let deleted = 0; deleted < trimBatchSize; deleted += 1) {
|
|
976
|
+
const next = keys.next();
|
|
977
|
+
if (next.done) {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
this.deleteSnapshot(record, {
|
|
981
|
+
table: "closed",
|
|
982
|
+
symbol: location.symbol,
|
|
983
|
+
localOrderId: next.value,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
symbolOrders = record.closedOrders.get(location.symbol);
|
|
524
987
|
}
|
|
525
988
|
}
|
|
526
989
|
|
|
990
|
+
private warnDroppedUnkeyedTerminalOrder(
|
|
991
|
+
record: OrderRecord,
|
|
992
|
+
snapshot: OrderSnapshot,
|
|
993
|
+
): void {
|
|
994
|
+
if (isOpenOrder(snapshot)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
this.context.publishRuntimeError(
|
|
999
|
+
"order",
|
|
1000
|
+
new Error(
|
|
1001
|
+
"Dropped terminal order update without orderId or clientOrderId",
|
|
1002
|
+
),
|
|
1003
|
+
{
|
|
1004
|
+
accountId: record.accountId,
|
|
1005
|
+
venue: record.venue,
|
|
1006
|
+
symbol: snapshot.symbol,
|
|
1007
|
+
},
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private warnSystemClientOrderIdOnlyClaim(
|
|
1012
|
+
record: OrderRecord,
|
|
1013
|
+
snapshot: OrderSnapshot,
|
|
1014
|
+
): void {
|
|
1015
|
+
if (
|
|
1016
|
+
snapshot.orderId ||
|
|
1017
|
+
!snapshot.clientOrderId ||
|
|
1018
|
+
!this.isSystemClientOrderId(snapshot.clientOrderId)
|
|
1019
|
+
) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
this.context.publishRuntimeError(
|
|
1024
|
+
"order",
|
|
1025
|
+
new Error(
|
|
1026
|
+
"Received system clientOrderId without orderId; cid-only claim is unstable",
|
|
1027
|
+
),
|
|
1028
|
+
{
|
|
1029
|
+
accountId: record.accountId,
|
|
1030
|
+
venue: record.venue,
|
|
1031
|
+
symbol: snapshot.symbol,
|
|
1032
|
+
},
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private warnProvisionalTerminalOrder(
|
|
1037
|
+
record: OrderRecord,
|
|
1038
|
+
snapshot: OrderSnapshot,
|
|
1039
|
+
): void {
|
|
1040
|
+
// Terminal order missing orderId but carrying clientOrderId: stored under a
|
|
1041
|
+
// provisional client key and warned. The adapter contract requires terminal
|
|
1042
|
+
// updates to carry orderId (see adapter-contract.md); clientOrderId alone
|
|
1043
|
+
// cannot guarantee a stable unique primary key.
|
|
1044
|
+
if (snapshot.orderId || isOpenOrder(snapshot) || !snapshot.clientOrderId) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
this.context.publishRuntimeError(
|
|
1049
|
+
"order",
|
|
1050
|
+
new Error(
|
|
1051
|
+
"Stored terminal order without orderId using provisional clientOrderId key",
|
|
1052
|
+
),
|
|
1053
|
+
{
|
|
1054
|
+
accountId: record.accountId,
|
|
1055
|
+
venue: record.venue,
|
|
1056
|
+
symbol: snapshot.symbol,
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private getSnapshotAtLocation(
|
|
1062
|
+
record: OrderRecord,
|
|
1063
|
+
location: OrderLocation,
|
|
1064
|
+
): OrderSnapshot | undefined {
|
|
1065
|
+
return this.getOrderTable(record, location.table)
|
|
1066
|
+
.get(location.symbol)
|
|
1067
|
+
?.get(location.localOrderId);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private getSnapshotByLocalOrderId(
|
|
1071
|
+
record: OrderRecord,
|
|
1072
|
+
localOrderId: string,
|
|
1073
|
+
): OrderSnapshot | undefined {
|
|
1074
|
+
const location = record.localOrderLocations.get(localOrderId);
|
|
1075
|
+
return location ? this.getSnapshotAtLocation(record, location) : undefined;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private getOrderTable(
|
|
1079
|
+
record: OrderRecord,
|
|
1080
|
+
table: OrderTable,
|
|
1081
|
+
): Map<string, Map<string, OrderSnapshot>> {
|
|
1082
|
+
return table === "open" ? record.openOrders : record.closedOrders;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private getOrCreateSymbolOrders(
|
|
1086
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
1087
|
+
symbol: string,
|
|
1088
|
+
): Map<string, OrderSnapshot> {
|
|
1089
|
+
const existing = table.get(symbol);
|
|
1090
|
+
if (existing) {
|
|
1091
|
+
return existing;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const created = new Map<string, OrderSnapshot>();
|
|
1095
|
+
table.set(symbol, created);
|
|
1096
|
+
return created;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private getOrCreateOrderIdSymbolIndex(
|
|
1100
|
+
record: OrderRecord,
|
|
1101
|
+
symbol: string,
|
|
1102
|
+
): Map<string, string> {
|
|
1103
|
+
const existing = record.orderIdIndex.get(symbol);
|
|
1104
|
+
if (existing) {
|
|
1105
|
+
return existing;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const created = new Map<string, string>();
|
|
1109
|
+
record.orderIdIndex.set(symbol, created);
|
|
1110
|
+
return created;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private getLocalOrderIdForVenueOrderId(
|
|
1114
|
+
record: OrderRecord,
|
|
1115
|
+
symbol: string,
|
|
1116
|
+
orderId: string,
|
|
1117
|
+
): string | undefined {
|
|
1118
|
+
return record.orderIdIndex.get(symbol)?.get(orderId);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private getSnapshotsForOrderId(
|
|
1122
|
+
record: OrderRecord,
|
|
1123
|
+
orderId: string,
|
|
1124
|
+
): OrderSnapshot[] {
|
|
1125
|
+
return this.getSnapshotsForLocalOrderIds(
|
|
1126
|
+
record,
|
|
1127
|
+
record.orderIdOnlyIndex.get(orderId),
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private getSnapshotsForClientOrderId(
|
|
1132
|
+
record: OrderRecord,
|
|
1133
|
+
clientOrderId: string,
|
|
1134
|
+
): OrderSnapshot[] {
|
|
1135
|
+
return this.getSnapshotsForLocalOrderIds(
|
|
1136
|
+
record,
|
|
1137
|
+
record.clientOrderIdIndex.get(clientOrderId),
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private getSnapshotsForLocalOrderIds(
|
|
1142
|
+
record: OrderRecord,
|
|
1143
|
+
localOrderIds?: Iterable<string>,
|
|
1144
|
+
): OrderSnapshot[] {
|
|
1145
|
+
if (!localOrderIds) {
|
|
1146
|
+
return [];
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const snapshots: OrderSnapshot[] = [];
|
|
1150
|
+
for (const localOrderId of localOrderIds) {
|
|
1151
|
+
const snapshot = this.getSnapshotByLocalOrderId(record, localOrderId);
|
|
1152
|
+
if (snapshot) {
|
|
1153
|
+
snapshots.push(snapshot);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return snapshots;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private getOpenOrderSnapshots(record: OrderRecord): OrderSnapshot[] {
|
|
1161
|
+
return this.getSnapshotsInTable(record.openOrders);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private getAllSnapshots(record: OrderRecord): OrderSnapshot[] {
|
|
1165
|
+
return [
|
|
1166
|
+
...this.getSnapshotsInTable(record.openOrders),
|
|
1167
|
+
...this.getSnapshotsInTable(record.closedOrders),
|
|
1168
|
+
];
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
private getSnapshotsInTable(
|
|
1172
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
1173
|
+
): OrderSnapshot[] {
|
|
1174
|
+
const snapshots: OrderSnapshot[] = [];
|
|
1175
|
+
for (const symbolOrders of table.values()) {
|
|
1176
|
+
snapshots.push(...symbolOrders.values());
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return snapshots;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private getSnapshotCount(record: OrderRecord): number {
|
|
1183
|
+
return (
|
|
1184
|
+
this.getSnapshotCountInTable(record.openOrders) +
|
|
1185
|
+
this.getSnapshotCountInTable(record.closedOrders)
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
private getSnapshotCountInTable(
|
|
1190
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
1191
|
+
): number {
|
|
1192
|
+
let size = 0;
|
|
1193
|
+
for (const symbolOrders of table.values()) {
|
|
1194
|
+
size += symbolOrders.size;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return size;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private addLocalOrderIdToSetIndex(
|
|
1201
|
+
index: Map<string, Set<string>>,
|
|
1202
|
+
key: string,
|
|
1203
|
+
localOrderId: string,
|
|
1204
|
+
): void {
|
|
1205
|
+
this.removeLocalOrderIdFromSetIndex(index, key, localOrderId);
|
|
1206
|
+
|
|
1207
|
+
const localOrderIds = index.get(key);
|
|
1208
|
+
if (localOrderIds) {
|
|
1209
|
+
localOrderIds.add(localOrderId);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
index.set(key, new Set([localOrderId]));
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private removeLocalOrderIdFromSetIndex(
|
|
1217
|
+
index: Map<string, Set<string>>,
|
|
1218
|
+
key: string,
|
|
1219
|
+
localOrderId: string,
|
|
1220
|
+
): void {
|
|
1221
|
+
const localOrderIds = index.get(key);
|
|
1222
|
+
if (!localOrderIds) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
localOrderIds.delete(localOrderId);
|
|
1227
|
+
|
|
1228
|
+
if (localOrderIds.size === 0) {
|
|
1229
|
+
index.delete(key);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private selectLatestSnapshot(
|
|
1234
|
+
snapshots: OrderSnapshot[],
|
|
1235
|
+
): OrderSnapshot | undefined {
|
|
1236
|
+
let latest: OrderSnapshot | undefined;
|
|
1237
|
+
for (const snapshot of snapshots) {
|
|
1238
|
+
if (!latest) {
|
|
1239
|
+
latest = snapshot;
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const snapshotOpen = isOpenOrder(snapshot);
|
|
1244
|
+
const latestOpen = isOpenOrder(latest);
|
|
1245
|
+
if (snapshotOpen !== latestOpen) {
|
|
1246
|
+
// Open candidate has absolute priority: current active order takes
|
|
1247
|
+
// precedence over historical terminal state (when clientOrderId is
|
|
1248
|
+
// reused, the old order is already closed).
|
|
1249
|
+
if (snapshotOpen) {
|
|
1250
|
+
latest = snapshot;
|
|
1251
|
+
}
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Both open or both closed: take the latest by updatedAt.
|
|
1256
|
+
// seq must not be used -- seq is a per-order version number and is not
|
|
1257
|
+
// comparable across orders (e.g. different orders that reuse a cid).
|
|
1258
|
+
if (snapshot.updatedAt > latest.updatedAt) {
|
|
1259
|
+
latest = snapshot;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return latest;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private applyUpdateToRecord(
|
|
1267
|
+
record: OrderRecord,
|
|
1268
|
+
accountId: string,
|
|
1269
|
+
venue: Venue,
|
|
1270
|
+
update: RawOrderUpdate,
|
|
1271
|
+
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
1272
|
+
): OrderSnapshot | undefined {
|
|
1273
|
+
const resolution = this.resolveLocalOrderIdForUpdate(record, update);
|
|
1274
|
+
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
1275
|
+
const previousLocation = record.localOrderLocations.get(localOrderId);
|
|
1276
|
+
const previous = previousLocation
|
|
1277
|
+
? this.getSnapshotAtLocation(record, previousLocation)
|
|
1278
|
+
: undefined;
|
|
1279
|
+
if (
|
|
1280
|
+
!shouldApplyWatermarkedUpdate(previous, update, {
|
|
1281
|
+
requestStartedAt: options.requestStartedAt,
|
|
1282
|
+
source: options.requestStartedAt === undefined ? "stream" : "rest",
|
|
1283
|
+
})
|
|
1284
|
+
) {
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const snapshot = this.createSnapshot(accountId, venue, update, previous);
|
|
1289
|
+
const location = this.setSnapshot(
|
|
1290
|
+
record,
|
|
1291
|
+
localOrderId,
|
|
1292
|
+
snapshot,
|
|
1293
|
+
previousLocation,
|
|
1294
|
+
);
|
|
1295
|
+
if (location && resolution.source === "pending" && update.clientOrderId) {
|
|
1296
|
+
this.clearPendingClientOrderClaim(
|
|
1297
|
+
record,
|
|
1298
|
+
update.clientOrderId,
|
|
1299
|
+
localOrderId,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
return location ? snapshot : undefined;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
527
1306
|
private createSnapshot(
|
|
528
1307
|
accountId: string,
|
|
529
1308
|
venue: Venue,
|
|
@@ -531,7 +1310,13 @@ export class OrderManagerImpl
|
|
|
531
1310
|
previous?: OrderSnapshot,
|
|
532
1311
|
): OrderSnapshot {
|
|
533
1312
|
const amount = new BigNumber(input.amount);
|
|
534
|
-
const
|
|
1313
|
+
const rawFilled = new BigNumber(input.filled);
|
|
1314
|
+
const filled =
|
|
1315
|
+
previous &&
|
|
1316
|
+
input.exchangeTs !== undefined &&
|
|
1317
|
+
previous.exchangeTs === input.exchangeTs
|
|
1318
|
+
? BigNumber.maximum(rawFilled, previous.filled)
|
|
1319
|
+
: rawFilled;
|
|
535
1320
|
const remaining =
|
|
536
1321
|
input.remaining === undefined
|
|
537
1322
|
? amount.minus(filled)
|
|
@@ -540,12 +1325,12 @@ export class OrderManagerImpl
|
|
|
540
1325
|
return {
|
|
541
1326
|
accountId,
|
|
542
1327
|
venue,
|
|
543
|
-
orderId: input.orderId,
|
|
544
|
-
clientOrderId: input.clientOrderId,
|
|
1328
|
+
orderId: input.orderId ?? previous?.orderId,
|
|
1329
|
+
clientOrderId: input.clientOrderId ?? previous?.clientOrderId,
|
|
545
1330
|
symbol: input.symbol,
|
|
546
1331
|
side: input.side,
|
|
547
1332
|
type: input.type,
|
|
548
|
-
status: input
|
|
1333
|
+
status: this.mergeOrderStatus(input, previous),
|
|
549
1334
|
price:
|
|
550
1335
|
input.price === undefined ? previous?.price : toCanonical(input.price),
|
|
551
1336
|
triggerPrice:
|
|
@@ -568,6 +1353,26 @@ export class OrderManagerImpl
|
|
|
568
1353
|
};
|
|
569
1354
|
}
|
|
570
1355
|
|
|
1356
|
+
private mergeOrderStatus(
|
|
1357
|
+
input: RawOrderUpdate,
|
|
1358
|
+
previous?: OrderSnapshot,
|
|
1359
|
+
): OrderSnapshot["status"] {
|
|
1360
|
+
if (!previous) {
|
|
1361
|
+
return input.status;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (
|
|
1365
|
+
input.exchangeTs !== undefined &&
|
|
1366
|
+
previous.exchangeTs !== undefined &&
|
|
1367
|
+
input.exchangeTs === previous.exchangeTs &&
|
|
1368
|
+
orderPriority(input.status) < orderPriority(previous.status)
|
|
1369
|
+
) {
|
|
1370
|
+
return previous.status;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return input.status;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
571
1376
|
private publishStatus(record: OrderRecord): void {
|
|
572
1377
|
const event: OrderStatusChangedEvent = {
|
|
573
1378
|
type: "order.status_changed",
|
|
@@ -581,6 +1386,78 @@ export class OrderManagerImpl
|
|
|
581
1386
|
this.context.publishHealthEvent(event);
|
|
582
1387
|
}
|
|
583
1388
|
|
|
1389
|
+
private generateLocalOrderId(options?: {
|
|
1390
|
+
record?: OrderRecord;
|
|
1391
|
+
avoidOpenClientOrderId?: boolean;
|
|
1392
|
+
}): string {
|
|
1393
|
+
while (true) {
|
|
1394
|
+
const candidate = `${SDK_CLIENT_ORDER_ID_PREFIX}${this.context.now().toString(36)}-${(this.localOrderSequence++).toString(36)}`;
|
|
1395
|
+
if (
|
|
1396
|
+
(options?.record &&
|
|
1397
|
+
options.avoidOpenClientOrderId &&
|
|
1398
|
+
this.isVenueClientOrderIdInUseForOpenOrder(
|
|
1399
|
+
options.record,
|
|
1400
|
+
candidate,
|
|
1401
|
+
)) ||
|
|
1402
|
+
options?.record?.pendingClientOrderIdIndex.has(candidate) ||
|
|
1403
|
+
!VENUE_CLIENT_ORDER_ID_PATTERN.test(candidate)
|
|
1404
|
+
) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return candidate;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private isVenueClientOrderIdInUseForOpenOrder(
|
|
1413
|
+
record: OrderRecord,
|
|
1414
|
+
venueClientOrderId: string,
|
|
1415
|
+
): boolean {
|
|
1416
|
+
for (const localOrderId of record.clientOrderIdIndex.get(
|
|
1417
|
+
venueClientOrderId,
|
|
1418
|
+
) ?? []) {
|
|
1419
|
+
const location = record.localOrderLocations.get(localOrderId);
|
|
1420
|
+
if (location?.table === "open") {
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private addPendingClientOrderClaim(
|
|
1429
|
+
record: OrderRecord,
|
|
1430
|
+
symbol: string,
|
|
1431
|
+
venueClientOrderId: string,
|
|
1432
|
+
localOrderId: string,
|
|
1433
|
+
): void {
|
|
1434
|
+
record.pendingClientOrderIdIndex.set(venueClientOrderId, {
|
|
1435
|
+
localOrderId,
|
|
1436
|
+
symbol,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private clearPendingClientOrderClaim(
|
|
1441
|
+
record: OrderRecord,
|
|
1442
|
+
venueClientOrderId: string,
|
|
1443
|
+
localOrderId: string,
|
|
1444
|
+
): void {
|
|
1445
|
+
const pending = record.pendingClientOrderIdIndex.get(venueClientOrderId);
|
|
1446
|
+
if (pending?.localOrderId === localOrderId) {
|
|
1447
|
+
record.pendingClientOrderIdIndex.delete(venueClientOrderId);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private shouldRetainPendingClaimAfterCreateError(error: unknown): boolean {
|
|
1452
|
+
return isTransportError(error) && error.kind === "timeout";
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private isSystemClientOrderId(clientOrderId: string): boolean {
|
|
1456
|
+
return SYSTEM_CLIENT_ORDER_ID_PATTERNS.some((pattern) =>
|
|
1457
|
+
pattern.test(clientOrderId),
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
584
1461
|
private validateCreateOrderInput(
|
|
585
1462
|
input: CreateOrderInput,
|
|
586
1463
|
venue: Venue,
|
|
@@ -596,6 +1473,21 @@ export class OrderManagerImpl
|
|
|
596
1473
|
},
|
|
597
1474
|
);
|
|
598
1475
|
}
|
|
1476
|
+
|
|
1477
|
+
if (
|
|
1478
|
+
input.clientOrderId !== undefined &&
|
|
1479
|
+
!VENUE_CLIENT_ORDER_ID_PATTERN.test(input.clientOrderId)
|
|
1480
|
+
) {
|
|
1481
|
+
throw this.createError(
|
|
1482
|
+
"ORDER_INPUT_INVALID",
|
|
1483
|
+
`clientOrderId must be 1-32 Binance-safe characters: ${input.accountId}`,
|
|
1484
|
+
{
|
|
1485
|
+
accountId: input.accountId,
|
|
1486
|
+
venue,
|
|
1487
|
+
symbol: input.symbol,
|
|
1488
|
+
},
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
599
1491
|
}
|
|
600
1492
|
|
|
601
1493
|
private validateCancelOrderInput(
|
|
@@ -621,11 +1513,21 @@ export class OrderManagerImpl
|
|
|
621
1513
|
accountId: string,
|
|
622
1514
|
venue: Venue,
|
|
623
1515
|
update: RawOrderUpdate,
|
|
1516
|
+
options: { localOrderId?: string } = {},
|
|
624
1517
|
): OrderSnapshot {
|
|
625
1518
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
626
|
-
const
|
|
1519
|
+
const resolution = this.resolveLocalOrderIdForUpdate(
|
|
1520
|
+
record,
|
|
1521
|
+
update,
|
|
1522
|
+
options.localOrderId,
|
|
1523
|
+
);
|
|
1524
|
+
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
1525
|
+
const previousLocation = record.localOrderLocations.get(localOrderId);
|
|
1526
|
+
const previous = previousLocation
|
|
1527
|
+
? this.getSnapshotAtLocation(record, previousLocation)
|
|
1528
|
+
: undefined;
|
|
627
1529
|
const snapshot = this.createSnapshot(accountId, venue, update, previous);
|
|
628
|
-
this.setSnapshot(record
|
|
1530
|
+
this.setSnapshot(record, localOrderId, snapshot, previousLocation);
|
|
629
1531
|
return snapshot;
|
|
630
1532
|
}
|
|
631
1533
|
|
|
@@ -653,7 +1555,8 @@ export class OrderManagerImpl
|
|
|
653
1555
|
symbol?: string;
|
|
654
1556
|
},
|
|
655
1557
|
): AcexError {
|
|
656
|
-
const
|
|
1558
|
+
const details = buildAcexErrorDetails(metadata);
|
|
1559
|
+
const error = new AcexError(code, message, { details });
|
|
657
1560
|
this.context.publishRuntimeError("order", error, metadata);
|
|
658
1561
|
return error;
|
|
659
1562
|
}
|
|
@@ -680,6 +1583,10 @@ export class OrderManagerImpl
|
|
|
680
1583
|
error instanceof Error ? error : new Error(message),
|
|
681
1584
|
metadata,
|
|
682
1585
|
);
|
|
683
|
-
|
|
1586
|
+
const details = buildAcexErrorDetails(metadata, error);
|
|
1587
|
+
return new AcexError(code, formatAcexErrorMessage(message, details), {
|
|
1588
|
+
cause: error,
|
|
1589
|
+
details,
|
|
1590
|
+
});
|
|
684
1591
|
}
|
|
685
1592
|
}
|