@imbingox/acex 0.1.0 → 0.3.0-beta.0

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.
Files changed (85) hide show
  1. package/README.md +96 -285
  2. package/index.ts +1 -0
  3. package/package.json +40 -23
  4. package/src/adapters/binance/adapter.ts +80 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/mark-price.ts +126 -0
  7. package/src/adapters/binance/market-catalog.ts +258 -0
  8. package/src/adapters/binance/private-adapter.ts +833 -0
  9. package/src/adapters/types.ts +219 -0
  10. package/src/client/context.ts +123 -0
  11. package/src/client/create-client.ts +6 -0
  12. package/src/client/private-subscription-coordinator.ts +512 -0
  13. package/src/client/runtime.ts +410 -0
  14. package/src/errors.ts +27 -0
  15. package/src/index.ts +5 -0
  16. package/src/internal/async-event-bus.ts +100 -0
  17. package/src/internal/filters.ts +117 -0
  18. package/src/internal/managed-websocket.ts +280 -0
  19. package/src/managers/account-manager.ts +609 -0
  20. package/src/managers/market-manager.ts +912 -0
  21. package/src/managers/order-manager.ts +685 -0
  22. package/src/types/account.ts +157 -0
  23. package/src/types/client.ts +79 -0
  24. package/src/types/index.ts +5 -0
  25. package/src/types/market.ts +152 -0
  26. package/src/types/order.ts +177 -0
  27. package/src/types/shared.ts +93 -0
  28. package/dist/adapters/binance/composite-adapter.d.ts +0 -116
  29. package/dist/adapters/binance/composite-adapter.js +0 -121
  30. package/dist/adapters/binance/market-types.d.ts +0 -63
  31. package/dist/adapters/binance/market-types.js +0 -1
  32. package/dist/adapters/binance/native-market-adapter.d.ts +0 -102
  33. package/dist/adapters/binance/native-market-adapter.js +0 -455
  34. package/dist/adapters/binance/normalizers.d.ts +0 -8
  35. package/dist/adapters/binance/normalizers.js +0 -123
  36. package/dist/adapters/binance/rest-client.d.ts +0 -17
  37. package/dist/adapters/binance/rest-client.js +0 -66
  38. package/dist/adapters/binance/symbol-router.d.ts +0 -9
  39. package/dist/adapters/binance/symbol-router.js +0 -174
  40. package/dist/adapters/binance/ws-client.d.ts +0 -24
  41. package/dist/adapters/binance/ws-client.js +0 -261
  42. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  43. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  44. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -180
  45. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -539
  46. package/dist/adapters/ccxt/binance-usdm-exchange.d.ts +0 -22
  47. package/dist/adapters/ccxt/binance-usdm-exchange.js +0 -23
  48. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  49. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  50. package/dist/adapters/types.d.ts +0 -210
  51. package/dist/adapters/types.js +0 -1
  52. package/dist/core/client.d.ts +0 -50
  53. package/dist/core/client.js +0 -403
  54. package/dist/core/recovery.d.ts +0 -22
  55. package/dist/core/recovery.js +0 -18
  56. package/dist/core/runtime.d.ts +0 -26
  57. package/dist/core/runtime.js +0 -150
  58. package/dist/errors/acex-error.d.ts +0 -25
  59. package/dist/errors/acex-error.js +0 -54
  60. package/dist/index.d.ts +0 -6
  61. package/dist/index.js +0 -3
  62. package/dist/managers/account-manager.d.ts +0 -41
  63. package/dist/managers/account-manager.js +0 -80
  64. package/dist/managers/market-manager.d.ts +0 -16
  65. package/dist/managers/market-manager.js +0 -28
  66. package/dist/managers/order-manager.d.ts +0 -87
  67. package/dist/managers/order-manager.js +0 -122
  68. package/dist/runtime/async-queue.d.ts +0 -8
  69. package/dist/runtime/async-queue.js +0 -88
  70. package/dist/runtime/request-id.d.ts +0 -1
  71. package/dist/runtime/request-id.js +0 -5
  72. package/dist/runtime/ws-connection-supervisor.d.ts +0 -76
  73. package/dist/runtime/ws-connection-supervisor.js +0 -522
  74. package/dist/store/account-store.d.ts +0 -52
  75. package/dist/store/account-store.js +0 -18
  76. package/dist/store/health-store.d.ts +0 -16
  77. package/dist/store/health-store.js +0 -29
  78. package/dist/store/market-store.d.ts +0 -42
  79. package/dist/store/market-store.js +0 -51
  80. package/dist/store/order-store.d.ts +0 -38
  81. package/dist/store/order-store.js +0 -49
  82. package/dist/testing/create-fake-runtime.d.ts +0 -5
  83. package/dist/testing/create-fake-runtime.js +0 -7
  84. package/dist/types/public.d.ts +0 -5
  85. package/dist/types/public.js +0 -1
