@imbingox/acex 0.4.0-beta.12 → 0.4.0-beta.14

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.
@@ -1,4 +1,3 @@
1
- import BigNumber from "bignumber.js";
2
1
  import type {
3
2
  RawOpenOrdersSnapshot,
4
3
  RawOrderUpdate,
@@ -17,7 +16,6 @@ import {
17
16
  formatAcexErrorMessage,
18
17
  } from "../errors.ts";
19
18
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
20
- import { toCanonical } from "../internal/decimal.ts";
21
19
  import { matchesOrderFilter } from "../internal/filters.ts";
22
20
  import { isTransportError } from "../internal/http-client.ts";
23
21
  import {
@@ -40,170 +38,41 @@ import type {
40
38
  UnsubscribeOrdersInput,
41
39
  Venue,
42
40
  } from "../types/index.ts";
43
-
44
- interface OrderRecord {
45
- accountId: string;
46
- venue: Venue;
47
- subscribed: boolean;
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>;
55
- status: OrderDataStatus;
56
- }
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
-
85
- function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
86
- return { ...status };
87
- }
88
-
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;
97
- orderId?: string;
98
- clientOrderId?: string;
99
- }): string[] {
100
- const keys: string[] = [];
101
- if (input.orderId) {
102
- keys.push(`symbol:${input.symbol}:order:${input.orderId}`);
103
- }
104
-
105
- if (input.clientOrderId) {
106
- keys.push(`symbol:${input.symbol}:client:${input.clientOrderId}`);
107
- }
108
-
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
- }
206
- }
41
+ import {
42
+ cloneOrderStatus,
43
+ createOrderDataStatus,
44
+ normalizeMaxClosedOrdersPerSymbol,
45
+ successfulStatus,
46
+ } from "./order/data-status.ts";
47
+ import {
48
+ getOrderLookupKeys,
49
+ isSystemClientOrderId,
50
+ SDK_CLIENT_ORDER_ID_PREFIX,
51
+ shouldMatchOrderQuery,
52
+ VENUE_CLIENT_ORDER_ID_PATTERN,
53
+ } from "./order/identity.ts";
54
+ import type {
55
+ OrderLocation,
56
+ OrderManagerOptions,
57
+ OrderRecord,
58
+ } from "./order/model.ts";
59
+ import { createSnapshot, isOpenOrder } from "./order/snapshot.ts";
60
+ import {
61
+ getAllSnapshots,
62
+ getExistingSnapshot,
63
+ getLocalOrderIdForVenueOrderId,
64
+ getLocationByLocalOrderId,
65
+ getOpenOrderSnapshots,
66
+ getSnapshotAtLocation,
67
+ getSnapshotByLocalOrderId,
68
+ getSnapshotCount,
69
+ getSnapshotsForClientOrderId,
70
+ getSnapshotsForOrderId,
71
+ isVenueClientOrderIdInUseForOpenOrder,
72
+ resolveLocalOrderIdForUpdate,
73
+ selectLatestSnapshot,
74
+ setSnapshot,
75
+ } from "./order/store.ts";
207
76
 
208
77
  export class OrderManagerImpl
209
78
  implements
@@ -320,13 +189,25 @@ export class OrderManagerImpl
320
189
  ...input,
321
190
  clientOrderId: venueClientOrderId,
322
191
  };
192
+ const requestStartedAt = this.context.now();
323
193
  const update = await this.context.createOrder(commandInput);
324
194
  const snapshot = this.applyCommandUpdate(
325
195
  input.accountId,
326
196
  account.venue,
327
197
  update,
328
- { localOrderId },
198
+ { localOrderId, requestStartedAt },
329
199
  );
