@imbingox/acex 0.3.0-beta.1 → 0.3.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.
@@ -1,11 +1,14 @@
1
1
  import type {
2
2
  AccountCredentials,
3
3
  CreateOrderType,
4
- Exchange,
5
4
  MarketDefinition,
6
5
  OrderSide,
7
6
  OrderStatus,
8
7
  PositionSide,
8
+ Venue,
9
+ VenueAccountCapabilities,
10
+ VenueMarketCapabilities,
11
+ VenueOrderCapabilities,
9
12
  } from "../types/index.ts";
10
13
 
11
14
  export interface StreamHandle {
@@ -68,7 +71,8 @@ export interface FundingRateStreamOptions {
68
71
  }
69
72
 
70
73
  export interface MarketAdapter {
71
- readonly exchange: Exchange;
74
+ readonly venue: Venue;
75
+ readonly marketCapabilities: VenueMarketCapabilities;
72
76
  loadMarkets(): Promise<MarketDefinition[]>;
73
77
  createL1BookStream(
74
78
  market: MarketDefinition,
@@ -89,6 +93,16 @@ export interface RawBalanceUpdate {
89
93
  total?: string;
90
94
  exchangeTs?: number;
91
95
  receivedAt: number;
96
+ lending?: RawLendingBalanceUpdate;
97
+ }
98
+
99
+ export interface RawLendingBalanceUpdate {
100
+ supplied: string;
101
+ borrowed: string;
102
+ interest: string;
103
+ netAsset: string;
104
+ supplyAPY?: string;
105
+ borrowAPY?: string;
92
106
  }
93
107
 
94
108
  export interface RawPositionUpdate {
@@ -106,11 +120,21 @@ export interface RawPositionUpdate {
106
120
 
107
121
  export interface RawRiskUpdate {
108
122
  equity?: string;
109
- marginRatio?: string;
123
+ riskRatio?: string;
110
124
  initialMargin?: string;
111
125
  maintenanceMargin?: string;
112
126
  exchangeTs?: number;
113
127
  receivedAt: number;
128
+ lending?: RawLendingRiskUpdate;
129
+ }
130
+
131
+ export interface RawLendingRiskUpdate {
132
+ marginLevel?: string;
133
+ healthFactor?: string;
134
+ ltv?: string;
135
+ liquidationThreshold?: string;
136
+ totalCollateralUSD?: string;
137
+ totalDebtUSD?: string;
114
138
  }
115
139
 
116
140
  export interface RawAccountBootstrap {
@@ -171,6 +195,7 @@ export interface CancelAllOrdersRequest {
171
195
  }
172
196
 
173
197
  export interface PrivateStreamCallbacks {
198
+ onAccountSnapshot(snapshot: RawAccountBootstrap): void;
174
199
  onAccountUpdate(update: RawAccountUpdate): void;
175
200
  onOrderUpdate(update: RawOrderUpdate): void;
176
201
  onDisconnected(): void;
@@ -183,11 +208,16 @@ export interface PrivateStreamOptions {
183
208
  reconnectDelayMs: number;
184
209
  reconnectMaxDelayMs: number;
185
210
  listenKeyKeepAliveMs: number;
211
+ juplendPollIntervalMs?: number;
186
212
  now?: () => number;
187
213
  }
188
214
 
189
215
  export interface PrivateUserDataAdapter {
190
- readonly exchange: Exchange;
216
+ readonly venue: Venue;
217
+ readonly readOnly: boolean;
218
+ readonly notes: string[];
219
+ readonly accountCapabilities: VenueAccountCapabilities;
220
+ readonly orderCapabilities: VenueOrderCapabilities;
191
221
  bootstrapAccount(
192
222
  credentials: AccountCredentials,
193
223
  accountOptions?: Record<string, unknown>,
@@ -9,15 +9,15 @@ import type {
9
9
  CancelAllOrdersInput,
10
10
  CancelOrderInput,
11
11
  CreateOrderInput,
12
- Exchange,
13
12
  HealthEvent,
14
13
  PrivateRuntimeReason,
15
14
  PrivateRuntimeStatus,
15
+ Venue,
16
16
  } from "../types/index.ts";
17
17
 
18
18
  export interface RegisteredAccountRecord {
19
19
  accountId: string;
20
- exchange: Exchange;
20
+ venue: Venue;
21
21
  credentials?: AccountCredentials;
22
22
  options?: Record<string, unknown>;
23
23
  }
@@ -49,7 +49,7 @@ export interface ManagerLifecycle {
49
49
 
50
50
  export interface AccountAwareManager {
51
51
  onAccountRemoved(accountId: string, now: number): void;
52
- onCredentialsUpdated(accountId: string, exchange: Exchange): void;
52
+ onCredentialsUpdated(accountId: string, venue: Venue): void;
53
53
  }
54
54
 
55
55
  export interface HealthReporter<T> {
@@ -65,46 +65,51 @@ export interface PrivateSubscriptionState {
65
65
  }
66
66
 
67
67
  export interface PrivateAccountDataConsumer {
68
- onPrivateAccountPending(accountId: string, exchange: Exchange): void;
68
+ onPrivateAccountPending(accountId: string, venue: Venue): void;
69
69
  onPrivateAccountBootstrap(
70
70
  accountId: string,
71
- exchange: Exchange,
71
+ venue: Venue,
72
72
  bootstrap: RawAccountBootstrap,
73
73
  ): void;
74
74
  onPrivateAccountUpdate(
75
75
  accountId: string,
76
- exchange: Exchange,
76
+ venue: Venue,
77
77
  update: RawAccountUpdate,
78
78
  ): void;
79
79
  onPrivateAccountStreamState(
80
80
  accountId: string,
81
- exchange: Exchange,
81
+ venue: Venue,
82
82
  state: PrivateSubscriptionState,
83
83
  ): void;
84
84
  }
85
85
 
86
86
  export interface PrivateOrderDataConsumer {
87
- onPrivateOrderPending(accountId: string, exchange: Exchange): void;
87
+ onPrivateOrderPending(accountId: string, venue: Venue): void;
88
88
  onPrivateOrderBootstrap(
89
89
  accountId: string,
90
- exchange: Exchange,
90
+ venue: Venue,
91
91
  snapshots: RawOrderUpdate[],
92
92
  ): void;
93
93
  onPrivateOrderUpdate(
94
94
  accountId: string,
95
- exchange: Exchange,
95
+ venue: Venue,
96
96
  update: RawOrderUpdate,
97
97
  ): void;
98
98
  onPrivateOrderStreamState(
99
99
  accountId: string,
100
- exchange: Exchange,
100
+ venue: Venue,
101
101
  state: PrivateSubscriptionState,
102
102
  ): void;
103
103
  }
104
104
 
105
105
  export function hasPrivateCredentials(
106
106
  credentials?: AccountCredentials,
107
+ venue?: Venue,
107
108
  ): boolean {
109
+ if (venue === "juplend") {
110
+ return Boolean(credentials?.apiKey);
111
+ }
112
+
108
113
  return Boolean(credentials?.apiKey && credentials.secret);
109
114
  }
110
115
 
@@ -3,7 +3,7 @@ import type {
3
3
  StreamHandle,
4
4
  } from "../adapters/types.ts";
5
5
  import { AcexError } from "../errors.ts";
6
- import type { AccountRuntimeOptions, Exchange } from "../types/index.ts";
6
+ import type { AccountRuntimeOptions, Venue } from "../types/index.ts";
7
7
  import type {
8
8
  ClientContext,
9
9
  PrivateAccountDataConsumer,
@@ -13,7 +13,7 @@ import type {
13
13
 
14
14
  interface PrivateSubscriptionRecord {
15
15
  accountId: string;
16
- exchange: Exchange;
16
+ venue: Venue;
17
17
  accountSubscribed: boolean;
18
18
  ordersSubscribed: boolean;
19
19
  accountReady: boolean;
@@ -30,24 +30,27 @@ const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
30
30
 
31
31
  export class PrivateSubscriptionCoordinator {
32
32
  private readonly context: ClientContext;
33
- private readonly adapter: PrivateUserDataAdapter;
33
+ private readonly adapters: Map<Venue, PrivateUserDataAdapter>;
34
34
  private readonly accountConsumer: PrivateAccountDataConsumer;
35
35
  private readonly orderConsumer: PrivateOrderDataConsumer;
36
36
  private readonly streamOpenTimeoutMs: number;
37
37
  private readonly streamReconnectDelayMs: number;
38
38
  private readonly streamReconnectMaxDelayMs: number;
39
39
  private readonly listenKeyKeepAliveMs: number;
40
+ private readonly juplendPollIntervalMs?: number;
40
41
  private readonly records = new Map<string, PrivateSubscriptionRecord>();
41
42
 
42
43
  constructor(
43
44
  context: ClientContext,
44
- adapter: PrivateUserDataAdapter,
45
+ adapters: PrivateUserDataAdapter[],
45
46
  accountConsumer: PrivateAccountDataConsumer,
46
47
  orderConsumer: PrivateOrderDataConsumer,
47
48
  options: AccountRuntimeOptions = {},
48
49
  ) {
49
50
  this.context = context;
50
- this.adapter = adapter;
51
+ this.adapters = new Map(
52
+ adapters.map((adapter) => [adapter.venue, adapter]),
53
+ );
51
54
  this.accountConsumer = accountConsumer;
52
55
  this.orderConsumer = orderConsumer;
53
56
  this.streamOpenTimeoutMs =
@@ -59,6 +62,7 @@ export class PrivateSubscriptionCoordinator {
59
62
  DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
60
63
  this.listenKeyKeepAliveMs =
61
64
  options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
65
+ this.juplendPollIntervalMs = options.juplend?.pollIntervalMs;
62
66
  }
63
67
 
64
68
  async subscribeAccountFeed(accountId: string): Promise<void> {
@@ -67,12 +71,17 @@ export class PrivateSubscriptionCoordinator {
67
71
  const needsPending = !record.stream && !record.startPromise;
68
72
  record.accountSubscribed = true;
69
73
  if (needsPending) {
70
- this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
74
+ this.accountConsumer.onPrivateAccountPending(accountId, record.venue);
71
75
  }
72
76
 
73
77
  try {
74
- await this.ensureStream(record, account);
75
- await this.bootstrapAccount(record, account);
78
+ if (record.venue === "juplend") {
79
+ await this.bootstrapAccount(record, account);
80
+ await this.ensureStream(record, account);
81
+ } else {
82
+ await this.ensureStream(record, account);
83
+ await this.bootstrapAccount(record, account);
84
+ }
76
85
  } catch (error) {
77
86
  record.accountSubscribed = false;
78
87
  this.closeIfUnused(record);
@@ -96,7 +105,7 @@ export class PrivateSubscriptionCoordinator {
96
105
  const needsPending = !record.stream && !record.startPromise;
97
106
  record.ordersSubscribed = true;
98
107
  if (needsPending) {
99
- this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
108
+ this.orderConsumer.onPrivateOrderPending(accountId, record.venue);
100
109
  }
101
110
 
102
111
  try {
@@ -128,13 +137,13 @@ export class PrivateSubscriptionCoordinator {
128
137
  if (record.accountSubscribed) {
129
138
  this.accountConsumer.onPrivateAccountPending(
130
139
  record.accountId,
131
- record.exchange,
140
+ record.venue,
132
141
  );
133
142
  }
134
143
  if (record.ordersSubscribed) {
135
144
  this.orderConsumer.onPrivateOrderPending(
136
145
  record.accountId,
137
- record.exchange,
146
+ record.venue,
138
147
  );
139
148
  }
140
149
 
@@ -165,10 +174,10 @@ export class PrivateSubscriptionCoordinator {
165
174
  }
166
175
 
167
176
  if (record.accountSubscribed) {
168
- this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
177
+ this.accountConsumer.onPrivateAccountPending(accountId, record.venue);
169
178
  }
170
179
  if (record.ordersSubscribed) {
171
- this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
180
+ this.orderConsumer.onPrivateOrderPending(accountId, record.venue);
172
181
  }
173
182
 
174
183
  void this.resumeRecord(record);
@@ -179,9 +188,14 @@ export class PrivateSubscriptionCoordinator {
179
188
  this.closeStream(record);
180
189
 
181
190
  try {
182
- await this.ensureStream(record, account);
183
- if (record.accountSubscribed) {
191
+ if (record.venue === "juplend" && record.accountSubscribed) {
184
192
  await this.bootstrapAccount(record, account);
193
+ await this.ensureStream(record, account);
194
+ } else {
195
+ await this.ensureStream(record, account);
196
+ if (record.accountSubscribed) {
197
+ await this.bootstrapAccount(record, account);
198
+ }
185
199
  }
186
200
  if (record.ordersSubscribed) {
187
201
  await this.bootstrapOrders(record, account);
@@ -193,16 +207,28 @@ export class PrivateSubscriptionCoordinator {
193
207
 
194
208
  private getAccount(accountId: string): RegisteredAccountRecord {
195
209
  const account = this.context.getRegisteredAccount(accountId);
196
- if (account.exchange !== this.adapter.exchange) {
210
+ if (!this.adapters.has(account.venue)) {
197
211
  throw new AcexError(
198
- "EXCHANGE_NOT_SUPPORTED",
199
- `Exchange is not supported yet: ${account.exchange}`,
212
+ "VENUE_NOT_SUPPORTED",
213
+ `Venue is not supported yet: ${account.venue}`,
200
214
  );
201
215
  }
202
216
 
203
217
  return account;
204
218
  }
205
219
 
220
+ private getAdapter(venue: Venue): PrivateUserDataAdapter {
221
+ const adapter = this.adapters.get(venue);
222
+ if (!adapter) {
223
+ throw new AcexError(
224
+ "VENUE_NOT_SUPPORTED",
225
+ `Venue is not supported yet: ${venue}`,
226
+ );
227
+ }
228
+
229
+ return adapter;
230
+ }
231
+
206
232
  private getOrCreateRecord(
207
233
  account: RegisteredAccountRecord,
208
234
  ): PrivateSubscriptionRecord {
@@ -213,7 +239,7 @@ export class PrivateSubscriptionCoordinator {
213
239
 
214
240
  const record: PrivateSubscriptionRecord = {
215
241
  accountId: account.accountId,
216
- exchange: account.exchange,
242
+ venue: account.venue,
217
243
  accountSubscribed: false,
218
244
  ordersSubscribed: false,
219
245
  accountReady: false,
@@ -275,9 +301,22 @@ export class PrivateSubscriptionCoordinator {
275
301
  );
276
302
  }
277
303
 
278
- const stream = this.adapter.createPrivateStream(
304
+ const adapter = this.getAdapter(record.venue);
305
+ const stream = adapter.createPrivateStream(
279
306
  credentials,
280
307
  {
308
+ onAccountSnapshot: (snapshot) => {
309
+ if (!record.accountSubscribed) {
310
+ return;
311
+ }
312
+
313
+ record.accountReady = true;
314
+ this.accountConsumer.onPrivateAccountBootstrap(
315
+ record.accountId,
316
+ record.venue,
317
+ snapshot,
318
+ );
319
+ },
281
320
  onAccountUpdate: (update) => {
282
321
  if (!record.accountSubscribed) {
283
322
  return;
@@ -286,7 +325,7 @@ export class PrivateSubscriptionCoordinator {
286
325
  record.accountReady = true;
287
326
  this.accountConsumer.onPrivateAccountUpdate(
288
327
  record.accountId,
289
- record.exchange,
328
+ record.venue,
290
329
  update,
291
330
  );
292
331
  },
@@ -298,7 +337,7 @@ export class PrivateSubscriptionCoordinator {
298
337
  record.orderReady = true;
299
338
  this.orderConsumer.onPrivateOrderUpdate(
300
339
  record.accountId,
301
- record.exchange,
340
+ record.venue,
302
341
  update,
303
342
  );
304
343
  },
@@ -306,7 +345,7 @@ export class PrivateSubscriptionCoordinator {
306
345
  if (record.accountSubscribed) {
307
346
  this.accountConsumer.onPrivateAccountStreamState(
308
347
  record.accountId,
309
- record.exchange,
348
+ record.venue,
310
349
  {
311
350
  runtimeStatus: "reconnecting",
312
351
  ready: record.accountReady,
@@ -317,7 +356,7 @@ export class PrivateSubscriptionCoordinator {
317
356
  if (record.ordersSubscribed) {
318
357
  this.orderConsumer.onPrivateOrderStreamState(
319
358
  record.accountId,
320
- record.exchange,
359
+ record.venue,
321
360
  {
322
361
  runtimeStatus: "reconnecting",
323
362
  ready: record.orderReady,
@@ -338,8 +377,19 @@ export class PrivateSubscriptionCoordinator {
338
377
  onError: (error) => {
339
378
  this.context.publishRuntimeError("adapter", error, {
340
379
  accountId: record.accountId,
341
- exchange: record.exchange,
380
+ venue: record.venue,
342
381
  });
382
+ if (record.accountSubscribed) {
383
+ this.accountConsumer.onPrivateAccountStreamState(
384
+ record.accountId,
385
+ record.venue,
386
+ {
387
+ runtimeStatus: "degraded",
388
+ ready: record.accountReady,
389
+ reason: "http_failed",
390
+ },
391
+ );
392
+ }
343
393
  },
344
394
  },
345
395
  {
@@ -347,6 +397,7 @@ export class PrivateSubscriptionCoordinator {
347
397
  reconnectDelayMs: this.streamReconnectDelayMs,
348
398
  reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
349
399
  listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
400
+ juplendPollIntervalMs: this.juplendPollIntervalMs,
350
401
  now: () => this.context.now(),
351
402
  },
352
403
  account.options,
@@ -361,16 +412,16 @@ export class PrivateSubscriptionCoordinator {
361
412
  const runtimeError =
362
413
  error instanceof Error
363
414
  ? error
364
- : new Error("Failed to open Binance private stream");
415
+ : new Error(`Failed to open ${record.venue} private stream`);
365
416
  this.context.publishRuntimeError("adapter", runtimeError, {
366
417
  accountId: record.accountId,
367
- exchange: record.exchange,
418
+ venue: record.venue,
368
419
  });
369
420
 
370
421
  if (record.accountSubscribed) {
371
422
  this.accountConsumer.onPrivateAccountStreamState(
372
423
  record.accountId,
373
- record.exchange,
424
+ record.venue,
374
425
  {
375
426
  runtimeStatus: "degraded",
376
427
  ready: record.accountReady,
@@ -381,7 +432,7 @@ export class PrivateSubscriptionCoordinator {
381
432
  if (record.ordersSubscribed) {
382
433
  this.orderConsumer.onPrivateOrderStreamState(
383
434
  record.accountId,
384
- record.exchange,
435
+ record.venue,
385
436
  {
386
437
  runtimeStatus: "degraded",
387
438
  ready: record.orderReady,
@@ -402,16 +453,13 @@ export class PrivateSubscriptionCoordinator {
402
453
  if (record.accountSubscribed) {
403
454
  this.accountConsumer.onPrivateAccountPending(
404
455
  record.accountId,
405
- record.exchange,
456
+ record.venue,
406
457
  );
407
458
  await this.bootstrapAccount(record, account);
408
459
  }
409
460
 
410
461
  if (record.ordersSubscribed) {
411
- this.orderConsumer.onPrivateOrderPending(
412
- record.accountId,
413
- record.exchange,
414
- );
462
+ this.orderConsumer.onPrivateOrderPending(record.accountId, record.venue);
415
463
  await this.bootstrapOrders(record, account);
416
464
  }
417
465
  }
@@ -421,9 +469,9 @@ export class PrivateSubscriptionCoordinator {
421
469
  account: RegisteredAccountRecord,
422
470
  ): Promise<void> {
423
471
  try {
424
- const bootstrap = await this.adapter.bootstrapAccount(
472
+ const bootstrap = await this.getAdapter(record.venue).bootstrapAccount(
425
473
  account.credentials ?? {},
426
- account.options,
474
+ { ...account.options, accountId: account.accountId },
427
475
  );
428
476
  if (!record.accountSubscribed) {
429
477
  return;
@@ -432,7 +480,7 @@ export class PrivateSubscriptionCoordinator {
432
480
  record.accountReady = true;
433
481
  this.accountConsumer.onPrivateAccountBootstrap(
434
482
  record.accountId,
435
- record.exchange,
483
+ record.venue,
436
484
  bootstrap,
437
485
  );
438
486
  } catch (error) {
@@ -441,24 +489,28 @@ export class PrivateSubscriptionCoordinator {
441
489
  "adapter",
442
490
  error instanceof Error
443
491
  ? error
444
- : new Error("Failed to bootstrap Binance private account state"),
492
+ : new Error(
493
+ `Failed to bootstrap ${record.venue} private account state`,
494
+ ),
445
495
  {
446
496
  accountId: record.accountId,
447
- exchange: record.exchange,
497
+ venue: record.venue,
448
498
  },
449
499
  );
450
500
  this.accountConsumer.onPrivateAccountStreamState(
451
501
  record.accountId,
452
- record.exchange,
502
+ record.venue,
453
503
  {
454
504
  runtimeStatus: "degraded",
455
505
  ready: false,
456
- reason: "auth_failed",
506
+ reason: record.venue === "juplend" ? "http_failed" : "auth_failed",
457
507
  },
458
508
  );
509
+ const reason =
510
+ error instanceof Error && error.message ? ` (${error.message})` : "";
459
511
  throw new AcexError(
460
512
  "ACCOUNT_BOOTSTRAP_FAILED",
461
- `Failed to bootstrap account data: ${record.accountId}`,
513
+ `Failed to bootstrap account data: ${record.accountId}${reason}`,
462
514
  );
463
515
  }
464
516
  }
@@ -468,7 +520,7 @@ export class PrivateSubscriptionCoordinator {
468
520
  account: RegisteredAccountRecord,
469
521
  ): Promise<void> {
470
522
  try {
471
- const snapshots = await this.adapter.bootstrapOpenOrders(
523
+ const snapshots = await this.getAdapter(record.venue).bootstrapOpenOrders(
472
524
  account.credentials ?? {},
473
525
  account.options,
474
526
  );
@@ -479,7 +531,7 @@ export class PrivateSubscriptionCoordinator {
479
531
  record.orderReady = true;
480
532
  this.orderConsumer.onPrivateOrderBootstrap(
481
533
  record.accountId,
482
- record.exchange,
534
+ record.venue,
483
535
  snapshots,
484
536
  );
485
537
  } catch (error) {
@@ -488,15 +540,17 @@ export class PrivateSubscriptionCoordinator {
488
540
  "adapter",
489
541
  error instanceof Error
490
542
  ? error
491
- : new Error("Failed to bootstrap Binance private order state"),
543
+ : new Error(
544
+ `Failed to bootstrap ${record.venue} private order state`,
545
+ ),
492
546
  {
493
547
  accountId: record.accountId,
494
- exchange: record.exchange,
548
+ venue: record.venue,
495
549
  },
496
550
  );
497
551
  this.orderConsumer.onPrivateOrderStreamState(
498
552
  record.accountId,
499
- record.exchange,
553
+ record.venue,
500
554
  {
501
555
  runtimeStatus: "degraded",
502
556
  ready: false,