@imbingox/acex 0.1.0-beta.2 → 0.1.0-beta.3

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.
@@ -0,0 +1,512 @@
1
+ import type {
2
+ PrivateUserDataAdapter,
3
+ StreamHandle,
4
+ } from "../adapters/types.ts";
5
+ import { AcexError } from "../errors.ts";
6
+ import type { AccountRuntimeOptions, Exchange } from "../types/index.ts";
7
+ import type {
8
+ ClientContext,
9
+ PrivateAccountDataConsumer,
10
+ PrivateOrderDataConsumer,
11
+ RegisteredAccountRecord,
12
+ } from "./context.ts";
13
+
14
+ interface PrivateSubscriptionRecord {
15
+ accountId: string;
16
+ exchange: Exchange;
17
+ accountSubscribed: boolean;
18
+ ordersSubscribed: boolean;
19
+ accountReady: boolean;
20
+ orderReady: boolean;
21
+ stream?: StreamHandle;
22
+ startPromise?: Promise<void>;
23
+ reconcilePromise?: Promise<void>;
24
+ }
25
+
26
+ const DEFAULT_STREAM_OPEN_TIMEOUT_MS = 15_000;
27
+ const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
28
+ const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
29
+ const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
30
+
31
+ export class PrivateSubscriptionCoordinator {
32
+ private readonly context: ClientContext;
33
+ private readonly adapter: PrivateUserDataAdapter;
34
+ private readonly accountConsumer: PrivateAccountDataConsumer;
35
+ private readonly orderConsumer: PrivateOrderDataConsumer;
36
+ private readonly streamOpenTimeoutMs: number;
37
+ private readonly streamReconnectDelayMs: number;
38
+ private readonly streamReconnectMaxDelayMs: number;
39
+ private readonly listenKeyKeepAliveMs: number;
40
+ private readonly records = new Map<string, PrivateSubscriptionRecord>();
41
+
42
+ constructor(
43
+ context: ClientContext,
44
+ adapter: PrivateUserDataAdapter,
45
+ accountConsumer: PrivateAccountDataConsumer,
46
+ orderConsumer: PrivateOrderDataConsumer,
47
+ options: AccountRuntimeOptions = {},
48
+ ) {
49
+ this.context = context;
50
+ this.adapter = adapter;
51
+ this.accountConsumer = accountConsumer;
52
+ this.orderConsumer = orderConsumer;
53
+ this.streamOpenTimeoutMs =
54
+ options.streamOpenTimeoutMs ?? DEFAULT_STREAM_OPEN_TIMEOUT_MS;
55
+ this.streamReconnectDelayMs =
56
+ options.streamReconnectDelayMs ?? DEFAULT_STREAM_RECONNECT_DELAY_MS;
57
+ this.streamReconnectMaxDelayMs =
58
+ options.streamReconnectMaxDelayMs ??
59
+ DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
60
+ this.listenKeyKeepAliveMs =
61
+ options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
62
+ }
63
+
64
+ async subscribeAccountFeed(accountId: string): Promise<void> {
65
+ const account = this.getAccount(accountId);
66
+ const record = this.getOrCreateRecord(account);
67
+ const needsPending = !record.stream && !record.startPromise;
68
+ record.accountSubscribed = true;
69
+ if (needsPending) {
70
+ this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
71
+ }
72
+
73
+ try {
74
+ await this.ensureStream(record, account);
75
+ await this.bootstrapAccount(record, account);
76
+ } catch (error) {
77
+ record.accountSubscribed = false;
78
+ this.closeIfUnused(record);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ unsubscribeAccountFeed(accountId: string): void {
84
+ const record = this.records.get(accountId);
85
+ if (!record) {
86
+ return;
87
+ }
88
+
89
+ record.accountSubscribed = false;
90
+ this.closeIfUnused(record);
91
+ }
92
+
93
+ async subscribeOrderFeed(accountId: string): Promise<void> {
94
+ const account = this.getAccount(accountId);
95
+ const record = this.getOrCreateRecord(account);
96
+ const needsPending = !record.stream && !record.startPromise;
97
+ record.ordersSubscribed = true;
98
+ if (needsPending) {
99
+ this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
100
+ }
101
+
102
+ try {
103
+ await this.ensureStream(record, account);
104
+ await this.bootstrapOrders(record, account);
105
+ } catch (error) {
106
+ record.ordersSubscribed = false;
107
+ this.closeIfUnused(record);
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ unsubscribeOrderFeed(accountId: string): void {
113
+ const record = this.records.get(accountId);
114
+ if (!record) {
115
+ return;
116
+ }
117
+
118
+ record.ordersSubscribed = false;
119
+ this.closeIfUnused(record);
120
+ }
121
+
122
+ onClientStarted(): void {
123
+ for (const record of this.records.values()) {
124
+ if (!this.isActive(record)) {
125
+ continue;
126
+ }
127
+
128
+ if (record.accountSubscribed) {
129
+ this.accountConsumer.onPrivateAccountPending(
130
+ record.accountId,
131
+ record.exchange,
132
+ );
133
+ }
134
+ if (record.ordersSubscribed) {
135
+ this.orderConsumer.onPrivateOrderPending(
136
+ record.accountId,
137
+ record.exchange,
138
+ );
139
+ }
140
+
141
+ void this.resumeRecord(record);
142
+ }
143
+ }
144
+
145
+ onClientStopping(): void {
146
+ for (const record of this.records.values()) {
147
+ this.closeStream(record);
148
+ }
149
+ }
150
+
151
+ onAccountRemoved(accountId: string): void {
152
+ const record = this.records.get(accountId);
153
+ if (!record) {
154
+ return;
155
+ }
156
+
157
+ this.closeStream(record);
158
+ this.records.delete(accountId);
159
+ }
160
+
161
+ onCredentialsUpdated(accountId: string): void {
162
+ const record = this.records.get(accountId);
163
+ if (!record || !this.isActive(record)) {
164
+ return;
165
+ }
166
+
167
+ if (record.accountSubscribed) {
168
+ this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
169
+ }
170
+ if (record.ordersSubscribed) {
171
+ this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
172
+ }
173
+
174
+ void this.resumeRecord(record);
175
+ }
176
+
177
+ private async resumeRecord(record: PrivateSubscriptionRecord): Promise<void> {
178
+ const account = this.getAccount(record.accountId);
179
+ this.closeStream(record);
180
+
181
+ try {
182
+ await this.ensureStream(record, account);
183
+ if (record.accountSubscribed) {
184
+ await this.bootstrapAccount(record, account);
185
+ }
186
+ if (record.ordersSubscribed) {
187
+ await this.bootstrapOrders(record, account);
188
+ }
189
+ } catch {
190
+ // Errors are already published to the runtime error bus.
191
+ }
192
+ }
193
+
194
+ private getAccount(accountId: string): RegisteredAccountRecord {
195
+ const account = this.context.getRegisteredAccount(accountId);
196
+ if (account.exchange !== this.adapter.exchange) {
197
+ throw new AcexError(
198
+ "EXCHANGE_NOT_SUPPORTED",
199
+ `Exchange is not supported yet: ${account.exchange}`,
200
+ );
201
+ }
202
+
203
+ return account;
204
+ }
205
+
206
+ private getOrCreateRecord(
207
+ account: RegisteredAccountRecord,
208
+ ): PrivateSubscriptionRecord {
209
+ const existing = this.records.get(account.accountId);
210
+ if (existing) {
211
+ return existing;
212
+ }
213
+
214
+ const record: PrivateSubscriptionRecord = {
215
+ accountId: account.accountId,
216
+ exchange: account.exchange,
217
+ accountSubscribed: false,
218
+ ordersSubscribed: false,
219
+ accountReady: false,
220
+ orderReady: false,
221
+ };
222
+
223
+ this.records.set(account.accountId, record);
224
+ return record;
225
+ }
226
+
227
+ private isActive(record: PrivateSubscriptionRecord): boolean {
228
+ return record.accountSubscribed || record.ordersSubscribed;
229
+ }
230
+
231
+ private closeIfUnused(record: PrivateSubscriptionRecord): void {
232
+ if (this.isActive(record)) {
233
+ return;
234
+ }
235
+
236
+ this.closeStream(record);
237
+ this.records.delete(record.accountId);
238
+ }
239
+
240
+ private closeStream(record: PrivateSubscriptionRecord): void {
241
+ record.stream?.close();
242
+ record.stream = undefined;
243
+ }
244
+
245
+ private async ensureStream(
246
+ record: PrivateSubscriptionRecord,
247
+ account: RegisteredAccountRecord,
248
+ ): Promise<void> {
249
+ if (record.stream) {
250
+ return;
251
+ }
252
+
253
+ if (!record.startPromise) {
254
+ record.startPromise = this.startStream(record, account);
255
+ }
256
+
257
+ try {
258
+ await record.startPromise;
259
+ } finally {
260
+ if (record.startPromise) {
261
+ record.startPromise = undefined;
262
+ }
263
+ }
264
+ }
265
+
266
+ private async startStream(
267
+ record: PrivateSubscriptionRecord,
268
+ account: RegisteredAccountRecord,
269
+ ): Promise<void> {
270
+ const credentials = account.credentials;
271
+ if (!credentials) {
272
+ throw new AcexError(
273
+ "CREDENTIALS_MISSING",
274
+ `Account credentials are required for private subscriptions: ${account.accountId}`,
275
+ );
276
+ }
277
+
278
+ const stream = this.adapter.createPrivateStream(
279
+ credentials,
280
+ {
281
+ onAccountUpdate: (update) => {
282
+ if (!record.accountSubscribed) {
283
+ return;
284
+ }
285
+
286
+ record.accountReady = true;
287
+ this.accountConsumer.onPrivateAccountUpdate(
288
+ record.accountId,
289
+ record.exchange,
290
+ update,
291
+ );
292
+ },
293
+ onOrderUpdate: (update) => {
294
+ if (!record.ordersSubscribed) {
295
+ return;
296
+ }
297
+
298
+ record.orderReady = true;
299
+ this.orderConsumer.onPrivateOrderUpdate(
300
+ record.accountId,
301
+ record.exchange,
302
+ update,
303
+ );
304
+ },
305
+ onDisconnected: () => {
306
+ if (record.accountSubscribed) {
307
+ this.accountConsumer.onPrivateAccountStreamState(
308
+ record.accountId,
309
+ record.exchange,
310
+ {
311
+ runtimeStatus: "reconnecting",
312
+ ready: record.accountReady,
313
+ reason: "ws_disconnected",
314
+ },
315
+ );
316
+ }
317
+ if (record.ordersSubscribed) {
318
+ this.orderConsumer.onPrivateOrderStreamState(
319
+ record.accountId,
320
+ record.exchange,
321
+ {
322
+ runtimeStatus: "reconnecting",
323
+ ready: record.orderReady,
324
+ reason: "ws_disconnected",
325
+ },
326
+ );
327
+ }
328
+ },
329
+ onReconnected: () => {
330
+ if (!record.reconcilePromise) {
331
+ record.reconcilePromise = this.reconcileRecord(record)
332
+ .catch(() => {})
333
+ .finally(() => {
334
+ record.reconcilePromise = undefined;
335
+ });
336
+ }
337
+ },
338
+ onError: (error) => {
339
+ this.context.publishRuntimeError("adapter", error, {
340
+ accountId: record.accountId,
341
+ exchange: record.exchange,
342
+ });
343
+ },
344
+ },
345
+ {
346
+ openTimeoutMs: this.streamOpenTimeoutMs,
347
+ reconnectDelayMs: this.streamReconnectDelayMs,
348
+ reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
349
+ listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
350
+ now: () => this.context.now(),
351
+ },
352
+ account.options,
353
+ );
354
+
355
+ record.stream = stream;
356
+
357
+ try {
358
+ await stream.ready;
359
+ } catch (error) {
360
+ this.closeStream(record);
361
+ const runtimeError =
362
+ error instanceof Error
363
+ ? error
364
+ : new Error("Failed to open Binance private stream");
365
+ this.context.publishRuntimeError("adapter", runtimeError, {
366
+ accountId: record.accountId,
367
+ exchange: record.exchange,
368
+ });
369
+
370
+ if (record.accountSubscribed) {
371
+ this.accountConsumer.onPrivateAccountStreamState(
372
+ record.accountId,
373
+ record.exchange,
374
+ {
375
+ runtimeStatus: "degraded",
376
+ ready: record.accountReady,
377
+ reason: "ws_disconnected",
378
+ },
379
+ );
380
+ }
381
+ if (record.ordersSubscribed) {
382
+ this.orderConsumer.onPrivateOrderStreamState(
383
+ record.accountId,
384
+ record.exchange,
385
+ {
386
+ runtimeStatus: "degraded",
387
+ ready: record.orderReady,
388
+ reason: "ws_disconnected",
389
+ },
390
+ );
391
+ }
392
+
393
+ throw runtimeError;
394
+ }
395
+ }
396
+
397
+ private async reconcileRecord(
398
+ record: PrivateSubscriptionRecord,
399
+ ): Promise<void> {
400
+ const account = this.getAccount(record.accountId);
401
+
402
+ if (record.accountSubscribed) {
403
+ this.accountConsumer.onPrivateAccountPending(
404
+ record.accountId,
405
+ record.exchange,
406
+ );
407
+ await this.bootstrapAccount(record, account);
408
+ }
409
+
410
+ if (record.ordersSubscribed) {
411
+ this.orderConsumer.onPrivateOrderPending(
412
+ record.accountId,
413
+ record.exchange,
414
+ );
415
+ await this.bootstrapOrders(record, account);
416
+ }
417
+ }
418
+
419
+ private async bootstrapAccount(
420
+ record: PrivateSubscriptionRecord,
421
+ account: RegisteredAccountRecord,
422
+ ): Promise<void> {
423
+ try {
424
+ const bootstrap = await this.adapter.bootstrapAccount(
425
+ account.credentials ?? {},
426
+ account.options,
427
+ );
428
+ if (!record.accountSubscribed) {
429
+ return;
430
+ }
431
+
432
+ record.accountReady = true;
433
+ this.accountConsumer.onPrivateAccountBootstrap(
434
+ record.accountId,
435
+ record.exchange,
436
+ bootstrap,
437
+ );
438
+ } catch (error) {
439
+ record.accountReady = false;
440
+ this.context.publishRuntimeError(
441
+ "adapter",
442
+ error instanceof Error
443
+ ? error
444
+ : new Error("Failed to bootstrap Binance private account state"),
445
+ {
446
+ accountId: record.accountId,
447
+ exchange: record.exchange,
448
+ },
449
+ );
450
+ this.accountConsumer.onPrivateAccountStreamState(
451
+ record.accountId,
452
+ record.exchange,
453
+ {
454
+ runtimeStatus: "degraded",
455
+ ready: false,
456
+ reason: "auth_failed",
457
+ },
458
+ );
459
+ throw new AcexError(
460
+ "ACCOUNT_BOOTSTRAP_FAILED",
461
+ `Failed to bootstrap account data: ${record.accountId}`,
462
+ );
463
+ }
464
+ }
465
+
466
+ private async bootstrapOrders(
467
+ record: PrivateSubscriptionRecord,
468
+ account: RegisteredAccountRecord,
469
+ ): Promise<void> {
470
+ try {
471
+ const snapshots = await this.adapter.bootstrapOpenOrders(
472
+ account.credentials ?? {},
473
+ account.options,
474
+ );
475
+ if (!record.ordersSubscribed) {
476
+ return;
477
+ }
478
+
479
+ record.orderReady = true;
480
+ this.orderConsumer.onPrivateOrderBootstrap(
481
+ record.accountId,
482
+ record.exchange,
483
+ snapshots,
484
+ );
485
+ } catch (error) {
486
+ record.orderReady = false;
487
+ this.context.publishRuntimeError(
488
+ "adapter",
489
+ error instanceof Error
490
+ ? error
491
+ : new Error("Failed to bootstrap Binance private order state"),
492
+ {
493
+ accountId: record.accountId,
494
+ exchange: record.exchange,
495
+ },
496
+ );
497
+ this.orderConsumer.onPrivateOrderStreamState(
498
+ record.accountId,
499
+ record.exchange,
500
+ {
501
+ runtimeStatus: "degraded",
502
+ ready: false,
503
+ reason: "auth_failed",
504
+ },
505
+ );
506
+ throw new AcexError(
507
+ "ORDER_BOOTSTRAP_FAILED",
508
+ `Failed to bootstrap order data: ${record.accountId}`,
509
+ );
510
+ }
511
+ }
512
+ }
@@ -1,4 +1,12 @@
1
1
  import { BinanceMarketAdapter } from "../adapters/binance/adapter.ts";
2
+ import { BinancePrivateAdapter } from "../adapters/binance/private-adapter.ts";
3
+ import type {
4
+ CancelAllOrdersRequest,
5
+ CancelOrderRequest,
6
+ CreateOrderRequest,
7
+ PrivateUserDataAdapter,
8
+ RawOrderUpdate,
9
+ } from "../adapters/types.ts";
2
10
  import { AcexError, type AcexErrorCode } from "../errors.ts";
3
11
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
4
12
  import { matchesHealthFilter } from "../internal/filters.ts";
@@ -10,11 +18,14 @@ import type {
10
18
  AccountManager,
11
19
  AcexClient,
12
20
  AcexInternalError,
21
+ CancelAllOrdersInput,
22
+ CancelOrderInput,
13
23
  ClientEventStreams,
14
24
  ClientHealthSnapshot,
15
25
  ClientStatus,
16
26
  ClientStatusChangedEvent,
17
27
  CreateClientOptions,
28
+ CreateOrderInput,
18
29
  HealthEvent,
19
30
  HealthEventFilter,
20
31
  MarketManager,
@@ -27,8 +38,23 @@ import {
27
38
  type ClientContext,
28
39
  hasPrivateCredentials,
29
40
  mergeCredentials,
41
+ type PrivateAccountDataConsumer,
42
+ type PrivateOrderDataConsumer,
30
43
  type RegisteredAccountRecord,
31
44
  } from "./context.ts";
45
+ import { PrivateSubscriptionCoordinator } from "./private-subscription-coordinator.ts";
46
+
47
+ const activeClients = new Set<AcexClientImpl>();
48
+
49
+ export function stopAllClientsForTests(): void {
50
+ const clients = [...activeClients];
51
+ activeClients.clear();
52
+ for (const client of clients) {
53
+ void client.stop().catch(() => {
54
+ // Test cleanup should be best-effort and never mask the original failure.
55
+ });
56
+ }
57
+ }
32
58
 
33
59
  class ClientEventStreamsImpl implements ClientEventStreams {
34
60
  constructor(
@@ -61,11 +87,16 @@ export class AcexClientImpl implements AcexClient, ClientContext {
61
87
  private readonly marketManager: MarketManagerImpl;
62
88
  private readonly accountManager: AccountManagerImpl;
63
89
  private readonly orderManager: OrderManagerImpl;
90
+ private readonly privateAdapter: PrivateUserDataAdapter;
91
+ private readonly privateCoordinator: PrivateSubscriptionCoordinator;
64
92
 
65
93
  constructor(options: CreateClientOptions = {}) {
66
- const adapter = new BinanceMarketAdapter();
94
+ activeClients.add(this);
67
95
 
68
- this.marketManager = new MarketManagerImpl(this, adapter, {
96
+ const marketAdapter = new BinanceMarketAdapter();
97
+ this.privateAdapter = new BinancePrivateAdapter();
98
+
99
+ this.marketManager = new MarketManagerImpl(this, marketAdapter, {
69
100
  initialL1TimeoutMs: options.market?.l1InitialMessageTimeoutMs,
70
101
  l1StaleAfterMs: options.market?.l1StaleAfterMs,
71
102
  l1ReconnectDelayMs: options.market?.l1ReconnectDelayMs,
@@ -73,6 +104,13 @@ export class AcexClientImpl implements AcexClient, ClientContext {
73
104
  });
74
105
  this.accountManager = new AccountManagerImpl(this);
75
106
  this.orderManager = new OrderManagerImpl(this);
107
+ this.privateCoordinator = new PrivateSubscriptionCoordinator(
108
+ this,
109
+ this.privateAdapter,
110
+ this.accountManager as PrivateAccountDataConsumer,
111
+ this.orderManager as PrivateOrderDataConsumer,
112
+ options.account,
113
+ );
76
114
 
77
115
  this.market = this.marketManager;
78
116
  this.account = this.accountManager;
@@ -141,6 +179,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
141
179
 
142
180
  this.accountManager.onCredentialsUpdated(accountId, account.exchange);
143
181
  this.orderManager.onCredentialsUpdated(accountId, account.exchange);
182
+ this.privateCoordinator.onCredentialsUpdated(accountId);
144
183
  }
145
184
 
146
185
  async removeAccount(accountId: string): Promise<void> {
@@ -154,6 +193,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
154
193
  }
155
194
 
156
195
  const now = this.now();
196
+ this.privateCoordinator.onAccountRemoved(accountId);
157
197
  this.accountManager.onAccountRemoved(accountId, now);
158
198
  this.orderManager.onAccountRemoved(accountId, now);
159
199
  this.registeredAccounts.delete(accountId);
@@ -170,6 +210,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
170
210
  this.marketManager.onClientStarted();
171
211
  this.accountManager.onClientStarted();
172
212
  this.orderManager.onClientStarted();
213
+ this.privateCoordinator.onClientStarted();
173
214
  }
174
215
 
175
216
  async stop(_options?: StopOptions): Promise<void> {
@@ -183,6 +224,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
183
224
  this.setClientStatus("stopping");
184
225
 
185
226
  const now = this.now();
227
+ this.privateCoordinator.onClientStopping();
186
228
  this.marketManager.onClientStopping(now);
187
229
  this.accountManager.onClientStopping(now);
188
230
  this.orderManager.onClientStopping(now);
@@ -231,6 +273,70 @@ export class AcexClientImpl implements AcexClient, ClientContext {
231
273
  );
232
274
  }
233
275
 
276
+ subscribePrivateAccountFeed(accountId: string): Promise<void> {
277
+ return this.privateCoordinator.subscribeAccountFeed(accountId);
278
+ }
279
+
280
+ unsubscribePrivateAccountFeed(accountId: string): void {
281
+ this.privateCoordinator.unsubscribeAccountFeed(accountId);
282
+ }
283
+
284
+ subscribePrivateOrderFeed(accountId: string): Promise<void> {
285
+ return this.privateCoordinator.subscribeOrderFeed(accountId);
286
+ }
287
+
288
+ unsubscribePrivateOrderFeed(accountId: string): void {
289
+ this.privateCoordinator.unsubscribeOrderFeed(accountId);
290
+ }
291
+
292
+ createOrder(input: CreateOrderInput): Promise<RawOrderUpdate> {
293
+ const account = this.getPrivateCommandAccount(input.accountId);
294
+ const request: CreateOrderRequest = {
295
+ symbol: input.symbol,
296
+ side: input.side,
297
+ type: input.type,
298
+ amount: input.amount,
299
+ price: input.type === "limit" ? input.price : undefined,
300
+ clientOrderId: input.clientOrderId,
301
+ reduceOnly: input.reduceOnly,
302
+ positionSide: input.positionSide,
303
+ };
304
+
305
+ return this.privateAdapter.createOrder(
306
+ account.credentials ?? {},
307
+ request,
308
+ account.options,
309
+ );
310
+ }
311
+
312
+ cancelOrder(input: CancelOrderInput): Promise<RawOrderUpdate> {
313
+ const account = this.getPrivateCommandAccount(input.accountId);
314
+ const request: CancelOrderRequest = {
315
+ symbol: input.symbol,
316
+ orderId: input.orderId,
317
+ clientOrderId: input.clientOrderId,
318
+ };
319
+
320
+ return this.privateAdapter.cancelOrder(
321
+ account.credentials ?? {},
322
+ request,
323
+ account.options,
324
+ );
325
+ }
326
+
327
+ cancelAllOrders(input: CancelAllOrdersInput): Promise<RawOrderUpdate[]> {
328
+ const account = this.getPrivateCommandAccount(input.accountId);
329
+ const request: CancelAllOrdersRequest = {
330
+ symbol: input.symbol,
331
+ };
332
+
333
+ return this.privateAdapter.cancelAllOrders(
334
+ account.credentials ?? {},
335
+ request,
336
+ account.options,
337
+ );
338
+ }
339
+
234
340
  publishRuntimeError(
235
341
  source: AcexInternalError["source"],
236
342
  error: Error,
@@ -280,4 +386,25 @@ export class AcexClientImpl implements AcexClient, ClientContext {
280
386
  });
281
387
  return error;
282
388
  }
389
+
390
+ private getPrivateCommandAccount(accountId: string): RegisteredAccountRecord {
391
+ const account = this.getRegisteredAccount(accountId);
392
+ if (account.exchange !== this.privateAdapter.exchange) {
393
+ throw this.createError(
394
+ "EXCHANGE_NOT_SUPPORTED",
395
+ `Exchange is not supported yet: ${account.exchange}`,
396
+ { accountId, exchange: account.exchange },
397
+ );
398
+ }
399
+
400
+ if (!hasPrivateCredentials(account.credentials)) {
401
+ throw this.createError(
402
+ "CREDENTIALS_MISSING",
403
+ `Account credentials are required for private order commands: ${accountId}`,
404
+ { accountId, exchange: account.exchange },
405
+ );
406
+ }
407
+
408
+ return account;
409
+ }
283
410
  }