@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.10

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.
@@ -15,7 +15,12 @@ import type {
15
15
  PrivateSubscriptionState,
16
16
  } from "../client/context.ts";
17
17
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
18
+ import { toCanonical } from "../internal/decimal.ts";
18
19
  import { matchesAccountFilter } from "../internal/filters.ts";
20
+ import {
21
+ canDeleteMissingFromSnapshot,
22
+ shouldApplyWatermarkedUpdate,
23
+ } from "../internal/watermark.ts";
19
24
  import type {
20
25
  AccountDataStatus,
21
26
  AccountEvent,
@@ -56,6 +61,50 @@ function getBigNumber(
56
61
  return value === undefined ? fallback : new BigNumber(value);
57
62
  }
58
63
 
64
+ function isZeroDecimal(value: string): boolean {
65
+ return new BigNumber(value).isZero();
66
+ }
67
+
68
+ function isZeroBalance(balance: BalanceSnapshot): boolean {
69
+ return (
70
+ isZeroDecimal(balance.free) &&
71
+ isZeroDecimal(balance.used) &&
72
+ isZeroDecimal(balance.total)
73
+ );
74
+ }
75
+
76
+ function successfulStatus(
77
+ status: AccountDataStatus,
78
+ options: {
79
+ ready?: boolean;
80
+ lastReceivedAt?: number;
81
+ lastReadyAt?: number;
82
+ preserveStatus?: boolean;
83
+ },
84
+ ): AccountDataStatus {
85
+ const preservesStreamState =
86
+ options.preserveStatus &&
87
+ (status.runtimeStatus === "reconnecting" ||
88
+ status.reason === "ws_disconnected" ||
89
+ status.reason === "heartbeat_timeout");
90
+ const ready = options.ready ?? true;
91
+
92
+ return {
93
+ ...status,
94
+ activity: "active",
95
+ ready,
96
+ runtimeStatus: preservesStreamState ? status.runtimeStatus : "healthy",
97
+ reason: preservesStreamState ? status.reason : undefined,
98
+ lastReceivedAt: options.lastReceivedAt ?? status.lastReceivedAt,
99
+ lastReadyAt: ready
100
+ ? (options.lastReadyAt ??
101
+ (options.preserveStatus ? status.lastReadyAt : undefined) ??
102
+ Date.now())
103
+ : status.lastReadyAt,
104
+ inactiveSince: undefined,
105
+ };
106
+ }
107
+
59
108
  export class AccountManagerImpl
60
109
  implements
61
110
  AccountManager,
@@ -277,7 +326,7 @@ export class AccountManagerImpl
277
326
  accountId: string,
278
327
  venue: Venue,
279
328
  update: RawAccountUpdate,
280
- options: { preserveStatus?: boolean } = {},
329
+ options: { preserveStatus?: boolean; requestStartedAt?: number } = {},
281
330
  ): void {
282
331
  const record = this.getOrCreateRecord(accountId, venue);
283
332
  if (!record.subscribed) {
@@ -295,7 +344,17 @@ export class AccountManagerImpl
295
344
  );
296
345
  let risk = previous.risk;
297
346
 
347
+ let latestAppliedAt = 0;
298
348
  for (const balance of update.balances ?? []) {
349
+ if (
350
+ !shouldApplyWatermarkedUpdate(balances[balance.asset], balance, {
351
+ requestStartedAt: options.requestStartedAt,
352
+ source: options.requestStartedAt === undefined ? "stream" : "rest",
353
+ })
354
+ ) {
355
+ continue;
356
+ }
357
+
299
358
  const nextBalance = this.createBalance(
300
359
  accountId,
301
360
  venue,
@@ -303,6 +362,7 @@ export class AccountManagerImpl
303
362
  balances[balance.asset],
304
363
  );
305
364
  balances[balance.asset] = nextBalance;
365
+ latestAppliedAt = Math.max(latestAppliedAt, nextBalance.receivedAt);
306
366
  this.accountBus.publish({
307
367
  type: "balance.updated",
308
368
  accountId,
@@ -315,6 +375,15 @@ export class AccountManagerImpl
315
375
 
316
376
  for (const position of update.positions ?? []) {
317
377
  const key = positionKey(position.symbol, position.side);
378
+ if (
379
+ !shouldApplyWatermarkedUpdate(positions.get(key), position, {
380
+ requestStartedAt: options.requestStartedAt,
381
+ source: options.requestStartedAt === undefined ? "stream" : "rest",
382
+ })
383
+ ) {
384
+ continue;
385
+ }
386
+
318
387
  const nextPosition = this.createPosition(
319
388
  accountId,
320
389
  venue,
@@ -322,12 +391,13 @@ export class AccountManagerImpl
322
391
  positions.get(key),
323
392
  );
324
393
 
325
- if (nextPosition.size.isZero()) {
394
+ if (isZeroDecimal(nextPosition.size)) {
326
395
  positions.delete(key);
327
396
  } else {
328
397
  positions.set(key, nextPosition);
329
398
  }
330
399
 
400
+ latestAppliedAt = Math.max(latestAppliedAt, nextPosition.receivedAt);
331
401
  this.accountBus.publish({
332
402
  type: "position.updated",
333
403
  accountId,
@@ -338,8 +408,15 @@ export class AccountManagerImpl
338
408
  });
339
409
  }
340
410
 
341
- if (update.risk) {
411
+ if (
412
+ update.risk &&
413
+ shouldApplyWatermarkedUpdate(previous.risk, update.risk, {
414
+ requestStartedAt: options.requestStartedAt,
415
+ source: options.requestStartedAt === undefined ? "stream" : "rest",
416
+ })
417
+ ) {
342
418
  risk = this.createRisk(accountId, venue, update.risk, previous.risk);
419
+ latestAppliedAt = Math.max(latestAppliedAt, risk.receivedAt);
343
420
  this.accountBus.publish({
344
421
  type: "risk.updated",
345
422
  accountId,
@@ -349,34 +426,166 @@ export class AccountManagerImpl
349
426
  });
350
427
  }
351
428
 
429
+ if (latestAppliedAt === 0) {
430
+ return;
431
+ }
432
+
433
+ record.snapshot = {
434
+ accountId,
435
+ venue,
436
+ balances,
437
+ positions: [...positions.values()],
438
+ risk,
439
+ exchangeTs:
440
+ update.exchangeTs === undefined
441
+ ? previous.exchangeTs
442
+ : update.exchangeTs,
443
+ receivedAt: latestAppliedAt,
444
+ updatedAt: latestAppliedAt,
445
+ };
446
+ record.status = successfulStatus(record.status, {
447
+ preserveStatus: options.preserveStatus,
448
+ lastReceivedAt: latestAppliedAt,
449
+ lastReadyAt: latestAppliedAt,
450
+ });
451
+ this.publishStatus(record);
452
+ }
453
+
454
+ onPrivateAccountReconcile(
455
+ accountId: string,
456
+ venue: Venue,
457
+ snapshot: RawAccountBootstrap,
458
+ options: { requestStartedAt: number; preserveStatus?: boolean },
459
+ ): void {
460
+ const record = this.getOrCreateRecord(accountId, venue);
461
+ if (!record.subscribed) {
462
+ return;
463
+ }
464
+
465
+ const previous =
466
+ record.snapshot ?? this.createEmptySnapshot(accountId, venue);
467
+ const balances = { ...previous.balances };
468
+ const positions = new Map(
469
+ previous.positions.map((position) => [
470
+ positionKey(position.symbol, position.side),
471
+ position,
472
+ ]),
473
+ );
474
+ let risk = previous.risk;
475
+
476
+ const incomingBalanceAssets = new Set<string>();
477
+ for (const balance of snapshot.balances) {
478
+ incomingBalanceAssets.add(balance.asset);
479
+ if (
480
+ !shouldApplyWatermarkedUpdate(balances[balance.asset], balance, {
481
+ requestStartedAt: options.requestStartedAt,
482
+ source: "rest",
483
+ })
484
+ ) {
485
+ continue;
486
+ }
487
+
488
+ const nextBalance = this.createBalance(
489
+ accountId,
490
+ venue,
491
+ balance,
492
+ balances[balance.asset],
493
+ );
494
+ if (isZeroBalance(nextBalance)) {
495
+ delete balances[balance.asset];
496
+ } else {
497
+ balances[balance.asset] = nextBalance;
498
+ }
499
+ }
500
+
501
+ for (const [asset, balance] of Object.entries(balances)) {
502
+ if (
503
+ (!incomingBalanceAssets.has(asset) || isZeroBalance(balance)) &&
504
+ canDeleteMissingFromSnapshot(balance, {
505
+ requestStartedAt: options.requestStartedAt,
506
+ snapshotExchangeTs: snapshot.exchangeTs,
507
+ })
508
+ ) {
509
+ delete balances[asset];
510
+ }
511
+ }
512
+
513
+ const incomingPositionKeys = new Set<string>();
514
+ for (const position of snapshot.positions) {
515
+ const key = positionKey(position.symbol, position.side);
516
+ incomingPositionKeys.add(key);
517
+ if (
518
+ !shouldApplyWatermarkedUpdate(positions.get(key), position, {
519
+ requestStartedAt: options.requestStartedAt,
520
+ source: "rest",
521
+ })
522
+ ) {
523
+ continue;
524
+ }
525
+
526
+ const nextPosition = this.createPosition(
527
+ accountId,
528
+ venue,
529
+ position,
530
+ positions.get(key),
531
+ );
532
+ if (isZeroDecimal(nextPosition.size)) {
533
+ positions.delete(key);
534
+ } else {
535
+ positions.set(key, nextPosition);
536
+ }
537
+ }
538
+
539
+ for (const [key, position] of positions.entries()) {
540
+ if (
541
+ !incomingPositionKeys.has(key) &&
542
+ canDeleteMissingFromSnapshot(position, {
543
+ requestStartedAt: options.requestStartedAt,
544
+ snapshotExchangeTs: snapshot.exchangeTs,
545
+ })
546
+ ) {
547
+ positions.delete(key);
548
+ }
549
+ }
550
+
551
+ if (
552
+ snapshot.risk &&
553
+ shouldApplyWatermarkedUpdate(previous.risk, snapshot.risk, {
554
+ requestStartedAt: options.requestStartedAt,
555
+ source: "rest",
556
+ })
557
+ ) {
558
+ risk = this.createRisk(accountId, venue, snapshot.risk, previous.risk);
559
+ }
560
+
352
561
  record.snapshot = {
353
562
  accountId,
354
563
  venue,
355
564
  balances,
356
565
  positions: [...positions.values()],
357
566
  risk,
358
- exchangeTs: update.exchangeTs ?? previous.exchangeTs,
359
- receivedAt: update.receivedAt,
360
- updatedAt: update.receivedAt,
567
+ exchangeTs:
568
+ snapshot.exchangeTs === undefined
569
+ ? previous.exchangeTs
570
+ : snapshot.exchangeTs,
571
+ receivedAt: snapshot.receivedAt,
572
+ updatedAt: snapshot.receivedAt,
361
573
  };
362
- record.status = options.preserveStatus
363
- ? {
364
- ...record.status,
365
- activity: "active",
366
- lastReceivedAt: update.receivedAt,
367
- lastReadyAt: record.status.lastReadyAt ?? update.receivedAt,
368
- inactiveSince: undefined,
369
- }
370
- : {
371
- ...record.status,
372
- activity: "active",
373
- ready: true,
374
- runtimeStatus: "healthy",
375
- reason: undefined,
376
- lastReceivedAt: update.receivedAt,
377
- lastReadyAt: update.receivedAt,
378
- inactiveSince: undefined,
379
- };
574
+ record.status = successfulStatus(record.status, {
575
+ preserveStatus: options.preserveStatus,
576
+ lastReceivedAt: snapshot.receivedAt,
577
+ lastReadyAt: snapshot.receivedAt,
578
+ });
579
+
580
+ const event: AccountSnapshotReplacedEvent = {
581
+ type: "account.snapshot_replaced",
582
+ accountId,
583
+ venue,
584
+ snapshot: record.snapshot,
585
+ ts: this.context.now(),
586
+ };
587
+
588
+ this.accountBus.publish(event);
380
589
  this.publishStatus(record);
381
590
  }
382
591
 
@@ -461,7 +670,7 @@ export class AccountManagerImpl
461
670
  );
462
671
  const positions = bootstrap.positions
463
672
  .map((position) => this.createPosition(accountId, venue, position))
464
- .filter((position) => !position.size.isZero());
673
+ .filter((position) => !isZeroDecimal(position.size));
465
674
  const risk = bootstrap.risk
466
675
  ? this.createRisk(accountId, venue, bootstrap.risk)
467
676
  : undefined;
@@ -499,9 +708,12 @@ export class AccountManagerImpl
499
708
  input: RawBalanceUpdate,
500
709
  previous?: BalanceSnapshot,
501
710
  ): BalanceSnapshot {
502
- const previousFree = previous?.free ?? new BigNumber(0);
503
- const previousUsed = previous?.used ?? new BigNumber(0);
504
- const previousTotal = previous?.total ?? previousFree.plus(previousUsed);
711
+ const previousFree = new BigNumber(previous?.free ?? 0);
712
+ const previousUsed = new BigNumber(previous?.used ?? 0);
713
+ const previousTotal =
714
+ previous?.total === undefined
715
+ ? previousFree.plus(previousUsed)
716
+ : new BigNumber(previous.total);
505
717
  const free = getBigNumber(input.free, previousFree);
506
718
  const total = getBigNumber(input.total, previousTotal);
507
719
  const used =
@@ -515,27 +727,27 @@ export class AccountManagerImpl
515
727
  accountId,
516
728
  venue,
517
729
  asset: input.asset,
518
- free,
519
- used,
520
- total,
730
+ free: toCanonical(free),
731
+ used: toCanonical(used),
732
+ total: toCanonical(total),
521
733
  exchangeTs: input.exchangeTs,
522
734
  receivedAt: input.receivedAt,
523
735
  updatedAt: input.receivedAt,
524
736
  seq: (previous?.seq ?? 0) + 1,
525
737
  lending: input.lending
526
738
  ? {
527
- supplied: new BigNumber(input.lending.supplied),
528
- borrowed: new BigNumber(input.lending.borrowed),
529
- interest: new BigNumber(input.lending.interest),
530
- netAsset: new BigNumber(input.lending.netAsset),
739
+ supplied: toCanonical(input.lending.supplied),
740
+ borrowed: toCanonical(input.lending.borrowed),
741
+ interest: toCanonical(input.lending.interest),
742
+ netAsset: toCanonical(input.lending.netAsset),
531
743
  supplyAPY:
532
744
  input.lending.supplyAPY === undefined
533
745
  ? undefined
534
- : new BigNumber(input.lending.supplyAPY),
746
+ : toCanonical(input.lending.supplyAPY),
535
747
  borrowAPY:
536
748
  input.lending.borrowAPY === undefined
537
749
  ? undefined
538
- : new BigNumber(input.lending.borrowAPY),
750
+ : toCanonical(input.lending.borrowAPY),
539
751
  }
540
752
  : previous?.lending,
541
753
  };
@@ -552,27 +764,27 @@ export class AccountManagerImpl
552
764
  venue,
553
765
  symbol: input.symbol,
554
766
  side: input.side,
555
- size: new BigNumber(input.size),
767
+ size: toCanonical(input.size),
556
768
  entryPrice:
557
769
  input.entryPrice === undefined
558
770
  ? previous?.entryPrice
559
- : new BigNumber(input.entryPrice),
771
+ : toCanonical(input.entryPrice),
560
772
  markPrice:
561
773
  input.markPrice === undefined
562
774
  ? previous?.markPrice
563
- : new BigNumber(input.markPrice),
775
+ : toCanonical(input.markPrice),
564
776
  unrealizedPnl:
565
777
  input.unrealizedPnl === undefined
566
778
  ? previous?.unrealizedPnl
567
- : new BigNumber(input.unrealizedPnl),
779
+ : toCanonical(input.unrealizedPnl),
568
780
  leverage:
569
781
  input.leverage === undefined
570
782
  ? previous?.leverage
571
- : new BigNumber(input.leverage),
783
+ : toCanonical(input.leverage),
572
784
  liquidationPrice:
573
785
  input.liquidationPrice === undefined
574
786
  ? previous?.liquidationPrice
575
- : new BigNumber(input.liquidationPrice),
787
+ : toCanonical(input.liquidationPrice),
576
788
  exchangeTs: input.exchangeTs,
577
789
  receivedAt: input.receivedAt,
578
790
  updatedAt: input.receivedAt,
@@ -592,27 +804,27 @@ export class AccountManagerImpl
592
804
  netEquity:
593
805
  input.netEquity === undefined
594
806
  ? previous?.netEquity
595
- : new BigNumber(input.netEquity),
807
+ : toCanonical(input.netEquity),
596
808
  riskEquity:
597
809
  input.riskEquity === undefined
598
810
  ? previous?.riskEquity
599
- : new BigNumber(input.riskEquity),
811
+ : toCanonical(input.riskEquity),
600
812
  riskRatio:
601
813
  input.riskRatio === undefined
602
814
  ? previous?.riskRatio
603
- : new BigNumber(input.riskRatio),
815
+ : toCanonical(input.riskRatio),
604
816
  riskLeverage:
605
817
  input.riskLeverage === undefined
606
818
  ? previous?.riskLeverage
607
- : new BigNumber(input.riskLeverage),
819
+ : toCanonical(input.riskLeverage),
608
820
  initialMargin:
609
821
  input.initialMargin === undefined
610
822
  ? previous?.initialMargin
611
- : new BigNumber(input.initialMargin),
823
+ : toCanonical(input.initialMargin),
612
824
  maintenanceMargin:
613
825
  input.maintenanceMargin === undefined
614
826
  ? previous?.maintenanceMargin
615
- : new BigNumber(input.maintenanceMargin),
827
+ : toCanonical(input.maintenanceMargin),
616
828
  exchangeTs: input.exchangeTs,
617
829
  receivedAt: input.receivedAt,
618
830
  updatedAt: input.receivedAt,
@@ -622,27 +834,27 @@ export class AccountManagerImpl
622
834
  marginLevel:
623
835
  input.lending.marginLevel === undefined
624
836
  ? undefined
625
- : new BigNumber(input.lending.marginLevel),
837
+ : toCanonical(input.lending.marginLevel),
626
838
  healthFactor:
627
839
  input.lending.healthFactor === undefined
628
840
  ? undefined
629
- : new BigNumber(input.lending.healthFactor),
841
+ : toCanonical(input.lending.healthFactor),
630
842
  ltv:
631
843
  input.lending.ltv === undefined
632
844
  ? undefined
633
- : new BigNumber(input.lending.ltv),
845
+ : toCanonical(input.lending.ltv),
634
846
  liquidationThreshold:
635
847
  input.lending.liquidationThreshold === undefined
636
848
  ? undefined
637
- : new BigNumber(input.lending.liquidationThreshold),
849
+ : toCanonical(input.lending.liquidationThreshold),
638
850
  totalCollateralUSD:
639
851
  input.lending.totalCollateralUSD === undefined
640
852
  ? undefined
641
- : new BigNumber(input.lending.totalCollateralUSD),
853
+ : toCanonical(input.lending.totalCollateralUSD),
642
854
  totalDebtUSD:
643
855
  input.lending.totalDebtUSD === undefined
644
856
  ? undefined
645
- : new BigNumber(input.lending.totalDebtUSD),
857
+ : toCanonical(input.lending.totalDebtUSD),
646
858
  }
647
859
  : previous?.lending,
648
860
  };