@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.11

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