@imbingox/acex 0.3.1-beta.0 → 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 +11 -10
- package/docs/api.md +502 -1030
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +93 -22
- 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/decimal.ts +19 -0
- 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 +267 -55
- package/src/managers/market-manager.ts +261 -60
- package/src/managers/order-manager.ts +798 -84
- package/src/types/account.ts +27 -28
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +37 -12
- package/src/types/order.ts +7 -7
- 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,9 +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";
|
|
20
|
+
import { toCanonical } from "../internal/decimal.ts";
|
|
13
21
|
import { matchesOrderFilter } from "../internal/filters.ts";
|
|
22
|
+
import {
|
|
23
|
+
canDeleteMissingFromSnapshot,
|
|
24
|
+
shouldApplyWatermarkedUpdate,
|
|
25
|
+
} from "../internal/watermark.ts";
|
|
14
26
|
import type {
|
|
15
27
|
CancelAllOrdersInput,
|
|
16
28
|
CancelOrderInput,
|
|
@@ -32,15 +44,55 @@ interface OrderRecord {
|
|
|
32
44
|
accountId: string;
|
|
33
45
|
venue: Venue;
|
|
34
46
|
subscribed: boolean;
|
|
35
|
-
|
|
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>>;
|
|
36
52
|
status: OrderDataStatus;
|
|
37
53
|
}
|
|
38
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
|
+
|
|
39
69
|
function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
40
70
|
return { ...status };
|
|
41
71
|
}
|
|
42
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
|
+
|
|
43
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: {
|
|
44
96
|
orderId?: string;
|
|
45
97
|
clientOrderId?: string;
|
|
46
98
|
}): string | undefined {
|
|
@@ -55,6 +107,94 @@ function getOrderLookupKey(input: {
|
|
|
55
107
|
return undefined;
|
|
56
108
|
}
|
|
57
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
|
+
|
|
58
198
|
export class OrderManagerImpl
|
|
59
199
|
implements
|
|
60
200
|
OrderManager,
|
|
@@ -66,13 +206,17 @@ export class OrderManagerImpl
|
|
|
66
206
|
readonly events: OrderEventStreams;
|
|
67
207
|
|
|
68
208
|
private readonly context: ClientContext;
|
|
209
|
+
private readonly maxClosedOrdersPerSymbol: number;
|
|
69
210
|
private readonly orderBus = new AsyncEventBus<OrderEvent>();
|
|
70
211
|
private readonly orderStatusBus =
|
|
71
212
|
new AsyncEventBus<OrderStatusChangedEvent>();
|
|
72
213
|
private readonly records = new Map<string, OrderRecord>();
|
|
73
214
|
|
|
74
|
-
constructor(context: ClientContext) {
|
|
215
|
+
constructor(context: ClientContext, options: OrderManagerOptions = {}) {
|
|
75
216
|
this.context = context;
|
|
217
|
+
this.maxClosedOrdersPerSymbol = normalizeMaxClosedOrdersPerSymbol(
|
|
218
|
+
options.maxClosedOrdersPerSymbol,
|
|
219
|
+
);
|
|
76
220
|
|
|
77
221
|
this.events = {
|
|
78
222
|
status: (filter) =>
|
|
@@ -101,7 +245,10 @@ export class OrderManagerImpl
|
|
|
101
245
|
async subscribeOrders(input: SubscribeOrdersInput): Promise<void> {
|
|
102
246
|
this.context.assertStarted();
|
|
103
247
|
const account = this.context.getRegisteredAccount(input.accountId);
|
|
104
|
-
if (
|
|
248
|
+
if (
|
|
249
|
+
this.context.getPrivateOrderCapabilities(account.venue)?.updates ===
|
|
250
|
+
"unsupported"
|
|
251
|
+
) {
|
|
105
252
|
throw this.createError(
|
|
106
253
|
"VENUE_NOT_SUPPORTED",
|
|
107
254
|
`Venue does not support private order subscriptions: ${account.venue}`,
|
|
@@ -217,17 +364,56 @@ export class OrderManagerImpl
|
|
|
217
364
|
return undefined;
|
|
218
365
|
}
|
|
219
366
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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;
|
|
223
378
|
}
|
|
224
379
|
|
|
225
380
|
if (
|
|
226
381
|
input.clientOrderId &&
|
|
227
|
-
snapshot.clientOrderId
|
|
382
|
+
snapshot.clientOrderId !== input.clientOrderId
|
|
228
383
|
) {
|
|
229
|
-
return
|
|
384
|
+
return undefined;
|
|
230
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
|
+
);
|
|
231
417
|
}
|
|
232
418
|
|
|
233
419
|
return undefined;
|
|
@@ -239,15 +425,11 @@ export class OrderManagerImpl
|
|
|
239
425
|
return [];
|
|
240
426
|
}
|
|
241
427
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
428
|
+
if (symbol) {
|
|
429
|
+
return [...(record.openOrders.get(symbol)?.values() ?? [])];
|
|
430
|
+
}
|
|
246
431
|
|
|
247
|
-
|
|
248
|
-
snapshot.status === "open" || snapshot.status === "partially_filled"
|
|
249
|
-
);
|
|
250
|
-
});
|
|
432
|
+
return this.getOpenOrderSnapshots(record);
|
|
251
433
|
}
|
|
252
434
|
|
|
253
435
|
getOrderStatus(accountId: string): OrderDataStatus | undefined {
|
|
@@ -315,7 +497,7 @@ export class OrderManagerImpl
|
|
|
315
497
|
|
|
316
498
|
record.status = {
|
|
317
499
|
...this.createStatus(accountId, venue, "active"),
|
|
318
|
-
ready: record
|
|
500
|
+
ready: this.getSnapshotCount(record) > 0,
|
|
319
501
|
runtimeStatus: "bootstrap_pending",
|
|
320
502
|
reason: undefined,
|
|
321
503
|
lastReceivedAt: record.status.lastReceivedAt,
|
|
@@ -328,40 +510,82 @@ export class OrderManagerImpl
|
|
|
328
510
|
onPrivateOrderBootstrap(
|
|
329
511
|
accountId: string,
|
|
330
512
|
venue: Venue,
|
|
331
|
-
|
|
332
|
-
|
|
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[] {
|
|
333
525
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
334
526
|
if (!record.subscribed) {
|
|
335
|
-
return;
|
|
527
|
+
return [];
|
|
336
528
|
}
|
|
337
529
|
|
|
338
|
-
const
|
|
339
|
-
for (const update of
|
|
340
|
-
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,
|
|
341
539
|
accountId,
|
|
342
540
|
venue,
|
|
343
541
|
update,
|
|
344
|
-
|
|
542
|
+
{
|
|
543
|
+
requestStartedAt: options.requestStartedAt,
|
|
544
|
+
preserveStatus: true,
|
|
545
|
+
},
|
|
345
546
|
);
|
|
346
|
-
|
|
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
|
+
}
|
|
347
558
|
}
|
|
348
559
|
|
|
349
|
-
record.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
),
|
|
354
583
|
);
|
|
355
|
-
record.status = {
|
|
356
|
-
|
|
357
|
-
activity: "active",
|
|
358
|
-
ready: true,
|
|
359
|
-
runtimeStatus: "healthy",
|
|
360
|
-
reason: undefined,
|
|
584
|
+
record.status = successfulStatus(record.status, {
|
|
585
|
+
preserveStatus: options.preserveStatus,
|
|
361
586
|
lastReceivedAt: latestTs || record.status.lastReceivedAt,
|
|
362
587
|
lastReadyAt: latestTs || this.context.now(),
|
|
363
|
-
|
|
364
|
-
};
|
|
588
|
+
});
|
|
365
589
|
|
|
366
590
|
const event: OrderSnapshotReplacedEvent = {
|
|
367
591
|
type: "order.snapshot_replaced",
|
|
@@ -373,21 +597,37 @@ export class OrderManagerImpl
|
|
|
373
597
|
|
|
374
598
|
this.orderBus.publish(event);
|
|
375
599
|
this.publishStatus(record);
|
|
600
|
+
return disappeared;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getPrivateOpenOrders(accountId: string): OrderSnapshot[] {
|
|
604
|
+
return this.getOpenOrders(accountId);
|
|
376
605
|
}
|
|
377
606
|
|
|
378
607
|
onPrivateOrderUpdate(
|
|
379
608
|
accountId: string,
|
|
380
609
|
venue: Venue,
|
|
381
610
|
update: RawOrderUpdate,
|
|
611
|
+
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
382
612
|
): void {
|
|
383
613
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
384
614
|
if (!record.subscribed) {
|
|
385
615
|
return;
|
|
386
616
|
}
|
|
387
617
|
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
631
|
|
|
392
632
|
const eventType =
|
|
393
633
|
snapshot.status === "filled"
|
|
@@ -407,16 +647,11 @@ export class OrderManagerImpl
|
|
|
407
647
|
ts: this.context.now(),
|
|
408
648
|
});
|
|
409
649
|
|
|
410
|
-
record.status = {
|
|
411
|
-
|
|
412
|
-
activity: "active",
|
|
413
|
-
ready: true,
|
|
414
|
-
runtimeStatus: "healthy",
|
|
415
|
-
reason: undefined,
|
|
650
|
+
record.status = successfulStatus(record.status, {
|
|
651
|
+
preserveStatus: options.preserveStatus,
|
|
416
652
|
lastReceivedAt: snapshot.receivedAt,
|
|
417
653
|
lastReadyAt: snapshot.updatedAt,
|
|
418
|
-
|
|
419
|
-
};
|
|
654
|
+
});
|
|
420
655
|
this.publishStatus(record);
|
|
421
656
|
}
|
|
422
657
|
|
|
@@ -467,7 +702,11 @@ export class OrderManagerImpl
|
|
|
467
702
|
accountId,
|
|
468
703
|
venue,
|
|
469
704
|
subscribed: false,
|
|
470
|
-
|
|
705
|
+
openOrders: new Map(),
|
|
706
|
+
closedOrders: new Map(),
|
|
707
|
+
orderIdIndex: new Map(),
|
|
708
|
+
orderIdOnlyIndex: new Map(),
|
|
709
|
+
clientOrderIdIndex: new Map(),
|
|
471
710
|
status: this.createStatus(accountId, venue, "inactive"),
|
|
472
711
|
};
|
|
473
712
|
|
|
@@ -491,18 +730,40 @@ export class OrderManagerImpl
|
|
|
491
730
|
|
|
492
731
|
private getExistingSnapshot(
|
|
493
732
|
record: OrderRecord,
|
|
494
|
-
update: { orderId?: string; clientOrderId?: string },
|
|
733
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
495
734
|
): OrderSnapshot | undefined {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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;
|
|
499
754
|
}
|
|
755
|
+
}
|
|
500
756
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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;
|
|
506
767
|
}
|
|
507
768
|
}
|
|
508
769
|
|
|
@@ -510,17 +771,441 @@ export class OrderManagerImpl
|
|
|
510
771
|
}
|
|
511
772
|
|
|
512
773
|
private setSnapshot(
|
|
513
|
-
|
|
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,
|
|
514
937
|
snapshot: OrderSnapshot,
|
|
515
938
|
): void {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
}
|
|
523
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;
|
|
524
1209
|
}
|
|
525
1210
|
|
|
526
1211
|
private createSnapshot(
|
|
@@ -530,7 +1215,13 @@ export class OrderManagerImpl
|
|
|
530
1215
|
previous?: OrderSnapshot,
|
|
531
1216
|
): OrderSnapshot {
|
|
532
1217
|
const amount = new BigNumber(input.amount);
|
|
533
|
-
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;
|
|
534
1225
|
const remaining =
|
|
535
1226
|
input.remaining === undefined
|
|
536
1227
|
? amount.minus(filled)
|
|
@@ -539,29 +1230,27 @@ export class OrderManagerImpl
|
|
|
539
1230
|
return {
|
|
540
1231
|
accountId,
|
|
541
1232
|
venue,
|
|
542
|
-
orderId: input.orderId,
|
|
543
|
-
clientOrderId: input.clientOrderId,
|
|
1233
|
+
orderId: input.orderId ?? previous?.orderId,
|
|
1234
|
+
clientOrderId: input.clientOrderId ?? previous?.clientOrderId,
|
|
544
1235
|
symbol: input.symbol,
|
|
545
1236
|
side: input.side,
|
|
546
1237
|
type: input.type,
|
|
547
|
-
status: input
|
|
1238
|
+
status: this.mergeOrderStatus(input, previous),
|
|
548
1239
|
price:
|
|
549
|
-
input.price === undefined
|
|
550
|
-
? previous?.price
|
|
551
|
-
: new BigNumber(input.price),
|
|
1240
|
+
input.price === undefined ? previous?.price : toCanonical(input.price),
|
|
552
1241
|
triggerPrice:
|
|
553
1242
|
input.triggerPrice === undefined
|
|
554
1243
|
? previous?.triggerPrice
|
|
555
|
-
:
|
|
556
|
-
amount,
|
|
557
|
-
filled,
|
|
558
|
-
remaining,
|
|
1244
|
+
: toCanonical(input.triggerPrice),
|
|
1245
|
+
amount: toCanonical(amount),
|
|
1246
|
+
filled: toCanonical(filled),
|
|
1247
|
+
remaining: toCanonical(remaining),
|
|
559
1248
|
reduceOnly: input.reduceOnly ?? previous?.reduceOnly,
|
|
560
1249
|
positionSide: input.positionSide ?? previous?.positionSide,
|
|
561
1250
|
avgFillPrice:
|
|
562
1251
|
input.avgFillPrice === undefined
|
|
563
1252
|
? previous?.avgFillPrice
|
|
564
|
-
:
|
|
1253
|
+
: toCanonical(input.avgFillPrice),
|
|
565
1254
|
exchangeTs: input.exchangeTs,
|
|
566
1255
|
receivedAt: input.receivedAt,
|
|
567
1256
|
updatedAt: input.receivedAt,
|
|
@@ -569,6 +1258,26 @@ export class OrderManagerImpl
|
|
|
569
1258
|
};
|
|
570
1259
|
}
|
|
571
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
|
+
|
|
572
1281
|
private publishStatus(record: OrderRecord): void {
|
|
573
1282
|
const event: OrderStatusChangedEvent = {
|
|
574
1283
|
type: "order.status_changed",
|
|
@@ -626,7 +1335,7 @@ export class OrderManagerImpl
|
|
|
626
1335
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
627
1336
|
const previous = this.getExistingSnapshot(record, update);
|
|
628
1337
|
const snapshot = this.createSnapshot(accountId, venue, update, previous);
|
|
629
|
-
this.setSnapshot(record
|
|
1338
|
+
this.setSnapshot(record, snapshot, previous);
|
|
630
1339
|
return snapshot;
|
|
631
1340
|
}
|
|
632
1341
|
|
|
@@ -654,7 +1363,8 @@ export class OrderManagerImpl
|
|
|
654
1363
|
symbol?: string;
|
|
655
1364
|
},
|
|
656
1365
|
): AcexError {
|
|
657
|
-
const
|
|
1366
|
+
const details = buildAcexErrorDetails(metadata);
|
|
1367
|
+
const error = new AcexError(code, message, { details });
|
|
658
1368
|
this.context.publishRuntimeError("order", error, metadata);
|
|
659
1369
|
return error;
|
|
660
1370
|
}
|
|
@@ -681,6 +1391,10 @@ export class OrderManagerImpl
|
|
|
681
1391
|
error instanceof Error ? error : new Error(message),
|
|
682
1392
|
metadata,
|
|
683
1393
|
);
|
|
684
|
-
|
|
1394
|
+
const details = buildAcexErrorDetails(metadata, error);
|
|
1395
|
+
return new AcexError(code, formatAcexErrorMessage(message, details), {
|
|
1396
|
+
cause: error,
|
|
1397
|
+
details,
|
|
1398
|
+
});
|
|
685
1399
|
}
|
|
686
1400
|
}
|