@raintree-technology/perps 0.1.3 → 0.1.4

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,12 +15,14 @@ const DECIBEL_DEFAULTS = {
15
15
  restUrl: "https://api.testnet.aptoslabs.com/decibel",
16
16
  wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
17
17
  packageAddress: "0x952535c3049e52f195f26798c2f1340d7dd5100edbe0f464e520a974d16fbe9f",
18
+ usdcAddress: "0xbdabb88aa9a875f3a2ebe0974e24f3ae5e57cfd17c6abdfef8a8111f43681b7e",
18
19
  },
19
20
  mainnet: {
20
21
  fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
21
22
  restUrl: "https://api.mainnet.aptoslabs.com/decibel",
22
23
  wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
23
- packageAddress: "0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06",
24
+ packageAddress: "0xe6683d451db246750f180fb78d9b5e0a855dacba64ddf5810dffdaeb221e46bf",
25
+ usdcAddress: "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b",
24
26
  },
25
27
  };
26
28
  export class DecibelAdapter {
@@ -34,20 +36,20 @@ export class DecibelAdapter {
34
36
  perp: true,
35
37
  margin: true,
36
38
  crossMargin: true,
37
- isolatedMargin: false,
39
+ isolatedMargin: true,
38
40
  stopOrders: true,
39
41
  takeProfitOrders: true,
40
42
  postOnly: true,
41
43
  reduceOnly: true,
42
44
  subaccounts: true,
43
- modifyOrders: false,
45
+ modifyOrders: true,
44
46
  batchOrders: false,
45
47
  cancelAllAfter: false,
46
48
  publicTrades: true,
47
- fundingHistory: false,
49
+ fundingHistory: true,
48
50
  orderHistory: true,
49
51
  mmp: false,
50
- twapOrders: false,
52
+ twapOrders: true,
51
53
  },
52
54
  urls: {
53
55
  app: "https://app.decibel.trade",
@@ -78,6 +80,8 @@ export class DecibelAdapter {
78
80
  assetContextsByMarketAddr = new Map();
79
81
  assetContextsTimestamp = 0;
80
82
  trackedMarketAddrs = new Set();
83
+ depthAggregationSize = 1;
84
+ twapStatusById = new Map();
81
85
  tickerSubscribers = new Map();
82
86
  orderBookSubscribers = new Map();
83
87
  globalSubscribers = new Set();
@@ -92,6 +96,7 @@ export class DecibelAdapter {
92
96
  const wsUrl = config.wsUrl ?? credentials?.wsUrl ?? defaults.wsUrl;
93
97
  const fullnodeUrl = config.rpcUrl ?? credentials?.fullnodeUrl ?? defaults.fullnodeUrl;
94
98
  const packageAddress = credentials?.packageAddress ?? defaults.packageAddress;
99
+ const usdcAddress = credentials?.usdcAddress ?? defaults.usdcAddress;
95
100
  this.accountAddress = credentials?.accountAddress ?? credentials?.subaccountAddress ?? null;
96
101
  this.subaccountAddress = credentials?.subaccountAddress ?? credentials?.accountAddress ?? null;
97
102
  this.restClient = new DecibelRestClient(restUrl, credentials?.apiBearerToken);
@@ -111,6 +116,7 @@ export class DecibelAdapter {
111
116
  this.wsFeed.on("account_open_orders", ({ data }) => this.handleWsAccountOpenOrders(data));
112
117
  this.wsFeed.on("user_trades", ({ data }) => this.handleWsUserTrades(data));
113
118
  this.wsFeed.on("order_updates", ({ data }) => this.handleWsOrderUpdate(data));
119
+ this.wsFeed.on("user_active_twaps", ({ data }) => this.handleWsActiveTwaps(data));
114
120
  this.wsFeed.on("notifications", ({ data }) => this.handleWsNotification(data));
115
121
  this.wsFeed.connect();
116
122
  // Subscribe to all_market_prices for global ticker updates
@@ -122,6 +128,7 @@ export class DecibelAdapter {
122
128
  this.wsFeed.subscribe(`account_open_orders:${this.subaccountAddress}`);
123
129
  this.wsFeed.subscribe(`order_updates:${this.subaccountAddress}`);
124
130
  this.wsFeed.subscribe(`user_trades:${this.subaccountAddress}`);
131
+ this.wsFeed.subscribe(`user_active_twaps:${this.subaccountAddress}`);
125
132
  this.wsFeed.subscribe(`notifications:${this.subaccountAddress}`);
126
133
  }
127
134
  }
@@ -142,6 +149,7 @@ export class DecibelAdapter {
142
149
  fullnodeUrl,
143
150
  network,
144
151
  packageAddress,
152
+ usdcAddress,
145
153
  privateKey: credentials.privateKey,
146
154
  subaccountAddress: this.subaccountAddress,
147
155
  });
@@ -336,15 +344,21 @@ export class DecibelAdapter {
336
344
  const equity = parseNumber(overview.equity ?? overview.perp_equity_balance);
337
345
  const totalCollateral = parseNumber(overview.total_collateral ?? overview.total_margin);
338
346
  const unrealizedPnl = parseNumber(overview.unrealized_pnl);
339
- const marginUsed = Math.max(0, totalCollateral - equity);
347
+ const hasWithdrawable = overview.usdc_cross_withdrawable_balance !== undefined ||
348
+ overview.usdc_isolated_withdrawable_balance !== undefined;
349
+ const withdrawable = parseNumber(overview.usdc_cross_withdrawable_balance) +
350
+ parseNumber(overview.usdc_isolated_withdrawable_balance);
351
+ const derivedLocked = Math.max(0, totalCollateral - equity);
352
+ const available = hasWithdrawable ? Math.max(0, withdrawable) : Math.max(0, equity - derivedLocked);
353
+ const locked = Math.max(0, equity - available);
340
354
  return [
341
355
  {
342
356
  asset: "USD",
343
357
  total: equity.toString(),
344
- available: Math.max(0, equity - marginUsed).toString(),
345
- locked: marginUsed.toString(),
358
+ available: available.toString(),
359
+ locked: locked.toString(),
346
360
  unrealizedPnl: unrealizedPnl.toString(),
347
- marginUsed: marginUsed.toString(),
361
+ marginUsed: locked.toString(),
348
362
  },
349
363
  ];
350
364
  }
@@ -483,8 +497,68 @@ export class DecibelAdapter {
483
497
  // --------------------------------------------------------------------------
484
498
  // Advanced Trading
485
499
  // --------------------------------------------------------------------------
486
- async modifyOrder(_params) {
487
- throw new Error("Decibel does not support order modification. Cancel and replace instead.");
500
+ async modifyOrder(params) {
501
+ this.ensureConnected();
502
+ this.ensureOrderManager();
503
+ const meta = this.requireMarket(params.market);
504
+ const existing = await this.getOrder(params.orderId);
505
+ if (!existing) {
506
+ throw new Error(`Order ${params.orderId} not found on Decibel`);
507
+ }
508
+ if (existing.type === "market") {
509
+ throw new Error("Decibel cannot modify filled/market orders");
510
+ }
511
+ const side = existing.side === "long" ? "buy" : "sell";
512
+ const sizeUnits = parseNumber(params.size ?? existing.size);
513
+ const price = parseNumber(params.price ?? existing.price);
514
+ if (!Number.isFinite(price) || price <= 0) {
515
+ throw new Error("Order price is required for Decibel order modification");
516
+ }
517
+ const timeInForce = existing.postOnly ? 1 : 0;
518
+ let stopPrice;
519
+ let tpTriggerPrice;
520
+ let tpLimitPrice;
521
+ let slTriggerPrice;
522
+ let slLimitPrice;
523
+ if (existing.type === "stop" || existing.type === "stop_limit") {
524
+ stopPrice = parseNumber(params.triggerPrice ?? existing.triggerPrice ?? existing.price);
525
+ if (existing.type === "stop_limit") {
526
+ slTriggerPrice = stopPrice;
527
+ slLimitPrice = price;
528
+ }
529
+ }
530
+ if (existing.type === "take_profit") {
531
+ tpTriggerPrice = parseNumber(params.triggerPrice ?? existing.triggerPrice);
532
+ tpLimitPrice = price;
533
+ }
534
+ const normalizedOrderId = this.normalizeTwapLookupId(params.orderId) ?? params.orderId;
535
+ const hasNumericOrderId = /^\d+$/.test(normalizedOrderId);
536
+ const txHash = await this.orderManager.updateOrder({
537
+ market: meta,
538
+ orderId: hasNumericOrderId ? normalizedOrderId : undefined,
539
+ clientOrderId: hasNumericOrderId ? undefined : normalizedOrderId,
540
+ side,
541
+ price,
542
+ sizeUnits,
543
+ timeInForce,
544
+ reduceOnly: existing.reduceOnly,
545
+ stopPrice,
546
+ tpTriggerPrice,
547
+ tpLimitPrice,
548
+ slTriggerPrice,
549
+ slLimitPrice,
550
+ });
551
+ const lookupId = hasNumericOrderId
552
+ ? normalizedOrderId
553
+ : existing.id;
554
+ for (let attempt = 0; attempt < 5; attempt++) {
555
+ const updated = await this.getOrder(lookupId);
556
+ if (updated) {
557
+ return updated;
558
+ }
559
+ await sleep(250);
560
+ }
561
+ throw new Error(`Decibel order updated (tx ${txHash}) but updated state is not yet queryable`);
488
562
  }
489
563
  async batchPlaceOrders(paramsList) {
490
564
  const results = [];
@@ -505,9 +579,6 @@ export class DecibelAdapter {
505
579
  }
506
580
  return results;
507
581
  }
508
- async cancelAllAfter(_timeoutMs) {
509
- throw new Error("Decibel does not support cancelAllAfter (dead man's switch)");
510
- }
511
582
  async getOrderHistory(market, limit = 100) {
512
583
  this.ensureConnected();
513
584
  this.ensureAccountReadAuth();
@@ -521,9 +592,34 @@ export class DecibelAdapter {
521
592
  }
522
593
  return orders;
523
594
  }
524
- async getFundingHistory(_market, _limit) {
525
- // Decibel does not expose a funding payment history endpoint
526
- return [];
595
+ async getFundingHistory(market, limit = 100) {
596
+ this.ensureConnected();
597
+ this.ensureAccountReadAuth();
598
+ const maxRows = Math.max(1, Math.min(limit, 500));
599
+ const targetMarket = market ? this.requireMarket(market) : null;
600
+ const rows = await this.restClient.getFundingHistory(this.accountAddress, maxRows, 0);
601
+ return rows
602
+ .filter((row) => {
603
+ if (!targetMarket)
604
+ return true;
605
+ const marketRaw = row.market ?? "";
606
+ if (marketRaw === targetMarket.marketAddr)
607
+ return true;
608
+ return this.resolveMarketSymbol(marketRaw) === targetMarket.symbol;
609
+ })
610
+ .map((row) => {
611
+ const amountRaw = parseNumber(row.realized_funding_amount);
612
+ const amount = row.is_funding_positive ? Math.abs(amountRaw) : -Math.abs(amountRaw);
613
+ const size = Math.abs(parseNumber(row.size));
614
+ const rate = size > 0 ? amount / size : 0;
615
+ const marketSymbol = this.resolveMarketSymbol(row.market) ?? this.normalizeSymbol(row.market);
616
+ return {
617
+ market: marketSymbol,
618
+ amount: amount.toString(),
619
+ rate: rate.toString(),
620
+ timestamp: parseNumber(row.transaction_unix_ms) || Date.now(),
621
+ };
622
+ });
527
623
  }
528
624
  async getPublicTrades(market, limit = 100) {
529
625
  this.ensureConnected();
@@ -544,34 +640,115 @@ export class DecibelAdapter {
544
640
  });
545
641
  }
546
642
  // --------------------------------------------------------------------------
547
- // Market Maker Protection
548
- // --------------------------------------------------------------------------
549
- async setMMP(_config) {
550
- throw new Error("Decibel does not support Market Maker Protection (MMP)");
551
- }
552
- async getMMP(_market) {
553
- throw new Error("Decibel does not support Market Maker Protection (MMP)");
554
- }
555
- async resetMMP(_market) {
556
- throw new Error("Decibel does not support Market Maker Protection (MMP)");
557
- }
558
- // --------------------------------------------------------------------------
559
643
  // TWAP Orders
560
644
  // --------------------------------------------------------------------------
561
- async placeTWAP(_params) {
562
- throw new Error("Decibel does not support TWAP orders");
645
+ async placeTWAP(params) {
646
+ this.ensureConnected();
647
+ this.ensureOrderManager();
648
+ const meta = this.requireMarket(params.market);
649
+ const sizeUnits = parseNumber(params.size);
650
+ if (!Number.isFinite(sizeUnits) || sizeUnits <= 0) {
651
+ throw new Error("TWAP size must be a positive number");
652
+ }
653
+ if (!Number.isFinite(params.durationMs) || params.durationMs <= 0) {
654
+ throw new Error("TWAP durationMs must be a positive number");
655
+ }
656
+ const requestedIntervalMs = params.intervalMs ?? Math.min(30_000, Math.max(1_000, Math.floor(params.durationMs / 10)));
657
+ const normalizedIntervalMs = Math.min(params.durationMs, Math.max(1_000, requestedIntervalMs));
658
+ const twapFrequencySeconds = Math.max(1, Math.floor(normalizedIntervalMs / 1_000));
659
+ const twapDurationSeconds = Math.max(1, Math.floor(params.durationMs / 1_000));
660
+ const side = params.side === "long" ? "buy" : "sell";
661
+ const clientOrderId = `perps-twap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
662
+ const submission = await this.orderManager.placeTwapOrder({
663
+ market: meta,
664
+ side,
665
+ sizeUnits,
666
+ reduceOnly: false,
667
+ clientOrderId,
668
+ twapFrequencySeconds,
669
+ twapDurationSeconds,
670
+ });
671
+ if (this.accountAddress && this.config?.credentials?.apiBearerToken) {
672
+ const lookupIds = [submission.orderId, clientOrderId].filter((value) => typeof value === "string" && value.length > 0);
673
+ for (let attempt = 0; attempt < 4; attempt++) {
674
+ for (const lookupId of lookupIds) {
675
+ const status = await this.getTWAPStatus(lookupId);
676
+ if (status) {
677
+ return status;
678
+ }
679
+ }
680
+ await sleep(250);
681
+ }
682
+ }
683
+ if (submission.orderId) {
684
+ return {
685
+ id: submission.orderId,
686
+ market: meta.symbol,
687
+ side: params.side,
688
+ totalSize: params.size,
689
+ executedSize: "0",
690
+ remainingSize: params.size,
691
+ status: "active",
692
+ startTime: Date.now(),
693
+ endTime: Date.now() + params.durationMs,
694
+ };
695
+ }
696
+ throw new Error(`Decibel TWAP submitted (${submission.txHash}) but no native order id/status was returned yet`);
563
697
  }
564
- async cancelTWAP(_twapId) {
565
- throw new Error("Decibel does not support TWAP orders");
698
+ async cancelTWAP(twapId) {
699
+ this.ensureConnected();
700
+ this.ensureOrderManager();
701
+ const status = await this.getTWAPStatus(twapId);
702
+ if (!status) {
703
+ return false;
704
+ }
705
+ const orderId = this.normalizeTwapLookupId(status.id);
706
+ if (!orderId) {
707
+ throw new Error(`Unable to resolve a cancellable TWAP order id from '${twapId}'`);
708
+ }
709
+ const market = this.requireMarket(status.market);
710
+ try {
711
+ await this.orderManager.cancelTwapOrder(orderId, market.marketAddr);
712
+ const updated = {
713
+ ...status,
714
+ status: "cancelled",
715
+ };
716
+ this.cacheTwapStatus(updated, [twapId, status.id]);
717
+ return true;
718
+ }
719
+ catch {
720
+ return false;
721
+ }
566
722
  }
567
- async getTWAPStatus(_twapId) {
568
- throw new Error("Decibel does not support TWAP orders");
723
+ async getTWAPStatus(twapId) {
724
+ this.ensureConnected();
725
+ const normalizedLookupId = this.normalizeTwapLookupId(twapId) ?? twapId;
726
+ const cached = this.twapStatusById.get(normalizedLookupId) ?? this.twapStatusById.get(twapId);
727
+ if (cached)
728
+ return cached;
729
+ if (!this.accountAddress || !this.config?.credentials?.apiBearerToken) {
730
+ return null;
731
+ }
732
+ const activeRows = await this.restClient.getActiveTwaps(this.accountAddress, 200);
733
+ const activeStatus = this.findAndCacheTwap(activeRows, normalizedLookupId);
734
+ if (activeStatus) {
735
+ return activeStatus;
736
+ }
737
+ const historyRows = await this.restClient.getTwapHistory(this.accountAddress, 200, 0);
738
+ return this.findAndCacheTwap(historyRows, normalizedLookupId);
569
739
  }
570
740
  // --------------------------------------------------------------------------
571
741
  // Margin Management
572
742
  // --------------------------------------------------------------------------
573
743
  async updateIsolatedMargin(_market, _amount) {
574
- throw new Error("Decibel does not support isolated margin adjustment");
744
+ this.ensureConnected();
745
+ this.ensureOrderManager();
746
+ const meta = this.requireMarket(_market);
747
+ const amount = parseNumber(_amount);
748
+ if (!Number.isFinite(amount) || amount === 0) {
749
+ throw new Error("Decibel isolated margin update requires a non-zero amount");
750
+ }
751
+ await this.orderManager.updateIsolatedPositionMargin(meta.marketAddr, amount);
575
752
  }
576
753
  subscribe(callbacks) {
577
754
  this.globalSubscribers.add(callbacks);
@@ -584,7 +761,7 @@ export class DecibelAdapter {
584
761
  subscribeOrderBook(market, callback) {
585
762
  const meta = this.requireMarket(market);
586
763
  this.trackMarket(meta.marketAddr);
587
- this.wsFeed?.subscribe(`depth:${meta.marketAddr}`);
764
+ this.wsFeed?.subscribe(`depth:${meta.marketAddr}:${this.depthAggregationSize}`);
588
765
  if (!this.orderBookSubscribers.has(meta.symbol)) {
589
766
  this.orderBookSubscribers.set(meta.symbol, new Set());
590
767
  }
@@ -657,21 +834,30 @@ export class DecibelAdapter {
657
834
  handleWsAccountOverview(data) {
658
835
  if (!isRecord(data))
659
836
  return;
837
+ const overviewPayload = isRecord(data.account_overview) ? data.account_overview : data;
660
838
  for (const callbacks of this.globalSubscribers) {
661
839
  if (!callbacks.onBalances)
662
840
  continue;
663
841
  try {
664
- const equity = parseNumber(toNumericInput(data.equity) ?? toNumericInput(data.perp_equity_balance));
665
- const totalCollateral = parseNumber(toNumericInput(data.total_collateral) ?? toNumericInput(data.total_margin));
666
- const unrealizedPnl = parseNumber(toNumericInput(data.unrealized_pnl));
667
- const marginUsed = Math.max(0, totalCollateral - equity);
842
+ const equity = parseNumber(toNumericInput(overviewPayload.equity) ?? toNumericInput(overviewPayload.perp_equity_balance));
843
+ const totalCollateral = parseNumber(toNumericInput(overviewPayload.total_collateral) ?? toNumericInput(overviewPayload.total_margin));
844
+ const unrealizedPnl = parseNumber(toNumericInput(overviewPayload.unrealized_pnl));
845
+ const hasWithdrawable = overviewPayload.usdc_cross_withdrawable_balance !== undefined ||
846
+ overviewPayload.usdc_isolated_withdrawable_balance !== undefined;
847
+ const withdrawable = parseNumber(toNumericInput(overviewPayload.usdc_cross_withdrawable_balance)) +
848
+ parseNumber(toNumericInput(overviewPayload.usdc_isolated_withdrawable_balance));
849
+ const derivedLocked = Math.max(0, totalCollateral - equity);
850
+ const available = hasWithdrawable
851
+ ? Math.max(0, withdrawable)
852
+ : Math.max(0, equity - derivedLocked);
853
+ const locked = Math.max(0, equity - available);
668
854
  callbacks.onBalances([{
669
855
  asset: "USD",
670
856
  total: equity.toString(),
671
- available: Math.max(0, equity - marginUsed).toString(),
672
- locked: marginUsed.toString(),
857
+ available: available.toString(),
858
+ locked: locked.toString(),
673
859
  unrealizedPnl: unrealizedPnl.toString(),
674
- marginUsed: marginUsed.toString(),
860
+ marginUsed: locked.toString(),
675
861
  }]);
676
862
  }
677
863
  catch (err) {
@@ -751,15 +937,65 @@ export class DecibelAdapter {
751
937
  handleWsOrderUpdate(data) {
752
938
  if (!isRecord(data))
753
939
  return;
754
- // order_updates carries a single order status change
755
- const orderData = isRecord(data.order) ? data.order : data;
756
- const order = this.toOrder(orderData);
940
+ // order_updates can arrive as:
941
+ // - { order: { ...orderFields } }
942
+ // - { order: { status, details, order: { ...orderFields } } }
943
+ // - { orders: [{ ...orderFields }] }
944
+ // - { ...orderFields }
945
+ const rows = Array.isArray(data.orders)
946
+ ? data.orders
947
+ : Array.isArray(data.items)
948
+ ? data.items
949
+ : null;
950
+ if (rows) {
951
+ const parsed = [];
952
+ for (const row of rows) {
953
+ if (!isRecord(row))
954
+ continue;
955
+ const order = this.toOrder(row);
956
+ if (order)
957
+ parsed.push(order);
958
+ }
959
+ if (parsed.length > 0) {
960
+ for (const callbacks of this.globalSubscribers) {
961
+ callbacks.onOrders?.(parsed);
962
+ }
963
+ }
964
+ return;
965
+ }
966
+ const outerOrder = isRecord(data.order) ? data.order : null;
967
+ const normalizedOrder = outerOrder && isRecord(outerOrder.order) ? outerOrder.order : outerOrder ?? data;
968
+ const order = this.toOrder(normalizedOrder);
757
969
  if (!order)
758
970
  return;
759
971
  for (const callbacks of this.globalSubscribers) {
760
972
  callbacks.onOrders?.([order]);
761
973
  }
762
974
  }
975
+ handleWsActiveTwaps(data) {
976
+ if (!isRecord(data))
977
+ return;
978
+ const rows = Array.isArray(data.twaps)
979
+ ? data.twaps
980
+ : Array.isArray(data.data)
981
+ ? data.data
982
+ : Array.isArray(data)
983
+ ? data
984
+ : [];
985
+ for (const row of rows) {
986
+ if (!isRecord(row))
987
+ continue;
988
+ const twapRow = row;
989
+ const status = this.toTwapStatus(twapRow);
990
+ const aliases = [
991
+ twapRow.order_id,
992
+ twapRow.client_order_id,
993
+ `order:${twapRow.order_id}`,
994
+ `client:${twapRow.client_order_id}`,
995
+ ].filter((value) => typeof value === "string" && value.length > 0);
996
+ this.cacheTwapStatus(status, aliases);
997
+ }
998
+ }
763
999
  handleWsNotification(data) {
764
1000
  if (!isRecord(data))
765
1001
  return;
@@ -890,7 +1126,7 @@ export class DecibelAdapter {
890
1126
  unrealizedPnl: parseNumber(row.unrealized_pnl ?? row.unrealized_funding).toString(),
891
1127
  realizedPnl: "0",
892
1128
  leverage: parseNumber(row.leverage ?? row.user_leverage) || 1,
893
- marginType: "cross",
1129
+ marginType: row.is_isolated ? "isolated" : "cross",
894
1130
  margin: "0",
895
1131
  timestamp: Date.now(),
896
1132
  };
@@ -943,6 +1179,83 @@ export class DecibelAdapter {
943
1179
  ...(orderId ? { orderId } : {}),
944
1180
  };
945
1181
  }
1182
+ toTwapStatus(row) {
1183
+ const totalSize = Math.max(0, parseNumber(row.orig_size));
1184
+ const remainingSize = Math.max(0, parseNumber(row.remaining_size));
1185
+ const executedSize = Math.max(0, totalSize - remainingSize);
1186
+ const startTime = parseNumber(row.start_unix_ms) || parseNumber(row.transaction_unix_ms) || Date.now();
1187
+ const durationMs = Math.max(0, parseNumber(row.duration_s) * 1_000);
1188
+ const endTime = durationMs > 0 ? startTime + durationMs : startTime;
1189
+ const market = this.resolveMarketSymbol(row.market) ?? this.normalizeSymbol(row.market);
1190
+ return {
1191
+ id: row.order_id || row.client_order_id || `twap-${startTime}`,
1192
+ market,
1193
+ side: row.is_buy ? "long" : "short",
1194
+ totalSize: totalSize.toString(),
1195
+ executedSize: executedSize.toString(),
1196
+ remainingSize: remainingSize.toString(),
1197
+ status: this.mapTwapState(row.status),
1198
+ startTime,
1199
+ endTime,
1200
+ };
1201
+ }
1202
+ mapTwapState(status) {
1203
+ const normalized = status.toLowerCase();
1204
+ if (normalized.includes("cancel"))
1205
+ return "cancelled";
1206
+ if (normalized.includes("finish") || normalized.includes("complete"))
1207
+ return "completed";
1208
+ return "active";
1209
+ }
1210
+ cacheTwapStatus(status, aliases = []) {
1211
+ const keys = new Set([status.id, ...aliases]);
1212
+ for (const key of keys) {
1213
+ if (!key)
1214
+ continue;
1215
+ this.twapStatusById.set(key, status);
1216
+ const normalized = this.normalizeTwapLookupId(key);
1217
+ if (normalized) {
1218
+ this.twapStatusById.set(normalized, status);
1219
+ }
1220
+ }
1221
+ }
1222
+ findAndCacheTwap(rows, lookupId) {
1223
+ for (const row of rows) {
1224
+ const status = this.toTwapStatus(row);
1225
+ const aliases = [row.order_id, row.client_order_id, `order:${row.order_id}`, `client:${row.client_order_id}`]
1226
+ .filter((value) => typeof value === "string" && value.length > 0);
1227
+ this.cacheTwapStatus(status, aliases);
1228
+ for (const alias of aliases) {
1229
+ if (this.isMatchingTwapLookupId(alias, lookupId)) {
1230
+ return status;
1231
+ }
1232
+ }
1233
+ if (this.isMatchingTwapLookupId(status.id, lookupId)) {
1234
+ return status;
1235
+ }
1236
+ }
1237
+ return null;
1238
+ }
1239
+ normalizeTwapLookupId(twapId) {
1240
+ const value = twapId.trim();
1241
+ if (!value)
1242
+ return null;
1243
+ for (const prefix of ["client:", "order:", "twap:"]) {
1244
+ if (value.startsWith(prefix) && value.length > prefix.length) {
1245
+ return value.slice(prefix.length);
1246
+ }
1247
+ }
1248
+ return value;
1249
+ }
1250
+ isMatchingTwapLookupId(candidate, lookupId) {
1251
+ const normalizedCandidate = this.normalizeTwapLookupId(candidate);
1252
+ const normalizedLookup = this.normalizeTwapLookupId(lookupId);
1253
+ if (candidate === lookupId)
1254
+ return true;
1255
+ if (!normalizedCandidate || !normalizedLookup)
1256
+ return false;
1257
+ return normalizedCandidate === normalizedLookup;
1258
+ }
946
1259
  resolveMarketSymbol(input) {
947
1260
  const raw = input.trim();
948
1261
  if (!raw)
@@ -1027,7 +1340,7 @@ export class DecibelAdapter {
1027
1340
  return;
1028
1341
  this.trackedMarketAddrs.add(marketAddr);
1029
1342
  this.wsFeed?.subscribe(`market_price:${marketAddr}`);
1030
- this.wsFeed?.subscribe(`depth:${marketAddr}`);
1343
+ this.wsFeed?.subscribe(`depth:${marketAddr}:${this.depthAggregationSize}`);
1031
1344
  this.wsFeed?.subscribe(`trades:${marketAddr}`);
1032
1345
  }
1033
1346
  handleWsPrice(marketAddr, data) {
@@ -1226,7 +1539,7 @@ export class DecibelAdapter {
1226
1539
  }
1227
1540
  }
1228
1541
  function parseNumber(value) {
1229
- if (value === undefined)
1542
+ if (value === undefined || value === null)
1230
1543
  return 0;
1231
1544
  const num = Number(value);
1232
1545
  return Number.isFinite(num) ? num : 0;
@@ -1249,6 +1562,11 @@ function toNumericInput(value) {
1249
1562
  }
1250
1563
  return undefined;
1251
1564
  }
1565
+ function sleep(ms) {
1566
+ return new Promise((resolve) => {
1567
+ setTimeout(resolve, ms);
1568
+ });
1569
+ }
1252
1570
  function coerceWsPricePayload(marketAddr, data) {
1253
1571
  const payload = isRecord(data) ? data : {};
1254
1572
  const market = typeof payload.market === "string" ? payload.market : marketAddr;
@@ -1258,6 +1576,9 @@ function coerceWsPricePayload(marketAddr, data) {
1258
1576
  mark_px: toNumericInput(payload.mark_px) ?? 0,
1259
1577
  mid_px: toNumericInput(payload.mid_px) ?? 0,
1260
1578
  funding_rate_bps: toNumericInput(payload.funding_rate_bps) ?? 0,
1579
+ is_funding_positive: typeof payload.is_funding_positive === "boolean"
1580
+ ? payload.is_funding_positive
1581
+ : undefined,
1261
1582
  open_interest: toNumericInput(payload.open_interest) ?? 0,
1262
1583
  transaction_unix_ms: toNumericInput(payload.transaction_unix_ms),
1263
1584
  };
@@ -2,7 +2,7 @@
2
2
  * Hyperliquid Adapter
3
3
  * Implements PerpDEXAdapter interface for Hyperliquid DEX
4
4
  */
5
- import { type PerpDEXAdapter, type ExchangeInfo, type ExchangeConfig, type Market, type Ticker, type OrderBook, type FundingRate, type Position, type Order, type Balance, type Trade, type OrderParams, type CancelOrderParams, type ModifyOrderParams, type FundingPayment, type PublicTrade, type SubscriptionCallbacks, type Unsubscribe, type MarginType, type MMPConfig, type MMPStatus, type TWAPParams, type TWAPStatus } from "./interface.js";
5
+ import { type PerpDEXAdapter, type ExchangeInfo, type ExchangeConfig, type Market, type Ticker, type OrderBook, type FundingRate, type Position, type Order, type Balance, type Trade, type OrderParams, type CancelOrderParams, type ModifyOrderParams, type PublicTrade, type SubscriptionCallbacks, type Unsubscribe, type MarginType, type TWAPParams, type TWAPStatus } from "./interface.js";
6
6
  export declare class HyperliquidAdapter implements PerpDEXAdapter {
7
7
  readonly info: ExchangeInfo;
8
8
  private httpTransport;
@@ -40,14 +40,9 @@ export declare class HyperliquidAdapter implements PerpDEXAdapter {
40
40
  batchCancelOrders(paramsList: CancelOrderParams[]): Promise<boolean[]>;
41
41
  cancelAllAfter(timeoutMs: number): Promise<void>;
42
42
  getOrderHistory(_market?: string, limit?: number): Promise<Order[]>;
43
- getFundingHistory(_market?: string, _limit?: number): Promise<FundingPayment[]>;
44
43
  getPublicTrades(market: string, limit?: number): Promise<PublicTrade[]>;
45
- setMMP(_config: MMPConfig): Promise<void>;
46
- getMMP(_market: string): Promise<MMPStatus>;
47
- resetMMP(_market: string): Promise<void>;
48
44
  placeTWAP(params: TWAPParams): Promise<TWAPStatus>;
49
45
  cancelTWAP(twapId: string): Promise<boolean>;
50
- getTWAPStatus(_twapId: string): Promise<TWAPStatus | null>;
51
46
  updateIsolatedMargin(market: string, amount: string): Promise<void>;
52
47
  subscribe(callbacks: SubscriptionCallbacks): Unsubscribe;
53
48
  subscribeOrderBook(market: string, callback: (book: OrderBook) => void): Unsubscribe;
@@ -555,9 +555,6 @@ export class HyperliquidAdapter {
555
555
  }
556
556
  return orders;
557
557
  }
558
- async getFundingHistory(_market, _limit) {
559
- throw new Error("Hyperliquid does not expose a per-user funding history endpoint");
560
- }
561
558
  async getPublicTrades(market, limit = 100) {
562
559
  this.ensureConnected();
563
560
  const coin = market.toUpperCase().replace("-PERP", "");
@@ -572,18 +569,6 @@ export class HyperliquidAdapter {
572
569
  }));
573
570
  }
574
571
  // --------------------------------------------------------------------------
575
- // Market Maker Protection
576
- // --------------------------------------------------------------------------
577
- async setMMP(_config) {
578
- throw new Error("Hyperliquid does not support Market Maker Protection (MMP)");
579
- }
580
- async getMMP(_market) {
581
- throw new Error("Hyperliquid does not support Market Maker Protection (MMP)");
582
- }
583
- async resetMMP(_market) {
584
- throw new Error("Hyperliquid does not support Market Maker Protection (MMP)");
585
- }
586
- // --------------------------------------------------------------------------
587
572
  // TWAP Orders
588
573
  // --------------------------------------------------------------------------
589
574
  async placeTWAP(params) {
@@ -622,9 +607,6 @@ export class HyperliquidAdapter {
622
607
  return false;
623
608
  }
624
609
  }
625
- async getTWAPStatus(_twapId) {
626
- throw new Error("Hyperliquid does not expose a TWAP status query endpoint");
627
- }
628
610
  // --------------------------------------------------------------------------
629
611
  // Margin Management
630
612
  // --------------------------------------------------------------------------