@@ -0,0 +1,685 @@
1
+ import BigNumber from "bignumber.js";
2
+ import type { RawOrderUpdate } from "../adapters/types.ts";
3
+ import type {
4
+ AccountAwareManager,
5
+ ClientContext,
6
+ HealthReporter,
7
+ ManagerLifecycle,
8
+ PrivateOrderDataConsumer,
9
+ PrivateSubscriptionState,
10
+ } from "../client/context.ts";
11
+ import { AcexError } from "../errors.ts";
12
+ import { AsyncEventBus } from "../internal/async-event-bus.ts";
13
+ import { matchesOrderFilter } from "../internal/filters.ts";
14
+ import type {
15
+ CancelAllOrdersInput,
16
+ CancelOrderInput,
17
+ CreateOrderInput,
18
+ Exchange,
19
+ GetOrderInput,
20
+ OrderDataStatus,
21
+ OrderEvent,
22
+ OrderEventStreams,
23
+ OrderManager,
24
+ OrderSnapshot,
25
+ OrderSnapshotReplacedEvent,
26
+ OrderStatusChangedEvent,
27
+ SubscribeOrdersInput,
28
+ UnsubscribeOrdersInput,
29
+ } from "../types/index.ts";
30
+
31
+ interface OrderRecord {
32
+ accountId: string;
33
+ exchange: Exchange;
34
+ subscribed: boolean;
35
+ snapshots: Map<string, OrderSnapshot>;
36
+ status: OrderDataStatus;
37
+ }
38
+
39
+ function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
40
+ return { ...status };
41
+ }
42
+
43
+ function getOrderLookupKey(input: {
44
+ orderId?: string;
45
+ clientOrderId?: string;
46
+ }): string | undefined {
47
+ if (input.orderId) {
48
+ return `order:${input.orderId}`;
49
+ }
50
+
51
+ if (input.clientOrderId) {
52
+ return `client:${input.clientOrderId}`;
53
+ }
54
+
55
+ return undefined;
56
+ }
57
+
58
+ export class OrderManagerImpl
59
+ implements
60
+ OrderManager,
61
+ ManagerLifecycle,
62
+ AccountAwareManager,
63
+ HealthReporter<OrderDataStatus>,
64
+ PrivateOrderDataConsumer
65
+ {
66
+ readonly events: OrderEventStreams;
67
+
68
+ private readonly context: ClientContext;
69
+ private readonly orderBus = new AsyncEventBus<OrderEvent>();
70
+ private readonly orderStatusBus =
71
+ new AsyncEventBus<OrderStatusChangedEvent>();
72
+ private readonly records = new Map<string, OrderRecord>();
73
+
74
+ constructor(context: ClientContext) {
75
+ this.context = context;
76
+
77
+ this.events = {
78
+ status: (filter) =>
79
+ this.orderStatusBus.stream((event) =>
80
+ matchesOrderFilter(
81
+ { accountId: event.accountId, exchange: event.exchange },
82
+ filter,
83
+ ),
84
+ ),
85
+ updates: (filter) =>
86
+ this.orderBus.stream((event) =>
87
+ matchesOrderFilter(
88
+ {
89
+ accountId: event.accountId,
90
+ exchange: event.exchange,
91
+ symbol: "symbol" in event ? event.symbol : undefined,
92
+ },
93
+ filter,
94
+ ),
95
+ ),
96
+ };
97
+ }
98
+
99
+ // --- OrderManager public API ---
100
+
101
+ async subscribeOrders(input: SubscribeOrdersInput): Promise<void> {
102
+ this.context.assertStarted();
103
+ const account = this.context.getRegisteredAccount(input.accountId);
104
+ this.context.ensurePrivateCredentials(input.accountId);
105
+
106
+ const record = this.getOrCreateRecord(input.accountId, account.exchange);
107
+ record.subscribed = true;
108
+
109
+ try {
110
+ await this.context.subscribePrivateOrderFeed(input.accountId);
111
+ } catch (error) {
112
+ record.subscribed = false;
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ async unsubscribeOrders(input: UnsubscribeOrdersInput): Promise<void> {
118
+ const record = this.records.get(input.accountId);
119
+ if (!record?.subscribed) {
120
+ return;
121
+ }
122
+
123
+ this.context.unsubscribePrivateOrderFeed(input.accountId);
124
+ record.subscribed = false;
125
+ record.status = {
126
+ ...record.status,
127
+ activity: "inactive",
128
+ runtimeStatus: "stopped",
129
+ reason: undefined,
130
+ inactiveSince: this.context.now(),
131
+ };
132
+ this.publishStatus(record);
133
+ }
134
+
135
+ async createOrder(input: CreateOrderInput): Promise<OrderSnapshot> {
136
+ this.context.assertStarted();
137
+ const account = this.context.getRegisteredAccount(input.accountId);
138
+ this.context.ensurePrivateCredentials(input.accountId);
139
+ this.validateCreateOrderInput(input, account.exchange);
140
+
141
+ try {
142
+ const update = await this.context.createOrder(input);
143
+ return this.applyCommandUpdate(input.accountId, account.exchange, update);
144
+ } catch (error) {
145
+ throw this.wrapCommandError(
146
+ "ORDER_CREATE_FAILED",
147
+ `Failed to create order for ${input.accountId}: ${input.symbol}`,
148
+ error,
149
+ {
150
+ accountId: input.accountId,
151
+ exchange: account.exchange,
152
+ symbol: input.symbol,
153
+ },
154
+ );
155
+ }
156
+ }
157
+
158
+ async cancelOrder(input: CancelOrderInput): Promise<OrderSnapshot> {
159
+ this.context.assertStarted();
160
+ const account = this.context.getRegisteredAccount(input.accountId);
161
+ this.context.ensurePrivateCredentials(input.accountId);
162
+ this.validateCancelOrderInput(input, account.exchange);
163
+
164
+ try {
165
+ const update = await this.context.cancelOrder(input);
166
+ return this.applyCommandUpdate(input.accountId, account.exchange, update);
167
+ } catch (error) {
168
+ throw this.wrapCommandError(
169
+ "ORDER_CANCEL_FAILED",
170
+ `Failed to cancel order for ${input.accountId}: ${input.symbol}`,
171
+ error,
172
+ {
173
+ accountId: input.accountId,
174
+ exchange: account.exchange,
175
+ symbol: input.symbol,
176
+ },
177
+ );
178
+ }
179
+ }
180
+
181
+ async cancelAllOrders(input: CancelAllOrdersInput): Promise<OrderSnapshot[]> {
182
+ this.context.assertStarted();
183
+ const account = this.context.getRegisteredAccount(input.accountId);
184
+ this.context.ensurePrivateCredentials(input.accountId);
185
+
186
+ try {
187
+ const updates = await this.context.cancelAllOrders(input);
188
+ return this.applyCommandUpdates(
189
+ input.accountId,
190
+ account.exchange,
191
+ updates,
192
+ );
193
+ } catch (error) {
194
+ throw this.wrapCommandError(
195
+ "ORDER_CANCEL_ALL_FAILED",
196
+ `Failed to cancel all orders for ${input.accountId}: ${input.symbol}`,
197
+ error,
198
+ {
199
+ accountId: input.accountId,
200
+ exchange: account.exchange,
201
+ symbol: input.symbol,
202
+ },
203
+ );
204
+ }
205
+ }
206
+
207
+ getOrder(input: GetOrderInput): OrderSnapshot | undefined {
208
+ const record = this.records.get(input.accountId);
209
+ if (!record) {
210
+ return undefined;
211
+ }
212
+
213
+ if (!input.orderId && !input.clientOrderId) {
214
+ return undefined;
215
+ }
216
+
217
+ for (const snapshot of record.snapshots.values()) {
218
+ if (input.orderId && snapshot.orderId === input.orderId) {
219
+ return snapshot;
220
+ }
221
+
222
+ if (
223
+ input.clientOrderId &&
224
+ snapshot.clientOrderId === input.clientOrderId
225
+ ) {
226
+ return snapshot;
227
+ }
228
+ }
229
+
230
+ return undefined;
231
+ }
232
+
233
+ getOpenOrders(accountId: string, symbol?: string): OrderSnapshot[] {
234
+ const record = this.records.get(accountId);
235
+ if (!record) {
236
+ return [];
237
+ }
238
+
239
+ return [...record.snapshots.values()].filter((snapshot) => {
240
+ if (symbol && snapshot.symbol !== symbol) {
241
+ return false;
242
+ }
243
+
244
+ return (
245
+ snapshot.status === "open" || snapshot.status === "partially_filled"
246
+ );
247
+ });
248
+ }
249
+
250
+ getOrderStatus(accountId: string): OrderDataStatus | undefined {
251
+ const status = this.records.get(accountId)?.status;
252
+ return status ? cloneOrderStatus(status) : undefined;
253
+ }
254
+
255
+ // --- ManagerLifecycle ---
256
+
257
+ onClientStarted(): void {}
258
+
259
+ onClientStopping(now: number): void {
260
+ for (const record of this.records.values()) {
261
+ if (!record.subscribed) {
262
+ continue;
263
+ }
264
+
265
+ record.status = {
266
+ ...record.status,
267
+ activity: "inactive",
268
+ runtimeStatus: "stopped",
269
+ reason: undefined,
270
+ inactiveSince: now,
271
+ };
272
+ this.publishStatus(record);
273
+ }
274
+ }
275
+
276
+ // --- AccountAwareManager ---
277
+
278
+ onAccountRemoved(accountId: string, now: number): void {
279
+ const record = this.records.get(accountId);
280
+ if (!record) {
281
+ return;
282
+ }
283
+
284
+ record.subscribed = false;
285
+ record.status = {
286
+ ...record.status,
287
+ activity: "inactive",
288
+ runtimeStatus: "stopped",
289
+ reason: undefined,
290
+ inactiveSince: now,
291
+ };
292
+ this.publishStatus(record);
293
+ this.records.delete(accountId);
294
+ }
295
+
296
+ onCredentialsUpdated(accountId: string, exchange: Exchange): void {
297
+ const record = this.records.get(accountId);
298
+ if (!record?.subscribed) {
299
+ return;
300
+ }
301
+
302
+ this.onPrivateOrderPending(accountId, exchange);
303
+ }
304
+
305
+ // --- PrivateOrderDataConsumer ---
306
+
307
+ onPrivateOrderPending(accountId: string, exchange: Exchange): void {
308
+ const record = this.getOrCreateRecord(accountId, exchange);
309
+ if (!record.subscribed) {
310
+ return;
311
+ }
312
+
313
+ record.status = {
314
+ ...this.createStatus(accountId, exchange, "active"),
315
+ ready: record.snapshots.size > 0,
316
+ runtimeStatus: "bootstrap_pending",
317
+ reason: undefined,
318
+ lastReceivedAt: record.status.lastReceivedAt,
319
+ lastReadyAt: record.status.lastReadyAt,
320
+ inactiveSince: undefined,
321
+ };
322
+ this.publishStatus(record);
323
+ }
324
+
325
+ onPrivateOrderBootstrap(
326
+ accountId: string,
327
+ exchange: Exchange,
328
+ snapshots: RawOrderUpdate[],
329
+ ): void {
330
+ const record = this.getOrCreateRecord(accountId, exchange);
331
+ if (!record.subscribed) {
332
+ return;
333
+ }
334
+
335
+ const nextSnapshots = new Map<string, OrderSnapshot>();
336
+ for (const update of snapshots) {
337
+ const snapshot = this.createSnapshot(
338
+ accountId,
339
+ exchange,
340
+ update,
341
+ this.getExistingSnapshot(record, update),
342
+ );
343
+ this.setSnapshot(nextSnapshots, snapshot);
344
+ }
345
+
346
+ record.snapshots = nextSnapshots;
347
+ const orderedSnapshots = [...record.snapshots.values()];
348
+ const latestTs = orderedSnapshots.reduce(
349
+ (max, snapshot) => Math.max(max, snapshot.updatedAt),
350
+ 0,
351
+ );
352
+ record.status = {
353
+ ...record.status,
354
+ activity: "active",
355
+ ready: true,
356
+ runtimeStatus: "healthy",
357
+ reason: undefined,
358
+ lastReceivedAt: latestTs || record.status.lastReceivedAt,
359
+ lastReadyAt: latestTs || this.context.now(),
360
+ inactiveSince: undefined,
361
+ };
362
+
363
+ const event: OrderSnapshotReplacedEvent = {
364
+ type: "order.snapshot_replaced",
365
+ accountId,
366
+ exchange,
367
+ snapshot: orderedSnapshots,
368
+ ts: this.context.now(),
369
+ };
370
+
371
+ this.orderBus.publish(event);
372
+ this.publishStatus(record);
373
+ }
374
+
375
+ onPrivateOrderUpdate(
376
+ accountId: string,
377
+ exchange: Exchange,
378
+ update: RawOrderUpdate,
379
+ ): void {
380
+ const record = this.getOrCreateRecord(accountId, exchange);
381
+ if (!record.subscribed) {
382
+ return;
383
+ }
384
+
385
+ const previous = this.getExistingSnapshot(record, update);
386
+ const snapshot = this.createSnapshot(accountId, exchange, update, previous);
387
+ this.setSnapshot(record.snapshots, snapshot);
388
+
389
+ const eventType =
390
+ snapshot.status === "filled"
391
+ ? "order.filled"
392
+ : snapshot.status === "rejected"
393
+ ? "order.rejected"
394
+ : snapshot.status === "canceled" || snapshot.status === "expired"
395
+ ? "order.canceled"
396
+ : "order.updated";
397
+
398
+ this.orderBus.publish({
399
+ type: eventType,
400
+ accountId,
401
+ exchange,
402
+ symbol: snapshot.symbol,
403
+ snapshot,
404
+ ts: this.context.now(),
405
+ });
406
+
407
+ record.status = {
408
+ ...record.status,
409
+ activity: "active",
410
+ ready: true,
411
+ runtimeStatus: "healthy",
412
+ reason: undefined,
413
+ lastReceivedAt: snapshot.receivedAt,
414
+ lastReadyAt: snapshot.updatedAt,
415
+ inactiveSince: undefined,
416
+ };
417
+ this.publishStatus(record);
418
+ }
419
+
420
+ onPrivateOrderStreamState(
421
+ accountId: string,
422
+ exchange: Exchange,
423
+ state: PrivateSubscriptionState,
424
+ ): void {
425
+ const record = this.getOrCreateRecord(accountId, exchange);
426
+ if (!record.subscribed) {
427
+ return;
428
+ }
429
+
430
+ record.status = {
431
+ ...record.status,
432
+ activity: "active",
433
+ ready: state.ready,
434
+ runtimeStatus: state.runtimeStatus,
435
+ reason: state.reason,
436
+ lastReceivedAt: state.lastReceivedAt ?? record.status.lastReceivedAt,
437
+ lastReadyAt: state.lastReadyAt ?? record.status.lastReadyAt,
438
+ inactiveSince: undefined,
439
+ };
440
+ this.publishStatus(record);
441
+ }
442
+
443
+ // --- HealthReporter ---
444
+
445
+ getStatuses(): OrderDataStatus[] {
446
+ return [...this.records.values()]
447
+ .map((record) => cloneOrderStatus(record.status))
448
+ .sort((left, right) =>
449
+ `${left.exchange}:${left.accountId}`.localeCompare(
450
+ `${right.exchange}:${right.accountId}`,
451
+ ),
452
+ );
453
+ }
454
+
455
+ // --- Internal helpers ---
456
+
457
+ private getOrCreateRecord(
458
+ accountId: string,
459
+ exchange: Exchange,
460
+ ): OrderRecord {
461
+ const existing = this.records.get(accountId);
462
+ if (existing) {
463
+ return existing;
464
+ }
465
+
466
+ const record: OrderRecord = {
467
+ accountId,
468
+ exchange,
469
+ subscribed: false,
470
+ snapshots: new Map(),
471
+ status: this.createStatus(accountId, exchange, "inactive"),
472
+ };
473
+
474
+ this.records.set(accountId, record);
475
+ return record;
476
+ }
477
+
478
+ private createStatus(
479
+ accountId: string,
480
+ exchange: Exchange,
481
+ activity: "active" | "inactive",
482
+ ): OrderDataStatus {
483
+ return {
484
+ accountId,
485
+ exchange,
486
+ activity,
487
+ ready: false,
488
+ runtimeStatus: activity === "active" ? "bootstrap_pending" : "stopped",
489
+ };
490
+ }
491
+
492
+ private getExistingSnapshot(
493
+ record: OrderRecord,
494
+ update: { orderId?: string; clientOrderId?: string },
495
+ ): OrderSnapshot | undefined {
496
+ for (const snapshot of record.snapshots.values()) {
497
+ if (update.orderId && snapshot.orderId === update.orderId) {
498
+ return snapshot;
499
+ }
500
+
501
+ if (
502
+ update.clientOrderId &&
503
+ snapshot.clientOrderId === update.clientOrderId
504
+ ) {
505
+ return snapshot;
506
+ }
507
+ }
508
+
509
+ return undefined;
510
+ }
511
+
512
+ private setSnapshot(
513
+ snapshots: Map<string, OrderSnapshot>,
514
+ snapshot: OrderSnapshot,
515
+ ): void {
516
+ const lookupKey =
517
+ getOrderLookupKey(snapshot) ??
518
+ getOrderLookupKey({
519
+ clientOrderId: snapshot.clientOrderId,
520
+ });
521
+ if (lookupKey) {
522
+ snapshots.set(lookupKey, snapshot);
523
+ }
524
+ }
525
+
526
+ private createSnapshot(
527
+ accountId: string,
528
+ exchange: Exchange,
529
+ input: RawOrderUpdate,
530
+ previous?: OrderSnapshot,
531
+ ): OrderSnapshot {
532
+ const amount = new BigNumber(input.amount);
533
+ const filled = new BigNumber(input.filled);
534
+ const remaining =
535
+ input.remaining === undefined
536
+ ? amount.minus(filled)
537
+ : new BigNumber(input.remaining);
538
+
539
+ return {
540
+ accountId,
541
+ exchange,
542
+ orderId: input.orderId,
543
+ clientOrderId: input.clientOrderId,
544
+ symbol: input.symbol,
545
+ side: input.side,
546
+ type: input.type,
547
+ status: input.status,
548
+ price:
549
+ input.price === undefined
550
+ ? previous?.price
551
+ : new BigNumber(input.price),
552
+ triggerPrice:
553
+ input.triggerPrice === undefined
554
+ ? previous?.triggerPrice
555
+ : new BigNumber(input.triggerPrice),
556
+ amount,
557
+ filled,
558
+ remaining,
559
+ reduceOnly: input.reduceOnly ?? previous?.reduceOnly,
560
+ positionSide: input.positionSide ?? previous?.positionSide,
561
+ avgFillPrice:
562
+ input.avgFillPrice === undefined
563
+ ? previous?.avgFillPrice
564
+ : new BigNumber(input.avgFillPrice),
565
+ exchangeTs: input.exchangeTs,
566
+ receivedAt: input.receivedAt,
567
+ updatedAt: input.receivedAt,
568
+ seq: (previous?.seq ?? 0) + 1,
569
+ };
570
+ }
571
+
572
+ private publishStatus(record: OrderRecord): void {
573
+ const event: OrderStatusChangedEvent = {
574
+ type: "order.status_changed",
575
+ accountId: record.accountId,
576
+ exchange: record.exchange,
577
+ status: cloneOrderStatus(record.status),
578
+ ts: this.context.now(),
579
+ };
580
+
581
+ this.orderStatusBus.publish(event);
582
+ this.context.publishHealthEvent(event);
583
+ }
584
+
585
+ private validateCreateOrderInput(
586
+ input: CreateOrderInput,
587
+ exchange: Exchange,
588
+ ): void {
589
+ if (input.type === "limit" && !input.price) {
590
+ throw this.createError(
591
+ "ORDER_INPUT_INVALID",
592
+ `Limit orders require price: ${input.accountId}`,
593
+ {
594
+ accountId: input.accountId,
595
+ exchange,
596
+ symbol: input.symbol,
597
+ },
598
+ );
599
+ }
600
+ }
601
+
602
+ private validateCancelOrderInput(
603
+ input: CancelOrderInput,
604
+ exchange: Exchange,
605
+ ): void {
606
+ if (input.orderId || input.clientOrderId) {
607
+ return;
608
+ }
609
+
610
+ throw this.createError(
611
+ "ORDER_INPUT_INVALID",
612
+ `cancelOrder requires orderId or clientOrderId: ${input.accountId}`,
613
+ {
614
+ accountId: input.accountId,
615
+ exchange,
616
+ symbol: input.symbol,
617
+ },
618
+ );
619
+ }
620
+
621
+ private applyCommandUpdate(
622
+ accountId: string,
623
+ exchange: Exchange,
624
+ update: RawOrderUpdate,
625
+ ): OrderSnapshot {
626
+ const record = this.getOrCreateRecord(accountId, exchange);
627
+ const previous = this.getExistingSnapshot(record, update);
628
+ const snapshot = this.createSnapshot(accountId, exchange, update, previous);
629
+ this.setSnapshot(record.snapshots, snapshot);
630
+ return snapshot;
631
+ }
632
+
633
+ private applyCommandUpdates(
634
+ accountId: string,
635
+ exchange: Exchange,
636
+ updates: RawOrderUpdate[],
637
+ ): OrderSnapshot[] {
638
+ return updates.map((update) =>
639
+ this.applyCommandUpdate(accountId, exchange, update),
640
+ );
641
+ }
642
+
643
+ private createError(
644
+ code:
645
+ | "ORDER_CANCEL_ALL_FAILED"
646
+ | "ORDER_CANCEL_FAILED"
647
+ | "ORDER_CREATE_FAILED"
648
+ | "ORDER_INPUT_INVALID",
649
+ message: string,
650
+ metadata: {
651
+ accountId: string;
652
+ exchange: Exchange;
653
+ symbol?: string;
654
+ },
655
+ ): AcexError {
656
+ const error = new AcexError(code, message);
657
+ this.context.publishRuntimeError("order", error, metadata);
658
+ return error;
659
+ }
660
+
661
+ private wrapCommandError(
662
+ code:
663
+ | "ORDER_CANCEL_ALL_FAILED"
664
+ | "ORDER_CANCEL_FAILED"
665
+ | "ORDER_CREATE_FAILED",
666
+ message: string,
667
+ error: unknown,
668
+ metadata: {
669
+ accountId: string;
670
+ exchange: Exchange;
671
+ symbol: string;
672
+ },
673
+ ): AcexError {
674
+ if (error instanceof AcexError) {
675
+ return error;
676
+ }
677
+
678
+ this.context.publishRuntimeError(
679
+ "adapter",
680
+ error instanceof Error ? error : new Error(message),
681
+ metadata,
682
+ );
683
+ return new AcexError(code, message);
684
+ }
685
+ }