@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.
- package/CHANGELOG.md +19 -1
- package/README.md +12 -2
- package/dist/adapters/aevo.d.ts +1 -6
- package/dist/adapters/aevo.js +0 -21
- package/dist/adapters/certification.js +55 -9
- package/dist/adapters/decibel/order-manager.d.ts +41 -0
- package/dist/adapters/decibel/order-manager.js +216 -0
- package/dist/adapters/decibel/rest-client.d.ts +21 -0
- package/dist/adapters/decibel/rest-client.js +15 -8
- package/dist/adapters/decibel/ws-feed.js +19 -4
- package/dist/adapters/decibel.d.ts +15 -10
- package/dist/adapters/decibel.js +371 -50
- package/dist/adapters/hyperliquid.d.ts +1 -6
- package/dist/adapters/hyperliquid.js +0 -18
- package/dist/adapters/interface.d.ts +15 -14
- package/dist/adapters/orderly.d.ts +1 -9
- package/dist/adapters/orderly.js +0 -33
- package/dist/adapters/paradex.d.ts +1 -9
- package/dist/adapters/paradex.js +1 -34
- package/dist/commands/arb/alert.d.ts +0 -4
- package/dist/commands/arb/alert.js +4 -14
- package/dist/commands/markets/index.js +2 -0
- package/dist/commands/markets/read-simple.d.ts +2 -0
- package/dist/commands/markets/read-simple.js +130 -0
- package/dist/commands/order/advanced-simple.d.ts +2 -0
- package/dist/commands/order/advanced-simple.js +820 -0
- package/dist/commands/order/index.js +2 -0
- package/dist/lib/prompts.d.ts +0 -18
- package/dist/lib/prompts.js +1 -22
- package/dist/lib/schema.d.ts +8 -8
- package/dist/lib/schema.js +47 -8
- package/package.json +1 -1
package/dist/adapters/decibel.js
CHANGED
|
@@ -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: "
|
|
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:
|
|
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:
|
|
45
|
+
modifyOrders: true,
|
|
44
46
|
batchOrders: false,
|
|
45
47
|
cancelAllAfter: false,
|
|
46
48
|
publicTrades: true,
|
|
47
|
-
fundingHistory:
|
|
49
|
+
fundingHistory: true,
|
|
48
50
|
orderHistory: true,
|
|
49
51
|
mmp: false,
|
|
50
|
-
twapOrders:
|
|
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
|
|
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:
|
|
345
|
-
locked:
|
|
358
|
+
available: available.toString(),
|
|
359
|
+
locked: locked.toString(),
|
|
346
360
|
unrealizedPnl: unrealizedPnl.toString(),
|
|
347
|
-
marginUsed:
|
|
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(
|
|
487
|
-
|
|
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(
|
|
525
|
-
|
|
526
|
-
|
|
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(
|
|
562
|
-
|
|
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(
|
|
565
|
-
|
|
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(
|
|
568
|
-
|
|
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
|
-
|
|
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(
|
|
665
|
-
const totalCollateral = parseNumber(toNumericInput(
|
|
666
|
-
const unrealizedPnl = parseNumber(toNumericInput(
|
|
667
|
-
const
|
|
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:
|
|
672
|
-
locked:
|
|
857
|
+
available: available.toString(),
|
|
858
|
+
locked: locked.toString(),
|
|
673
859
|
unrealizedPnl: unrealizedPnl.toString(),
|
|
674
|
-
marginUsed:
|
|
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
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
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
|
// --------------------------------------------------------------------------
|