200
+ if (!snapshot) {
201
+ throw this.createError(
202
+ "ORDER_CREATE_FAILED",
203
+ `Failed to store created order snapshot for ${input.accountId}: ${input.symbol}`,
204
+ {
205
+ accountId: input.accountId,
206
+ venue: account.venue,
207
+ symbol: input.symbol,
208
+ },
209
+ );
210
+ }
330
211
  this.clearPendingClientOrderClaim(
331
212
  record,
332
213
  venueClientOrderId,
@@ -361,8 +242,29 @@ export class OrderManagerImpl
361
242
  this.validateCancelOrderInput(input, account.venue);
362
243
 
363
244
  try {
245
+ const requestStartedAt = this.context.now();
364
246
  const update = await this.context.cancelOrder(input);
365
- return this.applyCommandUpdate(input.accountId, account.venue, update);
247
+ const snapshot = this.applyCommandUpdate(
248
+ input.accountId,
249
+ account.venue,
250
+ update,
251
+ {
252
+ requestStartedAt,
253
+ },
254
+ );
255
+ if (!snapshot) {
256
+ throw this.createError(
257
+ "ORDER_CANCEL_FAILED",
258
+ `Failed to store canceled order snapshot for ${input.accountId}: ${input.symbol}`,
259
+ {
260
+ accountId: input.accountId,
261
+ venue: account.venue,
262
+ symbol: input.symbol,
263
+ },
264
+ );
265
+ }
266
+
267
+ return snapshot;
366
268
  } catch (error) {
367
269
  throw this.wrapCommandError(
368
270
  "ORDER_CANCEL_FAILED",
@@ -383,8 +285,29 @@ export class OrderManagerImpl
383
285
  this.context.ensurePrivateCredentials(input.accountId);
384
286
 
385
287
  try {
288
+ const requestStartedAt = this.context.now();
386
289
  const updates = await this.context.cancelAllOrders(input);
387
- return this.applyCommandUpdates(input.accountId, account.venue, updates);
290
+ const snapshots = this.applyCommandUpdates(
291
+ input.accountId,
292
+ account.venue,
293
+ updates,
294
+ {
295
+ requestStartedAt,
296
+ },
297
+ );
298
+ if (!snapshots) {
299
+ throw this.createError(
300
+ "ORDER_CANCEL_ALL_FAILED",
301
+ `Failed to store canceled order snapshots for ${input.accountId}: ${input.symbol}`,
302
+ {
303
+ accountId: input.accountId,
304
+ venue: account.venue,
305
+ symbol: input.symbol,
306
+ },
307
+ );
308
+ }
309
+
310
+ return snapshots;
388
311
  } catch (error) {
389
312
  throw this.wrapCommandError(
390
313
  "ORDER_CANCEL_ALL_FAILED",
@@ -410,13 +333,13 @@ export class OrderManagerImpl
410
333
  }
411
334
 
412
335
  if (input.symbol && input.orderId) {
413
- const localOrderId = this.getLocalOrderIdForVenueOrderId(
336
+ const localOrderId = getLocalOrderIdForVenueOrderId(
414
337
  record,
415
338
  input.symbol,
416
339
  input.orderId,
417
340
  );
418
341
  const snapshot = localOrderId
419
- ? this.getSnapshotByLocalOrderId(record, localOrderId)
342
+ ? getSnapshotByLocalOrderId(record, localOrderId)
420
343
  : undefined;
421
344
  if (!snapshot) {
422
345
  return undefined;
@@ -433,16 +356,16 @@ export class OrderManagerImpl
433
356
  }
434
357
 
435
358
  if (input.orderId) {
436
- return this.selectLatestSnapshot(
437
- this.getSnapshotsForOrderId(record, input.orderId).filter((snapshot) =>
359
+ return selectLatestSnapshot(
360
+ getSnapshotsForOrderId(record, input.orderId).filter((snapshot) =>
438
361
  shouldMatchOrderQuery(snapshot, input),
439
362
  ),
440
363
  );
441
364
  }
442
365
 
443
366
  if (input.clientOrderId) {
444
- return this.selectLatestSnapshot(
445
- this.getSnapshotsForClientOrderId(record, input.clientOrderId).filter(
367
+ return selectLatestSnapshot(
368
+ getSnapshotsForClientOrderId(record, input.clientOrderId).filter(
446
369
  (snapshot) => shouldMatchOrderQuery(snapshot, input),
447
370
  ),
448
371
  );
@@ -457,11 +380,7 @@ export class OrderManagerImpl
457
380
  return [];
458
381
  }
459
382
 
460
- if (symbol) {
461
- return [...(record.openOrders.get(symbol)?.values() ?? [])];
462
- }
463
-
464
- return this.getOpenOrderSnapshots(record);
383
+ return getOpenOrderSnapshots(record, symbol);
465
384
  }
466
385
 
467
386
  getOrderStatus(accountId: string): OrderDataStatus | undefined {
@@ -528,8 +447,8 @@ export class OrderManagerImpl
528
447
  }
529
448
 
530
449
  record.status = {
531
- ...this.createStatus(accountId, venue, "active"),
532
- ready: this.getSnapshotCount(record) > 0,
450
+ ...createOrderDataStatus(accountId, venue, "active"),
451
+ ready: getSnapshotCount(record) > 0,
533
452
  runtimeStatus: "bootstrap_pending",
534
453
  reason: undefined,
535
454
  lastReceivedAt: record.status.lastReceivedAt,
@@ -564,7 +483,7 @@ export class OrderManagerImpl
564
483
  for (const lookupKey of getOrderLookupKeys(update)) {
565
484
  openSetKeys.add(lookupKey);
566
485
  }
567
- const current = this.getExistingSnapshot(record, update);
486
+ const current = getExistingSnapshot(record, update);
568
487
  const nextSnapshot = this.applyUpdateToRecord(
569
488
  record,
570
489
  accountId,
@@ -586,7 +505,7 @@ export class OrderManagerImpl
586
505
  }
587
506
  }
588
507
 
589
- const disappeared = this.getOpenOrderSnapshots(record).filter((order) => {
508
+ const disappeared = getOpenOrderSnapshots(record).filter((order) => {
590
509
  if (!isOpenOrder(order)) {
591
510
  return false;
592
511
  }
@@ -605,7 +524,7 @@ export class OrderManagerImpl
605
524
  });
606
525
  });
607
526
 
608
- const orderedSnapshots = this.getAllSnapshots(record);
527
+ const orderedSnapshots = getAllSnapshots(record);
609
528
  const latestTs = Math.max(
610
529
  snapshot.snapshotReceivedAt,
611
530
  orderedSnapshots.reduce(
@@ -741,45 +660,13 @@ export class OrderManagerImpl
741
660
  orderIdOnlyIndex: new Map(),
742
661
  clientOrderIdIndex: new Map(),
743
662
  pendingClientOrderIdIndex: new Map(),
744
- status: this.createStatus(accountId, venue, "inactive"),
663
+ status: createOrderDataStatus(accountId, venue, "inactive"),
745
664
  };
746
665
 
747
666
  this.records.set(accountId, record);
748
667
  return record;
749
668
  }
750
669
 
751
- private createStatus(
752
- accountId: string,
753
- venue: Venue,
754
- activity: "active" | "inactive",
755
- ): OrderDataStatus {
756
- return {
757
- accountId,
758
- venue,
759
- activity,
760
- ready: false,
761
- runtimeStatus: activity === "active" ? "bootstrap_pending" : "stopped",
762
- };
763
- }
764
-
765
- private getExistingSnapshot(
766
- record: OrderRecord,
767
- update: { symbol: string; orderId?: string; clientOrderId?: string },
768
- ): OrderSnapshot | undefined {
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
670
  private resolveLocalOrderIdForUpdate(
784
671
  record: OrderRecord,
785
672
  update: { symbol: string; orderId?: string; clientOrderId?: string },
@@ -788,203 +675,40 @@ export class OrderManagerImpl
788
675
  localOrderId?: string;
789
676
  source?: "exact" | "pending" | "provisional" | "preferred";
790
677
  } {
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" };
799
- }
800
- }
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 {};
678
+ const pending =
679
+ update.clientOrderId === undefined
680
+ ? undefined
681
+ : record.pendingClientOrderIdIndex.get(update.clientOrderId);
682
+
683
+ return resolveLocalOrderIdForUpdate(record, update, {
684
+ preferredLocalOrderId,
685
+ pendingLocalOrderId:
686
+ pending?.symbol === update.symbol ? pending.localOrderId : undefined,
687
+ });
830
688
  }
831
689
 
832
- private setSnapshot(
690
+ private writeSnapshot(
833
691
  record: OrderRecord,
834
692
  localOrderId: string,
835
693
  snapshot: OrderSnapshot,
836
694
  previousLocation?: OrderLocation,
837
- ): OrderLocation | undefined {
695
+ ): boolean {
838
696
  if (!snapshot.orderId && !snapshot.clientOrderId) {
839
697
  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
- );
698
+ return false;
884
699
  }
885
700
 
886
- if (snapshot.clientOrderId) {
887
- this.addLocalOrderIdToSetIndex(
888
- record.clientOrderIdIndex,
889
- snapshot.clientOrderId,
890
- localOrderId,
891
- );
701
+ const result = setSnapshot(record, localOrderId, snapshot, {
702
+ maxClosedOrdersPerSymbol: this.maxClosedOrdersPerSymbol,
703
+ previousLocation,
704
+ });
705
+ if (!result.location) {
706
+ return false;
892
707
  }
893
708
 
894
- this.trimClosedOrdersForSymbol(record, location);
895
709
  this.warnSystemClientOrderIdOnlyClaim(record, snapshot);
896
710
  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);
919
- if (
920
- symbolIndex?.get(snapshot.orderId) &&
921
- symbolIndex.get(snapshot.orderId) === location.localOrderId
922
- ) {
923
- symbolIndex.delete(snapshot.orderId);
924
- }
925
- if (symbolIndex?.size === 0) {
926
- record.orderIdIndex.delete(location.symbol);
927
- }
928
- this.removeLocalOrderIdFromSetIndex(
929
- record.orderIdOnlyIndex,
930
- snapshot.orderId,
931
- location.localOrderId,
932
- );
933
- }
934
-
935
- if (snapshot.clientOrderId) {
936
- this.removeLocalOrderIdFromSetIndex(
937
- record.clientOrderIdIndex,
938
- snapshot.clientOrderId,
939
- location.localOrderId,
940
- );
941
- }
942
-
943
- return snapshot;
944
- }
945
-
946
- private moveSnapshot(
947
- record: OrderRecord,
948
- previousLocation: OrderLocation,
949
- localOrderId: string,
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,
959
- ): void {
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);
987
- }
711
+ return true;
988
712
  }
989
713
 
990
714
  private warnDroppedUnkeyedTerminalOrder(
@@ -1015,7 +739,7 @@ export class OrderManagerImpl
1015
739
  if (
1016
740
  snapshot.orderId ||
1017
741
  !snapshot.clientOrderId ||
1018
- !this.isSystemClientOrderId(snapshot.clientOrderId)
742
+ !isSystemClientOrderId(snapshot.clientOrderId)
1019
743
  ) {
1020
744
  return;
1021
745
  }
@@ -1058,211 +782,6 @@ export class OrderManagerImpl
1058
782
  );
1059
783
  }
1060
784
 
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
785
  private applyUpdateToRecord(
1267
786
  record: OrderRecord,
1268
787
  accountId: string,
@@ -1272,9 +791,9 @@ export class OrderManagerImpl
1272
791
  ): OrderSnapshot | undefined {
1273
792
  const resolution = this.resolveLocalOrderIdForUpdate(record, update);
1274
793
  const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
1275
- const previousLocation = record.localOrderLocations.get(localOrderId);
794
+ const previousLocation = getLocationByLocalOrderId(record, localOrderId);
1276
795
  const previous = previousLocation
1277
- ? this.getSnapshotAtLocation(record, previousLocation)
796
+ ? getSnapshotAtLocation(record, previousLocation)
1278
797
  : undefined;
1279
798
  if (
1280
799
  !shouldApplyWatermarkedUpdate(previous, update, {
@@ -1285,14 +804,14 @@ export class OrderManagerImpl
1285
804
  return undefined;
1286
805
  }
1287
806
 
1288
- const snapshot = this.createSnapshot(accountId, venue, update, previous);
1289
- const location = this.setSnapshot(
807
+ const snapshot = createSnapshot(accountId, venue, update, previous);
808
+ const written = this.writeSnapshot(
1290
809
  record,
1291
810
  localOrderId,
1292
811
  snapshot,
1293
812
  previousLocation,
1294
813
  );
1295
- if (location && resolution.source === "pending" && update.clientOrderId) {
814
+ if (written && resolution.source === "pending" && update.clientOrderId) {
1296
815
  this.clearPendingClientOrderClaim(
1297
816
  record,
1298
817
  update.clientOrderId,
@@ -1300,77 +819,7 @@ export class OrderManagerImpl
1300
819
  );
1301
820
  }
1302
821
 
1303
- return location ? snapshot : undefined;
1304
- }
1305
-
1306
- private createSnapshot(
1307
- accountId: string,
1308
- venue: Venue,
1309
- input: RawOrderUpdate,
1310
- previous?: OrderSnapshot,
1311
- ): OrderSnapshot {
1312
- const amount = new BigNumber(input.amount);
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;
1320
- const remaining =
1321
- input.remaining === undefined
1322
- ? amount.minus(filled)
1323
- : new BigNumber(input.remaining);
1324
-
1325
- return {
1326
- accountId,
1327
- venue,
1328
- orderId: input.orderId ?? previous?.orderId,
1329
- clientOrderId: input.clientOrderId ?? previous?.clientOrderId,
1330
- symbol: input.symbol,
1331
- side: input.side,
1332
- type: input.type,
1333
- status: this.mergeOrderStatus(input, previous),
1334
- price:
1335
- input.price === undefined ? previous?.price : toCanonical(input.price),
1336
- triggerPrice:
1337
- input.triggerPrice === undefined
1338
- ? previous?.triggerPrice
1339
- : toCanonical(input.triggerPrice),
1340
- amount: toCanonical(amount),
1341
- filled: toCanonical(filled),
1342
- remaining: toCanonical(remaining),
1343
- reduceOnly: input.reduceOnly ?? previous?.reduceOnly,
1344
- positionSide: input.positionSide ?? previous?.positionSide,
1345
- avgFillPrice:
1346
- input.avgFillPrice === undefined
1347
- ? previous?.avgFillPrice
1348
- : toCanonical(input.avgFillPrice),
1349
- exchangeTs: input.exchangeTs,
1350
- receivedAt: input.receivedAt,
1351
- updatedAt: input.receivedAt,
1352
- seq: (previous?.seq ?? 0) + 1,
1353
- };
1354
- }
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;
822
+ return written ? snapshot : undefined;
1374
823
  }
1375
824
 
1376
825
  private publishStatus(record: OrderRecord): void {
@@ -1395,10 +844,7 @@ export class OrderManagerImpl
1395
844
  if (
1396
845
  (options?.record &&
1397
846
  options.avoidOpenClientOrderId &&
1398
- this.isVenueClientOrderIdInUseForOpenOrder(
1399
- options.record,
1400
- candidate,
1401
- )) ||
847
+ isVenueClientOrderIdInUseForOpenOrder(options.record, candidate)) ||
1402
848
  options?.record?.pendingClientOrderIdIndex.has(candidate) ||
1403
849
  !VENUE_CLIENT_ORDER_ID_PATTERN.test(candidate)
1404
850
  ) {
@@ -1409,22 +855,6 @@ export class OrderManagerImpl
1409
855
  }
1410
856
  }
1411
857
 
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
858
  private addPendingClientOrderClaim(
1429
859
  record: OrderRecord,
1430
860
  symbol: string,
@@ -1452,12 +882,6 @@ export class OrderManagerImpl
1452
882
  return isTransportError(error) && error.kind === "timeout";
1453
883
  }
1454
884
 
1455
- private isSystemClientOrderId(clientOrderId: string): boolean {
1456
- return SYSTEM_CLIENT_ORDER_ID_PATTERNS.some((pattern) =>
1457
- pattern.test(clientOrderId),
1458
- );
1459
- }
1460
-
1461
885
  private validateCreateOrderInput(
1462
886
  input: CreateOrderInput,
1463
887
  venue: Venue,
@@ -1513,8 +937,8 @@ export class OrderManagerImpl
1513
937
  accountId: string,
1514
938
  venue: Venue,
1515
939
  update: RawOrderUpdate,
1516
- options: { localOrderId?: string } = {},
1517
- ): OrderSnapshot {
940
+ options: { localOrderId?: string; requestStartedAt?: number } = {},
941
+ ): OrderSnapshot | undefined {
1518
942
  const record = this.getOrCreateRecord(accountId, venue);
1519
943
  const resolution = this.resolveLocalOrderIdForUpdate(
1520
944
  record,
@@ -1522,23 +946,47 @@ export class OrderManagerImpl
1522
946
  options.localOrderId,
1523
947
  );
1524
948
  const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
1525
- const previousLocation = record.localOrderLocations.get(localOrderId);
949
+ const previousLocation = getLocationByLocalOrderId(record, localOrderId);
1526
950
  const previous = previousLocation
1527
- ? this.getSnapshotAtLocation(record, previousLocation)
951
+ ? getSnapshotAtLocation(record, previousLocation)
952
+ : undefined;
953
+ if (
954
+ previous &&
955
+ !shouldApplyWatermarkedUpdate(previous, update, {
956
+ requestStartedAt: options.requestStartedAt,
957
+ source: "command",
958
+ })
959
+ ) {
960
+ return previous;
961
+ }
962
+
963
+ const snapshot = createSnapshot(accountId, venue, update, previous);
964
+ return this.writeSnapshot(record, localOrderId, snapshot, previousLocation)
965
+ ? snapshot
1528
966
  : undefined;
1529
- const snapshot = this.createSnapshot(accountId, venue, update, previous);
1530
- this.setSnapshot(record, localOrderId, snapshot, previousLocation);
1531
- return snapshot;
1532
967
  }
1533
968
 
1534
969
  private applyCommandUpdates(
1535
970
  accountId: string,
1536
971
  venue: Venue,
1537
972
  updates: RawOrderUpdate[],
1538
- ): OrderSnapshot[] {
1539
- return updates.map((update) =>
1540
- this.applyCommandUpdate(accountId, venue, update),
1541
- );
973
+ options: { requestStartedAt?: number } = {},
974
+ ): OrderSnapshot[] | undefined {
975
+ const snapshots: OrderSnapshot[] = [];
976
+ for (const update of updates) {
977
+ const snapshot = this.applyCommandUpdate(
978
+ accountId,
979
+ venue,
980
+ update,
981
+ options,
982
+ );
983
+ if (!snapshot) {
984
+ return undefined;
985
+ }
986
+ snapshots.push(snapshot);
987
+ }
988
+
989
+ return snapshots;
1542
990
  }
1543
991
 
1544
992
  private createError(