@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.10
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 +474 -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 +791 -76
- 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,18 @@ 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 {
|
|
23
|
+
canDeleteMissingFromSnapshot,
|
|
24
|
+
shouldApplyWatermarkedUpdate,
|
|
25
|
+
} from "../internal/watermark.ts";
|
|
15
26
|
import type {
|
|
16
27
|
CancelAllOrdersInput,
|
|
17
28
|
CancelOrderInput,
|
|
@@ -33,15 +44,55 @@ interface OrderRecord {
|
|
|
33
44
|
accountId: string;
|
|
34
45
|
venue: Venue;
|
|
35
46
|
subscribed: boolean;
|
|
36
|
-
|
|
47
|
+
openOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
48
|
+
closedOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
49
|
+
orderIdIndex: Map<string, Map<string, OrderLocation>>;
|
|
50
|
+
orderIdOnlyIndex: Map<string, Set<OrderLocation>>;
|
|
51
|
+
clientOrderIdIndex: Map<string, Set<OrderLocation>>;
|
|
37
52
|
status: OrderDataStatus;
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
type OrderTable = "open" | "closed";
|
|
56
|
+
|
|
57
|
+
interface OrderLocation {
|
|
58
|
+
table: OrderTable;
|
|
59
|
+
symbol: string;
|
|
60
|
+
key: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface OrderManagerOptions {
|
|
64
|
+
maxClosedOrdersPerSymbol?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL = 500;
|
|
68
|
+
|
|
40
69
|
function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
41
70
|
return { ...status };
|
|
42
71
|
}
|
|
43
72
|
|
|
73
|
+
function normalizeMaxClosedOrdersPerSymbol(value: number | undefined): number {
|
|
74
|
+
return value !== undefined && Number.isInteger(value) && value > 0
|
|
75
|
+
? value
|
|
76
|
+
: DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL;
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
function getOrderLookupKey(input: {
|
|
80
|
+
symbol: string;
|
|
81
|
+
orderId?: string;
|
|
82
|
+
clientOrderId?: string;
|
|
83
|
+
}): string | undefined {
|
|
84
|
+
if (input.orderId) {
|
|
85
|
+
return `symbol:${input.symbol}:order:${input.orderId}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (input.clientOrderId) {
|
|
89
|
+
return `symbol:${input.symbol}:client:${input.clientOrderId}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getOrderKey(input: {
|
|
45
96
|
orderId?: string;
|
|
46
97
|
clientOrderId?: string;
|
|
47
98
|
}): string | undefined {
|
|
@@ -56,6 +107,94 @@ function getOrderLookupKey(input: {
|
|
|
56
107
|
return undefined;
|
|
57
108
|
}
|
|
58
109
|
|
|
110
|
+
function shouldMatchOrderIdentity(
|
|
111
|
+
candidate: OrderSnapshot,
|
|
112
|
+
input: { symbol?: string; orderId?: string; clientOrderId?: string },
|
|
113
|
+
): boolean {
|
|
114
|
+
if (input.symbol && candidate.symbol !== input.symbol) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Boolean(
|
|
119
|
+
(input.orderId && candidate.orderId === input.orderId) ||
|
|
120
|
+
(input.clientOrderId && candidate.clientOrderId === input.clientOrderId),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function shouldMatchStoredOrderIdentity(
|
|
125
|
+
candidate: OrderSnapshot,
|
|
126
|
+
input: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
127
|
+
): boolean {
|
|
128
|
+
if (candidate.symbol !== input.symbol) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (candidate.orderId && input.orderId) {
|
|
133
|
+
return candidate.orderId === input.orderId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// clientOrderId 只作"尚未拿到 orderId 的订单"的临时身份:已带 orderId 的候选
|
|
137
|
+
// (含 clientOrderId 复用后躺在 closed 的旧订单)不得被 cid-only 更新归并,否则会
|
|
138
|
+
// carry-forward 旧 orderId、污染 closed。orderId 后填充时 candidate 仍无 orderId,照常匹配。
|
|
139
|
+
return Boolean(
|
|
140
|
+
input.clientOrderId &&
|
|
141
|
+
candidate.clientOrderId === input.clientOrderId &&
|
|
142
|
+
!candidate.orderId,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function successfulStatus(
|
|
147
|
+
status: OrderDataStatus,
|
|
148
|
+
options: {
|
|
149
|
+
ready?: boolean;
|
|
150
|
+
lastReceivedAt?: number;
|
|
151
|
+
lastReadyAt?: number;
|
|
152
|
+
preserveStatus?: boolean;
|
|
153
|
+
},
|
|
154
|
+
): OrderDataStatus {
|
|
155
|
+
const preservesStreamState =
|
|
156
|
+
options.preserveStatus &&
|
|
157
|
+
(status.runtimeStatus === "reconnecting" ||
|
|
158
|
+
status.reason === "ws_disconnected" ||
|
|
159
|
+
status.reason === "heartbeat_timeout");
|
|
160
|
+
const ready = options.ready ?? true;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...status,
|
|
164
|
+
activity: "active",
|
|
165
|
+
ready,
|
|
166
|
+
runtimeStatus: preservesStreamState ? status.runtimeStatus : "healthy",
|
|
167
|
+
reason: preservesStreamState ? status.reason : undefined,
|
|
168
|
+
lastReceivedAt: options.lastReceivedAt ?? status.lastReceivedAt,
|
|
169
|
+
lastReadyAt: ready
|
|
170
|
+
? (options.lastReadyAt ??
|
|
171
|
+
(options.preserveStatus ? status.lastReadyAt : undefined) ??
|
|
172
|
+
Date.now())
|
|
173
|
+
: status.lastReadyAt,
|
|
174
|
+
inactiveSince: undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isOpenOrder(snapshot: OrderSnapshot): boolean {
|
|
179
|
+
return snapshot.status === "open" || snapshot.status === "partially_filled";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function orderPriority(status: OrderSnapshot["status"]): number {
|
|
183
|
+
switch (status) {
|
|
184
|
+
case "filled":
|
|
185
|
+
return 5;
|
|
186
|
+
case "canceled":
|
|
187
|
+
case "expired":
|
|
188
|
+
return 4;
|
|
189
|
+
case "rejected":
|
|
190
|
+
return 3;
|
|
191
|
+
case "partially_filled":
|
|
192
|
+
return 2;
|
|
193
|
+
case "open":
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
59
198
|
export class OrderManagerImpl
|
|
60
199
|
implements
|
|
61
200
|
OrderManager,
|
|
@@ -67,13 +206,17 @@ export class OrderManagerImpl
|
|
|
67
206
|
readonly events: OrderEventStreams;
|
|
68
207
|
|
|
69
208
|
private readonly context: ClientContext;
|
|
209
|
+
private readonly maxClosedOrdersPerSymbol: number;
|
|
70
210
|
private readonly orderBus = new AsyncEventBus<OrderEvent>();
|
|
71
211
|
private readonly orderStatusBus =
|
|
72
212
|
new AsyncEventBus<OrderStatusChangedEvent>();
|
|
73
213
|
private readonly records = new Map<string, OrderRecord>();
|
|
74
214
|
|
|
75
|
-
constructor(context: ClientContext) {
|
|
215
|
+
constructor(context: ClientContext, options: OrderManagerOptions = {}) {
|
|
76
216
|
this.context = context;
|
|
217
|
+
this.maxClosedOrdersPerSymbol = normalizeMaxClosedOrdersPerSymbol(
|
|
218
|
+
options.maxClosedOrdersPerSymbol,
|
|
219
|
+
);
|
|
77
220
|
|
|
78
221
|
this.events = {
|
|
79
222
|
status: (filter) =>
|
|
@@ -102,7 +245,10 @@ export class OrderManagerImpl
|
|
|
102
245
|
async subscribeOrders(input: SubscribeOrdersInput): Promise<void> {
|
|
103
246
|
this.context.assertStarted();
|
|
104
247
|
const account = this.context.getRegisteredAccount(input.accountId);
|
|
105
|
-
if (
|
|
248
|
+
if (
|
|
249
|
+
this.context.getPrivateOrderCapabilities(account.venue)?.updates ===
|
|
250
|
+
"unsupported"
|
|
251
|
+
) {
|
|
106
252
|
throw this.createError(
|
|
107
253
|
"VENUE_NOT_SUPPORTED",
|
|
108
254
|
`Venue does not support private order subscriptions: ${account.venue}`,
|
|
@@ -218,17 +364,56 @@ export class OrderManagerImpl
|
|
|
218
364
|
return undefined;
|
|
219
365
|
}
|
|
220
366
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
367
|
+
if (input.symbol && input.orderId) {
|
|
368
|
+
const location = this.getOrderIdLocation(
|
|
369
|
+
record,
|
|
370
|
+
input.symbol,
|
|
371
|
+
input.orderId,
|
|
372
|
+
);
|
|
373
|
+
const snapshot = location
|
|
374
|
+
? this.getSnapshotAtLocation(record, location)
|
|
375
|
+
: undefined;
|
|
376
|
+
if (!snapshot) {
|
|
377
|
+
return undefined;
|
|
224
378
|
}
|
|
225
379
|
|
|
226
380
|
if (
|
|
227
381
|
input.clientOrderId &&
|
|
228
|
-
snapshot.clientOrderId
|
|
382
|
+
snapshot.clientOrderId !== input.clientOrderId
|
|
229
383
|
) {
|
|
230
|
-
return
|
|
384
|
+
return undefined;
|
|
231
385
|
}
|
|
386
|
+
|
|
387
|
+
return snapshot;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (input.orderId) {
|
|
391
|
+
return this.selectLatestSnapshot(
|
|
392
|
+
this.getSnapshotsForOrderId(record, input.orderId).filter(
|
|
393
|
+
(snapshot) =>
|
|
394
|
+
shouldMatchOrderIdentity(snapshot, {
|
|
395
|
+
symbol: input.symbol,
|
|
396
|
+
orderId: input.orderId,
|
|
397
|
+
}) &&
|
|
398
|
+
(!input.clientOrderId ||
|
|
399
|
+
shouldMatchOrderIdentity(snapshot, {
|
|
400
|
+
symbol: input.symbol,
|
|
401
|
+
clientOrderId: input.clientOrderId,
|
|
402
|
+
})),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (input.clientOrderId) {
|
|
408
|
+
return this.selectLatestSnapshot(
|
|
409
|
+
this.getSnapshotsForClientOrderId(record, input.clientOrderId).filter(
|
|
410
|
+
(snapshot) =>
|
|
411
|
+
shouldMatchOrderIdentity(snapshot, {
|
|
412
|
+
symbol: input.symbol,
|
|
413
|
+
clientOrderId: input.clientOrderId,
|
|
414
|
+
}),
|
|
415
|
+
),
|
|
416
|
+
);
|
|
232
417
|
}
|
|
233
418
|
|
|
234
419
|
return undefined;
|
|
@@ -240,15 +425,11 @@ export class OrderManagerImpl
|
|
|
240
425
|
return [];
|
|
241
426
|
}
|
|
242
427
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
428
|
+
if (symbol) {
|
|
429
|
+
return [...(record.openOrders.get(symbol)?.values() ?? [])];
|
|
430
|
+
}
|
|
247
431
|
|
|
248
|
-
|
|
249
|
-
snapshot.status === "open" || snapshot.status === "partially_filled"
|
|
250
|
-
);
|
|
251
|
-
});
|
|
432
|
+
return this.getOpenOrderSnapshots(record);
|
|
252
433
|
}
|
|
253
434
|
|
|
254
435
|
getOrderStatus(accountId: string): OrderDataStatus | undefined {
|
|
@@ -316,7 +497,7 @@ export class OrderManagerImpl
|
|
|
316
497
|
|
|
317
498
|
record.status = {
|
|
318
499
|
...this.createStatus(accountId, venue, "active"),
|
|
319
|
-
ready: record
|
|
500
|
+
ready: this.getSnapshotCount(record) > 0,
|
|
320
501
|
runtimeStatus: "bootstrap_pending",
|
|
321
502
|
reason: undefined,
|
|
322
503
|
lastReceivedAt: record.status.lastReceivedAt,
|
|
@@ -329,40 +510,82 @@ export class OrderManagerImpl
|
|
|
329
510
|
onPrivateOrderBootstrap(
|
|
330
511
|
accountId: string,
|
|
331
512
|
venue: Venue,
|
|
332
|
-
|
|
333
|
-
|
|
513
|
+
snapshot: RawOpenOrdersSnapshot,
|
|
514
|
+
options: { requestStartedAt: number; preserveStatus?: boolean },
|
|
515
|
+
): OrderSnapshot[] {
|
|
516
|
+
return this.onPrivateOrderReconcile(accountId, venue, snapshot, options);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
onPrivateOrderReconcile(
|
|
520
|
+
accountId: string,
|
|
521
|
+
venue: Venue,
|
|
522
|
+
snapshot: RawOpenOrdersSnapshot,
|
|
523
|
+
options: { requestStartedAt: number; preserveStatus?: boolean },
|
|
524
|
+
): OrderSnapshot[] {
|
|
334
525
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
335
526
|
if (!record.subscribed) {
|
|
336
|
-
return;
|
|
527
|
+
return [];
|
|
337
528
|
}
|
|
338
529
|
|
|
339
|
-
const
|
|
340
|
-
for (const update of
|
|
341
|
-
const
|
|
530
|
+
const openSetKeys = new Set<string>();
|
|
531
|
+
for (const update of snapshot.orders) {
|
|
532
|
+
const lookupKey = getOrderLookupKey(update);
|
|
533
|
+
if (lookupKey) {
|
|
534
|
+
openSetKeys.add(lookupKey);
|
|
535
|
+
}
|
|
536
|
+
const current = this.getExistingSnapshot(record, update);
|
|
537
|
+
const nextSnapshot = this.applyUpdateToRecord(
|
|
538
|
+
record,
|
|
342
539
|
accountId,
|
|
343
540
|
venue,
|
|
344
541
|
update,
|
|
345
|
-
|
|
542
|
+
{
|
|
543
|
+
requestStartedAt: options.requestStartedAt,
|
|
544
|
+
preserveStatus: true,
|
|
545
|
+
},
|
|
346
546
|
);
|
|
347
|
-
|
|
547
|
+
if (nextSnapshot) {
|
|
548
|
+
const nextLookupKey = getOrderLookupKey(nextSnapshot);
|
|
549
|
+
if (nextLookupKey) {
|
|
550
|
+
openSetKeys.add(nextLookupKey);
|
|
551
|
+
}
|
|
552
|
+
} else if (current) {
|
|
553
|
+
const currentLookupKey = getOrderLookupKey(current);
|
|
554
|
+
if (currentLookupKey) {
|
|
555
|
+
openSetKeys.add(currentLookupKey);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
348
558
|
}
|
|
349
559
|
|
|
350
|
-
record.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
560
|
+
const disappeared = this.getOpenOrderSnapshots(record).filter((order) => {
|
|
561
|
+
if (!isOpenOrder(order)) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const lookupKey = getOrderLookupKey(order);
|
|
566
|
+
if (!lookupKey || openSetKeys.has(lookupKey)) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return canDeleteMissingFromSnapshot(order, {
|
|
571
|
+
requestStartedAt: options.requestStartedAt,
|
|
572
|
+
snapshotExchangeTs: snapshot.snapshotExchangeTs,
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const orderedSnapshots = this.getAllSnapshots(record);
|
|
577
|
+
const latestTs = Math.max(
|
|
578
|
+
snapshot.snapshotReceivedAt,
|
|
579
|
+
orderedSnapshots.reduce(
|
|
580
|
+
(max, order) => Math.max(max, order.updatedAt),
|
|
581
|
+
0,
|
|
582
|
+
),
|
|
355
583
|
);
|
|
356
|
-
record.status = {
|
|
357
|
-
|
|
358
|
-
activity: "active",
|
|
359
|
-
ready: true,
|
|
360
|
-
runtimeStatus: "healthy",
|
|
361
|
-
reason: undefined,
|
|
584
|
+
record.status = successfulStatus(record.status, {
|
|
585
|
+
preserveStatus: options.preserveStatus,
|
|
362
586
|
lastReceivedAt: latestTs || record.status.lastReceivedAt,
|
|
363
587
|
lastReadyAt: latestTs || this.context.now(),
|
|
364
|
-
|
|
365
|
-
};
|
|
588
|
+
});
|
|
366
589
|
|
|
367
590
|
const event: OrderSnapshotReplacedEvent = {
|
|
368
591
|
type: "order.snapshot_replaced",
|
|
@@ -374,21 +597,37 @@ export class OrderManagerImpl
|
|
|
374
597
|
|
|
375
598
|
this.orderBus.publish(event);
|
|
376
599
|
this.publishStatus(record);
|
|
600
|
+
return disappeared;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getPrivateOpenOrders(accountId: string): OrderSnapshot[] {
|
|
604
|
+
return this.getOpenOrders(accountId);
|
|
377
605
|
}
|
|
378
606
|
|
|
379
607
|
onPrivateOrderUpdate(
|
|
380
608
|
accountId: string,
|
|
381
609
|
venue: Venue,
|
|
382
610
|
update: RawOrderUpdate,
|
|
611
|
+
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
383
612
|
): void {
|
|
384
613
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
385
614
|
if (!record.subscribed) {
|
|
386
615
|
return;
|
|
387
616
|
}
|
|
388
617
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
618
|
+
const snapshot = this.applyUpdateToRecord(
|
|
619
|
+
record,
|
|
620
|
+
accountId,
|
|
621
|
+
venue,
|
|
622
|
+
update,
|
|
623
|
+
{
|
|
624
|
+
requestStartedAt: options.requestStartedAt,
|
|
625
|
+
preserveStatus: options.preserveStatus,
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
if (!snapshot) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
392
631
|
|
|
393
632
|
const eventType =
|
|
394
633
|
snapshot.status === "filled"
|
|
@@ -408,16 +647,11 @@ export class OrderManagerImpl
|
|
|
408
647
|
ts: this.context.now(),
|
|
409
648
|
});
|
|
410
649
|
|
|
411
|
-
record.status = {
|
|
412
|
-
|
|
413
|
-
activity: "active",
|
|
414
|
-
ready: true,
|
|
415
|
-
runtimeStatus: "healthy",
|
|
416
|
-
reason: undefined,
|
|
650
|
+
record.status = successfulStatus(record.status, {
|
|
651
|
+
preserveStatus: options.preserveStatus,
|
|
417
652
|
lastReceivedAt: snapshot.receivedAt,
|
|
418
653
|
lastReadyAt: snapshot.updatedAt,
|
|
419
|
-
|
|
420
|
-
};
|
|
654
|
+
});
|
|
421
655
|
this.publishStatus(record);
|
|
422
656
|
}
|
|
423
657
|
|
|
@@ -468,7 +702,11 @@ export class OrderManagerImpl
|
|
|
468
702
|
accountId,
|
|
469
703
|
venue,
|
|
470
704
|
subscribed: false,
|
|
471
|
-
|
|
705
|
+
openOrders: new Map(),
|
|
706
|
+
closedOrders: new Map(),
|
|
707
|
+
orderIdIndex: new Map(),
|
|
708
|
+
orderIdOnlyIndex: new Map(),
|
|
709
|
+
clientOrderIdIndex: new Map(),
|
|
472
710
|
status: this.createStatus(accountId, venue, "inactive"),
|
|
473
711
|
};
|
|
474
712
|
|
|
@@ -492,18 +730,40 @@ export class OrderManagerImpl
|
|
|
492
730
|
|
|
493
731
|
private getExistingSnapshot(
|
|
494
732
|
record: OrderRecord,
|
|
495
|
-
update: { orderId?: string; clientOrderId?: string },
|
|
733
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
496
734
|
): OrderSnapshot | undefined {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
735
|
+
const location = this.getExistingSnapshotLocation(record, update);
|
|
736
|
+
return location ? this.getSnapshotAtLocation(record, location) : undefined;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private getExistingSnapshotLocation(
|
|
740
|
+
record: OrderRecord,
|
|
741
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
742
|
+
): OrderLocation | undefined {
|
|
743
|
+
if (update.orderId) {
|
|
744
|
+
const location = this.getOrderIdLocation(
|
|
745
|
+
record,
|
|
746
|
+
update.symbol,
|
|
747
|
+
update.orderId,
|
|
748
|
+
);
|
|
749
|
+
const snapshot = location
|
|
750
|
+
? this.getSnapshotAtLocation(record, location)
|
|
751
|
+
: undefined;
|
|
752
|
+
if (snapshot && shouldMatchStoredOrderIdentity(snapshot, update)) {
|
|
753
|
+
return location;
|
|
500
754
|
}
|
|
755
|
+
}
|
|
501
756
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
757
|
+
if (!update.clientOrderId) {
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
for (const location of record.clientOrderIdIndex.get(
|
|
762
|
+
update.clientOrderId,
|
|
763
|
+
) ?? []) {
|
|
764
|
+
const snapshot = this.getSnapshotAtLocation(record, location);
|
|
765
|
+
if (snapshot && shouldMatchStoredOrderIdentity(snapshot, update)) {
|
|
766
|
+
return location;
|
|
507
767
|
}
|
|
508
768
|
}
|
|
509
769
|
|
|
@@ -511,17 +771,441 @@ export class OrderManagerImpl
|
|
|
511
771
|
}
|
|
512
772
|
|
|
513
773
|
private setSnapshot(
|
|
514
|
-
|
|
774
|
+
record: OrderRecord,
|
|
775
|
+
snapshot: OrderSnapshot,
|
|
776
|
+
previous?: OrderSnapshot,
|
|
777
|
+
): OrderLocation | undefined {
|
|
778
|
+
const existing = previous ?? this.getExistingSnapshot(record, snapshot);
|
|
779
|
+
const previousLocation = existing
|
|
780
|
+
? this.getSnapshotLocation(existing)
|
|
781
|
+
: undefined;
|
|
782
|
+
|
|
783
|
+
if (previousLocation) {
|
|
784
|
+
return this.moveSnapshot(record, previousLocation, snapshot);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return this.insertSnapshot(record, snapshot);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private insertSnapshot(
|
|
791
|
+
record: OrderRecord,
|
|
792
|
+
snapshot: OrderSnapshot,
|
|
793
|
+
): OrderLocation | undefined {
|
|
794
|
+
const location = this.getSnapshotLocation(snapshot);
|
|
795
|
+
if (!location) {
|
|
796
|
+
this.warnDroppedUnkeyedTerminalOrder(record, snapshot);
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
this.deleteSnapshot(record, location);
|
|
801
|
+
|
|
802
|
+
const table = this.getOrderTable(record, location.table);
|
|
803
|
+
const symbolOrders = this.getOrCreateSymbolOrders(table, location.symbol);
|
|
804
|
+
symbolOrders.set(location.key, snapshot);
|
|
805
|
+
this.trimClosedOrdersForSymbol(record, location);
|
|
806
|
+
|
|
807
|
+
if (snapshot.orderId) {
|
|
808
|
+
const symbolIndex = this.getOrCreateOrderIdSymbolIndex(
|
|
809
|
+
record,
|
|
810
|
+
snapshot.symbol,
|
|
811
|
+
);
|
|
812
|
+
symbolIndex.set(snapshot.orderId, location);
|
|
813
|
+
this.addLocationToSetIndex(
|
|
814
|
+
record.orderIdOnlyIndex,
|
|
815
|
+
snapshot.orderId,
|
|
816
|
+
location,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (snapshot.clientOrderId) {
|
|
821
|
+
this.addLocationToSetIndex(
|
|
822
|
+
record.clientOrderIdIndex,
|
|
823
|
+
snapshot.clientOrderId,
|
|
824
|
+
location,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
this.warnProvisionalTerminalOrder(record, snapshot);
|
|
829
|
+
return location;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private deleteSnapshot(
|
|
833
|
+
record: OrderRecord,
|
|
834
|
+
location: OrderLocation,
|
|
835
|
+
): OrderSnapshot | undefined {
|
|
836
|
+
const snapshot = this.getSnapshotAtLocation(record, location);
|
|
837
|
+
if (!snapshot) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const table = this.getOrderTable(record, location.table);
|
|
842
|
+
const symbolOrders = table.get(location.symbol);
|
|
843
|
+
symbolOrders?.delete(location.key);
|
|
844
|
+
if (symbolOrders?.size === 0) {
|
|
845
|
+
table.delete(location.symbol);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (snapshot.orderId) {
|
|
849
|
+
const symbolIndex = record.orderIdIndex.get(location.symbol);
|
|
850
|
+
if (
|
|
851
|
+
symbolIndex?.get(snapshot.orderId) &&
|
|
852
|
+
this.locationsEqual(symbolIndex.get(snapshot.orderId), location)
|
|
853
|
+
) {
|
|
854
|
+
symbolIndex.delete(snapshot.orderId);
|
|
855
|
+
}
|
|
856
|
+
if (symbolIndex?.size === 0) {
|
|
857
|
+
record.orderIdIndex.delete(location.symbol);
|
|
858
|
+
}
|
|
859
|
+
this.removeLocationFromSetIndex(
|
|
860
|
+
record.orderIdOnlyIndex,
|
|
861
|
+
snapshot.orderId,
|
|
862
|
+
location,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (snapshot.clientOrderId) {
|
|
867
|
+
this.removeLocationFromSetIndex(
|
|
868
|
+
record.clientOrderIdIndex,
|
|
869
|
+
snapshot.clientOrderId,
|
|
870
|
+
location,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return snapshot;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private moveSnapshot(
|
|
878
|
+
record: OrderRecord,
|
|
879
|
+
previousLocation: OrderLocation,
|
|
880
|
+
snapshot: OrderSnapshot,
|
|
881
|
+
): OrderLocation | undefined {
|
|
882
|
+
this.deleteSnapshot(record, previousLocation);
|
|
883
|
+
return this.insertSnapshot(record, snapshot);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private trimClosedOrdersForSymbol(
|
|
887
|
+
record: OrderRecord,
|
|
888
|
+
location: OrderLocation,
|
|
889
|
+
): void {
|
|
890
|
+
if (location.table !== "closed") {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let symbolOrders = record.closedOrders.get(location.symbol);
|
|
895
|
+
if (!symbolOrders || symbolOrders.size <= this.maxClosedOrdersPerSymbol) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const trimBatchSize = Math.max(
|
|
900
|
+
1,
|
|
901
|
+
Math.floor(this.maxClosedOrdersPerSymbol / 10),
|
|
902
|
+
);
|
|
903
|
+
while (symbolOrders && symbolOrders.size > this.maxClosedOrdersPerSymbol) {
|
|
904
|
+
const keys = symbolOrders.keys();
|
|
905
|
+
for (let deleted = 0; deleted < trimBatchSize; deleted += 1) {
|
|
906
|
+
const next = keys.next();
|
|
907
|
+
if (next.done) {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
this.deleteSnapshot(record, {
|
|
911
|
+
table: "closed",
|
|
912
|
+
symbol: location.symbol,
|
|
913
|
+
key: next.value,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
symbolOrders = record.closedOrders.get(location.symbol);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private getSnapshotLocation(
|
|
921
|
+
snapshot: OrderSnapshot,
|
|
922
|
+
): OrderLocation | undefined {
|
|
923
|
+
const key = getOrderKey(snapshot);
|
|
924
|
+
if (!key) {
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
table: isOpenOrder(snapshot) ? "open" : "closed",
|
|
930
|
+
symbol: snapshot.symbol,
|
|
931
|
+
key,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private warnDroppedUnkeyedTerminalOrder(
|
|
936
|
+
record: OrderRecord,
|
|
515
937
|
snapshot: OrderSnapshot,
|
|
516
938
|
): void {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
939
|
+
if (isOpenOrder(snapshot)) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
this.context.publishRuntimeError(
|
|
944
|
+
"order",
|
|
945
|
+
new Error(
|
|
946
|
+
"Dropped terminal order update without orderId or clientOrderId",
|
|
947
|
+
),
|
|
948
|
+
{
|
|
949
|
+
accountId: record.accountId,
|
|
950
|
+
venue: record.venue,
|
|
951
|
+
symbol: snapshot.symbol,
|
|
952
|
+
},
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private warnProvisionalTerminalOrder(
|
|
957
|
+
record: OrderRecord,
|
|
958
|
+
snapshot: OrderSnapshot,
|
|
959
|
+
): void {
|
|
960
|
+
// 终态单缺 orderId 但有 clientOrderId: 用 client key provisional 存储并告警。
|
|
961
|
+
// adapter 契约要求终态带 orderId(见 adapter-contract.md);仅 cid 无法保证稳定唯一主键。
|
|
962
|
+
if (snapshot.orderId || isOpenOrder(snapshot) || !snapshot.clientOrderId) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
this.context.publishRuntimeError(
|
|
967
|
+
"order",
|
|
968
|
+
new Error(
|
|
969
|
+
"Stored terminal order without orderId using provisional clientOrderId key",
|
|
970
|
+
),
|
|
971
|
+
{
|
|
972
|
+
accountId: record.accountId,
|
|
973
|
+
venue: record.venue,
|
|
974
|
+
symbol: snapshot.symbol,
|
|
975
|
+
},
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private getSnapshotAtLocation(
|
|
980
|
+
record: OrderRecord,
|
|
981
|
+
location: OrderLocation,
|
|
982
|
+
): OrderSnapshot | undefined {
|
|
983
|
+
return this.getOrderTable(record, location.table)
|
|
984
|
+
.get(location.symbol)
|
|
985
|
+
?.get(location.key);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private getOrderTable(
|
|
989
|
+
record: OrderRecord,
|
|
990
|
+
table: OrderTable,
|
|
991
|
+
): Map<string, Map<string, OrderSnapshot>> {
|
|
992
|
+
return table === "open" ? record.openOrders : record.closedOrders;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private getOrCreateSymbolOrders(
|
|
996
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
997
|
+
symbol: string,
|
|
998
|
+
): Map<string, OrderSnapshot> {
|
|
999
|
+
const existing = table.get(symbol);
|
|
1000
|
+
if (existing) {
|
|
1001
|
+
return existing;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const created = new Map<string, OrderSnapshot>();
|
|
1005
|
+
table.set(symbol, created);
|
|
1006
|
+
return created;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private getOrCreateOrderIdSymbolIndex(
|
|
1010
|
+
record: OrderRecord,
|
|
1011
|
+
symbol: string,
|
|
1012
|
+
): Map<string, OrderLocation> {
|
|
1013
|
+
const existing = record.orderIdIndex.get(symbol);
|
|
1014
|
+
if (existing) {
|
|
1015
|
+
return existing;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const created = new Map<string, OrderLocation>();
|
|
1019
|
+
record.orderIdIndex.set(symbol, created);
|
|
1020
|
+
return created;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private getOrderIdLocation(
|
|
1024
|
+
record: OrderRecord,
|
|
1025
|
+
symbol: string,
|
|
1026
|
+
orderId: string,
|
|
1027
|
+
): OrderLocation | undefined {
|
|
1028
|
+
return record.orderIdIndex.get(symbol)?.get(orderId);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private getSnapshotsForOrderId(
|
|
1032
|
+
record: OrderRecord,
|
|
1033
|
+
orderId: string,
|
|
1034
|
+
): OrderSnapshot[] {
|
|
1035
|
+
return this.getSnapshotsForLocations(
|
|
1036
|
+
record,
|
|
1037
|
+
record.orderIdOnlyIndex.get(orderId),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private getSnapshotsForClientOrderId(
|
|
1042
|
+
record: OrderRecord,
|
|
1043
|
+
clientOrderId: string,
|
|
1044
|
+
): OrderSnapshot[] {
|
|
1045
|
+
return this.getSnapshotsForLocations(
|
|
1046
|
+
record,
|
|
1047
|
+
record.clientOrderIdIndex.get(clientOrderId),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private getSnapshotsForLocations(
|
|
1052
|
+
record: OrderRecord,
|
|
1053
|
+
locations?: Iterable<OrderLocation>,
|
|
1054
|
+
): OrderSnapshot[] {
|
|
1055
|
+
if (!locations) {
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const snapshots: OrderSnapshot[] = [];
|
|
1060
|
+
for (const location of locations) {
|
|
1061
|
+
const snapshot = this.getSnapshotAtLocation(record, location);
|
|
1062
|
+
if (snapshot) {
|
|
1063
|
+
snapshots.push(snapshot);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return snapshots;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private getOpenOrderSnapshots(record: OrderRecord): OrderSnapshot[] {
|
|
1071
|
+
return this.getSnapshotsInTable(record.openOrders);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private getAllSnapshots(record: OrderRecord): OrderSnapshot[] {
|
|
1075
|
+
return [
|
|
1076
|
+
...this.getSnapshotsInTable(record.openOrders),
|
|
1077
|
+
...this.getSnapshotsInTable(record.closedOrders),
|
|
1078
|
+
];
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private getSnapshotsInTable(
|
|
1082
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
1083
|
+
): OrderSnapshot[] {
|
|
1084
|
+
const snapshots: OrderSnapshot[] = [];
|
|
1085
|
+
for (const symbolOrders of table.values()) {
|
|
1086
|
+
snapshots.push(...symbolOrders.values());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return snapshots;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private getSnapshotCount(record: OrderRecord): number {
|
|
1093
|
+
return (
|
|
1094
|
+
this.getSnapshotCountInTable(record.openOrders) +
|
|
1095
|
+
this.getSnapshotCountInTable(record.closedOrders)
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private getSnapshotCountInTable(
|
|
1100
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
1101
|
+
): number {
|
|
1102
|
+
let size = 0;
|
|
1103
|
+
for (const symbolOrders of table.values()) {
|
|
1104
|
+
size += symbolOrders.size;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return size;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private addLocationToSetIndex(
|
|
1111
|
+
index: Map<string, Set<OrderLocation>>,
|
|
1112
|
+
key: string,
|
|
1113
|
+
location: OrderLocation,
|
|
1114
|
+
): void {
|
|
1115
|
+
this.removeLocationFromSetIndex(index, key, location);
|
|
1116
|
+
|
|
1117
|
+
const locations = index.get(key);
|
|
1118
|
+
if (locations) {
|
|
1119
|
+
locations.add(location);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
index.set(key, new Set([location]));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private removeLocationFromSetIndex(
|
|
1127
|
+
index: Map<string, Set<OrderLocation>>,
|
|
1128
|
+
key: string,
|
|
1129
|
+
location: OrderLocation,
|
|
1130
|
+
): void {
|
|
1131
|
+
const locations = index.get(key);
|
|
1132
|
+
if (!locations) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
for (const candidate of locations) {
|
|
1137
|
+
if (this.locationsEqual(candidate, location)) {
|
|
1138
|
+
locations.delete(candidate);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (locations.size === 0) {
|
|
1144
|
+
index.delete(key);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private locationsEqual(
|
|
1149
|
+
left: OrderLocation | undefined,
|
|
1150
|
+
right: OrderLocation,
|
|
1151
|
+
): boolean {
|
|
1152
|
+
return Boolean(
|
|
1153
|
+
left &&
|
|
1154
|
+
left.table === right.table &&
|
|
1155
|
+
left.symbol === right.symbol &&
|
|
1156
|
+
left.key === right.key,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private selectLatestSnapshot(
|
|
1161
|
+
snapshots: OrderSnapshot[],
|
|
1162
|
+
): OrderSnapshot | undefined {
|
|
1163
|
+
let latest: OrderSnapshot | undefined;
|
|
1164
|
+
for (const snapshot of snapshots) {
|
|
1165
|
+
if (!latest) {
|
|
1166
|
+
latest = snapshot;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const snapshotOpen = isOpenOrder(snapshot);
|
|
1171
|
+
const latestOpen = isOpenOrder(latest);
|
|
1172
|
+
if (snapshotOpen !== latestOpen) {
|
|
1173
|
+
// open 候选绝对优先:当前活跃订单优于历史终态(clientOrderId 复用时旧单已 closed)
|
|
1174
|
+
if (snapshotOpen) {
|
|
1175
|
+
latest = snapshot;
|
|
1176
|
+
}
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// 同为 open 或同为 closed: 取 updatedAt 最新。
|
|
1181
|
+
// 不能用 seq —— seq 是单订单版本号,跨订单(如复用 cid 的不同订单)不可比。
|
|
1182
|
+
if (snapshot.updatedAt > latest.updatedAt) {
|
|
1183
|
+
latest = snapshot;
|
|
1184
|
+
}
|
|
524
1185
|
}
|
|
1186
|
+
|
|
1187
|
+
return latest;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private applyUpdateToRecord(
|
|
1191
|
+
record: OrderRecord,
|
|
1192
|
+
accountId: string,
|
|
1193
|
+
venue: Venue,
|
|
1194
|
+
update: RawOrderUpdate,
|
|
1195
|
+
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
1196
|
+
): OrderSnapshot | undefined {
|
|
1197
|
+
const previous = this.getExistingSnapshot(record, update);
|
|
1198
|
+
if (
|
|
1199
|
+
!shouldApplyWatermarkedUpdate(previous, update, {
|
|
1200
|
+
requestStartedAt: options.requestStartedAt,
|
|
1201
|
+
source: options.requestStartedAt === undefined ? "stream" : "rest",
|
|
1202
|
+
})
|
|
1203
|
+
) {
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const snapshot = this.createSnapshot(accountId, venue, update, previous);
|
|
1208
|
+
return this.setSnapshot(record, snapshot, previous) ? snapshot : undefined;
|
|
525
1209
|
}
|
|
526
1210
|
|
|
527
1211
|
private createSnapshot(
|
|
@@ -531,7 +1215,13 @@ export class OrderManagerImpl
|
|
|
531
1215
|
previous?: OrderSnapshot,
|
|
532
1216
|
): OrderSnapshot {
|
|
533
1217
|
const amount = new BigNumber(input.amount);
|
|
534
|
-
const
|
|
1218
|
+
const rawFilled = new BigNumber(input.filled);
|
|
1219
|
+
const filled =
|
|
1220
|
+
previous &&
|
|
1221
|
+
input.exchangeTs !== undefined &&
|
|
1222
|
+
previous.exchangeTs === input.exchangeTs
|
|
1223
|
+
? BigNumber.maximum(rawFilled, previous.filled)
|
|
1224
|
+
: rawFilled;
|
|
535
1225
|
const remaining =
|
|
536
1226
|
input.remaining === undefined
|
|
537
1227
|
? amount.minus(filled)
|
|
@@ -540,12 +1230,12 @@ export class OrderManagerImpl
|
|
|
540
1230
|
return {
|
|
541
1231
|
accountId,
|
|
542
1232
|
venue,
|
|
543
|
-
orderId: input.orderId,
|
|
544
|
-
clientOrderId: input.clientOrderId,
|
|
1233
|
+
orderId: input.orderId ?? previous?.orderId,
|
|
1234
|
+
clientOrderId: input.clientOrderId ?? previous?.clientOrderId,
|
|
545
1235
|
symbol: input.symbol,
|
|
546
1236
|
side: input.side,
|
|
547
1237
|
type: input.type,
|
|
548
|
-
status: input
|
|
1238
|
+
status: this.mergeOrderStatus(input, previous),
|
|
549
1239
|
price:
|
|
550
1240
|
input.price === undefined ? previous?.price : toCanonical(input.price),
|
|
551
1241
|
triggerPrice:
|
|
@@ -568,6 +1258,26 @@ export class OrderManagerImpl
|
|
|
568
1258
|
};
|
|
569
1259
|
}
|
|
570
1260
|
|
|
1261
|
+
private mergeOrderStatus(
|
|
1262
|
+
input: RawOrderUpdate,
|
|
1263
|
+
previous?: OrderSnapshot,
|
|
1264
|
+
): OrderSnapshot["status"] {
|
|
1265
|
+
if (!previous) {
|
|
1266
|
+
return input.status;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (
|
|
1270
|
+
input.exchangeTs !== undefined &&
|
|
1271
|
+
previous.exchangeTs !== undefined &&
|
|
1272
|
+
input.exchangeTs === previous.exchangeTs &&
|
|
1273
|
+
orderPriority(input.status) < orderPriority(previous.status)
|
|
1274
|
+
) {
|
|
1275
|
+
return previous.status;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return input.status;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
571
1281
|
private publishStatus(record: OrderRecord): void {
|
|
572
1282
|
const event: OrderStatusChangedEvent = {
|
|
573
1283
|
type: "order.status_changed",
|
|
@@ -625,7 +1335,7 @@ export class OrderManagerImpl
|
|
|
625
1335
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
626
1336
|
const previous = this.getExistingSnapshot(record, update);
|
|
627
1337
|
const snapshot = this.createSnapshot(accountId, venue, update, previous);
|
|
628
|
-
this.setSnapshot(record
|
|
1338
|
+
this.setSnapshot(record, snapshot, previous);
|
|
629
1339
|
return snapshot;
|
|
630
1340
|
}
|
|
631
1341
|
|
|
@@ -653,7 +1363,8 @@ export class OrderManagerImpl
|
|
|
653
1363
|
symbol?: string;
|
|
654
1364
|
},
|
|
655
1365
|
): AcexError {
|
|
656
|
-
const
|
|
1366
|
+
const details = buildAcexErrorDetails(metadata);
|
|
1367
|
+
const error = new AcexError(code, message, { details });
|
|
657
1368
|
this.context.publishRuntimeError("order", error, metadata);
|
|
658
1369
|
return error;
|
|
659
1370
|
}
|
|
@@ -680,6 +1391,10 @@ export class OrderManagerImpl
|
|
|
680
1391
|
error instanceof Error ? error : new Error(message),
|
|
681
1392
|
metadata,
|
|
682
1393
|
);
|
|
683
|
-
|
|
1394
|
+
const details = buildAcexErrorDetails(metadata, error);
|
|
1395
|
+
return new AcexError(code, formatAcexErrorMessage(message, details), {
|
|
1396
|
+
cause: error,
|
|
1397
|
+
details,
|
|
1398
|
+
});
|
|
684
1399
|
}
|
|
685
1400
|
}
|