perp-cli 0.4.2 → 0.4.9
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/README.md +46 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +300 -63
- package/dist/commands/agent.js +16 -9
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +696 -9
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +6 -4
- package/dist/commands/market.js +35 -2
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +270 -267
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +11 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +21 -1
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +69 -0
- package/dist/exchanges/hyperliquid-spot.js +309 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +171 -73
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +415 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +210 -56
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +64 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +126 -161
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/mcp-server.js +43 -96
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.js +14 -3
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +80 -4
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +54 -32
- package/skills/perp-cli/references/strategies.md +27 -17
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import nacl from "tweetnacl";
|
|
2
1
|
import { PacificaClient } from "../pacifica/index.js";
|
|
3
|
-
|
|
4
|
-
return async (message) => {
|
|
5
|
-
return nacl.sign.detached(message, keypair.secretKey);
|
|
6
|
-
};
|
|
7
|
-
}
|
|
2
|
+
import { LocalSolanaSigner } from "../signer/solana-local.js";
|
|
8
3
|
export class PacificaAdapter {
|
|
9
4
|
name = "pacifica";
|
|
10
5
|
client;
|
|
11
|
-
|
|
6
|
+
_solanaSigner;
|
|
7
|
+
_hasRealKey;
|
|
12
8
|
account;
|
|
13
9
|
signMessage;
|
|
14
10
|
// In-memory caches removed — using file-based cache (src/cache.ts) for cross-process dedup
|
|
15
|
-
constructor(keypair, network = "mainnet", builderCode) {
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
11
|
+
constructor(keypair, network = "mainnet", builderCode, hasRealKey = true) {
|
|
12
|
+
this._solanaSigner = new LocalSolanaSigner(keypair);
|
|
13
|
+
this._hasRealKey = hasRealKey;
|
|
14
|
+
this.account = this._solanaSigner.getPublicKeyBase58();
|
|
18
15
|
this.client = new PacificaClient({ network, builderCode });
|
|
19
|
-
this.signMessage =
|
|
16
|
+
this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
|
|
17
|
+
}
|
|
18
|
+
ensureSigner() {
|
|
19
|
+
if (!this._hasRealKey) {
|
|
20
|
+
throw new Error("No private key configured. Run: perp init");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Inject an external Solana signer. */
|
|
24
|
+
setSigner(signer) {
|
|
25
|
+
this._solanaSigner = signer;
|
|
26
|
+
this.account = signer.getPublicKeyBase58();
|
|
27
|
+
this.signMessage = (msg) => this._solanaSigner.signMessage(msg);
|
|
28
|
+
}
|
|
29
|
+
/** Access the underlying Keypair (only available with LocalSolanaSigner). */
|
|
30
|
+
get keypair() {
|
|
31
|
+
if (this._solanaSigner instanceof LocalSolanaSigner) {
|
|
32
|
+
return this._solanaSigner._keypair;
|
|
33
|
+
}
|
|
34
|
+
throw new Error("keypair not available with external signer");
|
|
20
35
|
}
|
|
21
36
|
async _getPrices() {
|
|
22
37
|
const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
@@ -62,24 +77,15 @@ export class PacificaAdapter {
|
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
async getBalance() {
|
|
65
|
-
|
|
80
|
+
if (!this._hasRealKey)
|
|
81
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
82
|
+
const [info, positions] = await Promise.all([
|
|
66
83
|
this.client.getAccount(this.account),
|
|
67
84
|
this._getPositions(),
|
|
68
|
-
this._getPrices(),
|
|
69
85
|
]);
|
|
70
86
|
const raw = info;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let totalPnl = 0;
|
|
74
|
-
for (const p of positions) {
|
|
75
|
-
const mark = priceMap.get(p.symbol) ?? 0;
|
|
76
|
-
const entry = Number(p.entry_price);
|
|
77
|
-
const amount = Number(p.amount);
|
|
78
|
-
const side = p.side === "bid" ? "long" : "short";
|
|
79
|
-
totalPnl += side === "long"
|
|
80
|
-
? (mark - entry) * amount
|
|
81
|
-
: (entry - mark) * amount;
|
|
82
|
-
}
|
|
87
|
+
// Use API-provided unrealized PnL from positions
|
|
88
|
+
const totalPnl = positions.reduce((sum, p) => sum + Number(p.unrealized_pnl ?? 0), 0);
|
|
83
89
|
return {
|
|
84
90
|
equity: info.account_equity,
|
|
85
91
|
available: info.available_to_spend,
|
|
@@ -88,6 +94,8 @@ export class PacificaAdapter {
|
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
async getPositions() {
|
|
97
|
+
if (!this._hasRealKey)
|
|
98
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
91
99
|
const [positions, prices] = await Promise.all([
|
|
92
100
|
this._getPositions(),
|
|
93
101
|
this._getPrices(),
|
|
@@ -104,14 +112,8 @@ export class PacificaAdapter {
|
|
|
104
112
|
// Settings API may not be available
|
|
105
113
|
}
|
|
106
114
|
return positions.map((p) => {
|
|
107
|
-
const mark = priceMap.get(p.symbol)?.mark ?? "0";
|
|
108
|
-
const entryPrice = Number(p.entry_price);
|
|
109
|
-
const amount = Number(p.amount);
|
|
110
|
-
const markNum = Number(mark);
|
|
115
|
+
const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
|
|
111
116
|
const side = p.side === "bid" ? "long" : "short";
|
|
112
|
-
const pnl = side === "long"
|
|
113
|
-
? (markNum - entryPrice) * amount
|
|
114
|
-
: (entryPrice - markNum) * amount;
|
|
115
117
|
return {
|
|
116
118
|
symbol: p.symbol,
|
|
117
119
|
side: side,
|
|
@@ -119,12 +121,14 @@ export class PacificaAdapter {
|
|
|
119
121
|
entryPrice: String(p.entry_price),
|
|
120
122
|
markPrice: mark,
|
|
121
123
|
liquidationPrice: String(p.liquidation_price ?? "N/A"),
|
|
122
|
-
unrealizedPnl:
|
|
123
|
-
leverage: levMap.get(String(p.symbol)) ?? 1,
|
|
124
|
+
unrealizedPnl: String(p.unrealized_pnl ?? "0"),
|
|
125
|
+
leverage: p.leverage ?? levMap.get(String(p.symbol)) ?? 1,
|
|
124
126
|
};
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
129
|
async getOpenOrders() {
|
|
130
|
+
if (!this._hasRealKey)
|
|
131
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
128
132
|
const orders = await this.client.getOrders(this.account);
|
|
129
133
|
return orders.map((o) => {
|
|
130
134
|
const raw = o;
|
|
@@ -141,21 +145,37 @@ export class PacificaAdapter {
|
|
|
141
145
|
});
|
|
142
146
|
}
|
|
143
147
|
async marketOrder(symbol, side, size) {
|
|
148
|
+
this.ensureSigner();
|
|
144
149
|
return this.client.createMarketOrder({ symbol, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: false, slippage_percent: "1" }, this.account, this.signMessage);
|
|
145
150
|
}
|
|
146
151
|
async limitOrder(symbol, side, price, size, opts) {
|
|
152
|
+
this.ensureSigner();
|
|
147
153
|
return this.client.createLimitOrder({ symbol, price, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, tif: (opts?.tif ?? "GTC") }, this.account, this.signMessage);
|
|
148
154
|
}
|
|
149
155
|
async cancelOrder(symbol, orderId) {
|
|
156
|
+
this.ensureSigner();
|
|
150
157
|
return this.client.cancelOrder({ symbol, order_id: Number(orderId) }, this.account, this.signMessage);
|
|
151
158
|
}
|
|
152
|
-
async cancelAllOrders(
|
|
159
|
+
async cancelAllOrders(symbol) {
|
|
160
|
+
this.ensureSigner();
|
|
161
|
+
if (symbol) {
|
|
162
|
+
// Cancel only orders for this symbol: fetch open orders, filter, cancel individually
|
|
163
|
+
const orders = await this.getOpenOrders();
|
|
164
|
+
const filtered = orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase());
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const o of filtered) {
|
|
167
|
+
results.push(await this.cancelOrder(o.symbol, o.orderId));
|
|
168
|
+
}
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
153
171
|
return this.client.cancelAllOrders({ all_symbols: true, exclude_reduce_only: false }, this.account, this.signMessage);
|
|
154
172
|
}
|
|
155
173
|
async editOrder(symbol, orderId, price, size) {
|
|
174
|
+
this.ensureSigner();
|
|
156
175
|
return this.client.editOrder({ symbol, order_id: Number(orderId), price, amount: size }, this.account, this.signMessage);
|
|
157
176
|
}
|
|
158
177
|
async setLeverage(symbol, leverage, marginMode = "cross") {
|
|
178
|
+
this.ensureSigner();
|
|
159
179
|
await this.client.updateLeverage({ symbol, leverage }, this.account, this.signMessage);
|
|
160
180
|
if (marginMode === "isolated") {
|
|
161
181
|
await this.client.updateMarginMode({ symbol, is_isolated: true }, this.account, this.signMessage);
|
|
@@ -163,6 +183,7 @@ export class PacificaAdapter {
|
|
|
163
183
|
return { symbol, leverage, marginMode };
|
|
164
184
|
}
|
|
165
185
|
async stopOrder(symbol, side, size, triggerPrice, opts) {
|
|
186
|
+
this.ensureSigner();
|
|
166
187
|
return this.client.createStopOrder({
|
|
167
188
|
symbol,
|
|
168
189
|
side: side === "buy" ? "bid" : "ask",
|
|
@@ -173,12 +194,12 @@ export class PacificaAdapter {
|
|
|
173
194
|
async getRecentTrades(symbol, _limit = 20) {
|
|
174
195
|
const trades = await this.client.getTrades(symbol);
|
|
175
196
|
return trades.slice(0, _limit).map((t) => ({
|
|
176
|
-
time:
|
|
197
|
+
time: Number(t.created_at ?? 0),
|
|
177
198
|
symbol,
|
|
178
199
|
side: t.side === "bid" ? "buy" : "sell",
|
|
179
200
|
price: t.price,
|
|
180
201
|
size: t.amount,
|
|
181
|
-
fee: "0",
|
|
202
|
+
fee: String(t.fee ?? "0"),
|
|
182
203
|
}));
|
|
183
204
|
}
|
|
184
205
|
async getFundingHistory(symbol, limit = 10) {
|
|
@@ -205,6 +226,8 @@ export class PacificaAdapter {
|
|
|
205
226
|
}));
|
|
206
227
|
}
|
|
207
228
|
async getOrderHistory(limit = 30) {
|
|
229
|
+
if (!this._hasRealKey)
|
|
230
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
208
231
|
const result = await this.client.getOrderHistory(this.account);
|
|
209
232
|
const data = (result.data ?? result.orders ?? []);
|
|
210
233
|
if (!Array.isArray(data))
|
|
@@ -221,6 +244,8 @@ export class PacificaAdapter {
|
|
|
221
244
|
}));
|
|
222
245
|
}
|
|
223
246
|
async getTradeHistory(limit = 30) {
|
|
247
|
+
if (!this._hasRealKey)
|
|
248
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
224
249
|
const raw = await this.client.getTradeHistory(this.account);
|
|
225
250
|
const trades = (raw.data ?? raw);
|
|
226
251
|
if (!Array.isArray(trades))
|
|
@@ -235,6 +260,8 @@ export class PacificaAdapter {
|
|
|
235
260
|
}));
|
|
236
261
|
}
|
|
237
262
|
async getFundingPayments(limit = 30) {
|
|
263
|
+
if (!this._hasRealKey)
|
|
264
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
238
265
|
const raw = await this.client.getFundingAccountHistory(this.account);
|
|
239
266
|
const history = (raw.data ?? raw);
|
|
240
267
|
if (!Array.isArray(history))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spot trading interface for exchanges that support both spot and perp.
|
|
3
|
+
* Separate from ExchangeAdapter because spot uses balance-based tracking
|
|
4
|
+
* (not position-based) and has no leverage, liquidation, or funding concepts.
|
|
5
|
+
*
|
|
6
|
+
* Supported: Hyperliquid, Lighter (Pacifica is perp-only)
|
|
7
|
+
*/
|
|
8
|
+
export interface SpotMarketInfo {
|
|
9
|
+
symbol: string;
|
|
10
|
+
baseToken: string;
|
|
11
|
+
quoteToken: string;
|
|
12
|
+
markPrice: string;
|
|
13
|
+
volume24h: string;
|
|
14
|
+
sizeDecimals: number;
|
|
15
|
+
priceDecimals: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SpotBalance {
|
|
18
|
+
token: string;
|
|
19
|
+
total: string;
|
|
20
|
+
available: string;
|
|
21
|
+
held: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SpotAdapter {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
getSpotMarkets(): Promise<SpotMarketInfo[]>;
|
|
26
|
+
getSpotOrderbook(symbol: string): Promise<{
|
|
27
|
+
bids: [string, string][];
|
|
28
|
+
asks: [string, string][];
|
|
29
|
+
}>;
|
|
30
|
+
getSpotBalances(): Promise<SpotBalance[]>;
|
|
31
|
+
spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
|
|
32
|
+
spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
|
|
33
|
+
tif?: string;
|
|
34
|
+
}): Promise<unknown>;
|
|
35
|
+
spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* U-token (Unit protocol) → underlying perp symbol mapping.
|
|
39
|
+
* These are bridged tokens on HyperEVM with the same price as the underlying (~0.1% deviation).
|
|
40
|
+
*/
|
|
41
|
+
export declare const SPOT_PERP_TOKEN_MAP: Record<string, string>;
|
|
42
|
+
/** Reverse: perp symbol → U-token spot name on HL */
|
|
43
|
+
export declare const PERP_TO_SPOT_MAP: Record<string, string>;
|
|
44
|
+
/** Maximum price deviation (%) between spot and perp to be considered same underlying */
|
|
45
|
+
export declare const MAX_PRICE_DEVIATION_PCT = 5;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spot trading interface for exchanges that support both spot and perp.
|
|
3
|
+
* Separate from ExchangeAdapter because spot uses balance-based tracking
|
|
4
|
+
* (not position-based) and has no leverage, liquidation, or funding concepts.
|
|
5
|
+
*
|
|
6
|
+
* Supported: Hyperliquid, Lighter (Pacifica is perp-only)
|
|
7
|
+
*/
|
|
8
|
+
// ── Token mapping & price verification ──
|
|
9
|
+
/**
|
|
10
|
+
* U-token (Unit protocol) → underlying perp symbol mapping.
|
|
11
|
+
* These are bridged tokens on HyperEVM with the same price as the underlying (~0.1% deviation).
|
|
12
|
+
*/
|
|
13
|
+
export const SPOT_PERP_TOKEN_MAP = {
|
|
14
|
+
UBTC: "BTC",
|
|
15
|
+
UETH: "ETH",
|
|
16
|
+
USOL: "SOL",
|
|
17
|
+
UFART: "FARTCOIN",
|
|
18
|
+
};
|
|
19
|
+
/** Reverse: perp symbol → U-token spot name on HL */
|
|
20
|
+
export const PERP_TO_SPOT_MAP = Object.fromEntries(Object.entries(SPOT_PERP_TOKEN_MAP).map(([k, v]) => [v, k]));
|
|
21
|
+
/** Maximum price deviation (%) between spot and perp to be considered same underlying */
|
|
22
|
+
export const MAX_PRICE_DEVIATION_PCT = 5;
|
package/dist/execution-log.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export interface ExecutionRecord {
|
|
2
2
|
id: string;
|
|
3
3
|
timestamp: string;
|
|
4
|
-
type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge";
|
|
4
|
+
type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback" | "split_order";
|
|
5
5
|
exchange: string;
|
|
6
6
|
symbol: string;
|
|
7
7
|
side: string;
|
|
8
8
|
size: string;
|
|
9
9
|
price?: string;
|
|
10
10
|
notional?: number;
|
|
11
|
-
status: "success" | "failed" | "simulated";
|
|
11
|
+
status: "success" | "failed" | "simulated" | "unverified";
|
|
12
12
|
error?: string;
|
|
13
13
|
meta?: Record<string, unknown>;
|
|
14
14
|
dryRun: boolean;
|
package/dist/funding/rates.d.ts
CHANGED
|
@@ -43,3 +43,39 @@ export declare function fetchAllFundingRates(opts?: {
|
|
|
43
43
|
* Fetch rates for a single symbol across all exchanges.
|
|
44
44
|
*/
|
|
45
45
|
export declare function fetchSymbolFundingRates(symbol: string): Promise<SymbolFundingComparison | null>;
|
|
46
|
+
export interface SpotPerpSpread {
|
|
47
|
+
symbol: string;
|
|
48
|
+
/** Perp exchange with the highest absolute funding rate */
|
|
49
|
+
perpExchange: string;
|
|
50
|
+
/** Spot exchange(s) available for the hedge leg */
|
|
51
|
+
spotExchanges: string[];
|
|
52
|
+
/** Perp funding rate (raw) */
|
|
53
|
+
perpFundingRate: number;
|
|
54
|
+
/** Perp funding hourly rate */
|
|
55
|
+
perpHourlyRate: number;
|
|
56
|
+
/** Annualized spread % (spot funding = 0, so spread = |perp annual rate|) */
|
|
57
|
+
annualSpreadPct: number;
|
|
58
|
+
/** Best mark price */
|
|
59
|
+
bestMarkPrice: number;
|
|
60
|
+
/** Direction: "long-spot-short-perp" when perp funding > 0, else "sell-spot-long-perp" */
|
|
61
|
+
direction: "long-spot-short-perp" | "sell-spot-long-perp";
|
|
62
|
+
/** Estimated hourly income for $1000 notional */
|
|
63
|
+
estHourlyIncomeUsd: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fetch spot+perp funding rate spreads.
|
|
67
|
+
* Since spot has 0 funding cost, the spread is simply |perp funding rate|.
|
|
68
|
+
* Only includes symbols available on at least one spot exchange (HL or LT).
|
|
69
|
+
*
|
|
70
|
+
* Key safety checks:
|
|
71
|
+
* - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
|
|
72
|
+
* - Price validation: spot mid price must be within 5% of perp mark price
|
|
73
|
+
* (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
|
|
74
|
+
*/
|
|
75
|
+
export declare function fetchSpotPerpSpreads(opts?: {
|
|
76
|
+
symbols?: string[];
|
|
77
|
+
minSpread?: number;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
timestamp: string;
|
|
80
|
+
spreads: SpotPerpSpread[];
|
|
81
|
+
}>;
|
package/dist/funding/rates.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { toHourlyRate, annualizeRate, computeAnnualSpread, estimateHourlyFunding } from "./normalize.js";
|
|
9
9
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates as fetchLtFundingRates, } from "../shared-api.js";
|
|
10
10
|
import { getHistoricalAverages } from "./history.js";
|
|
11
|
+
import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/spot-interface.js";
|
|
11
12
|
// ── Default top symbols to track ──
|
|
12
13
|
export const TOP_SYMBOLS = [
|
|
13
14
|
"BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
|
|
@@ -170,3 +171,148 @@ export async function fetchSymbolFundingRates(symbol) {
|
|
|
170
171
|
const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()] });
|
|
171
172
|
return snapshot.symbols[0] ?? null;
|
|
172
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Fetch spot+perp funding rate spreads.
|
|
176
|
+
* Since spot has 0 funding cost, the spread is simply |perp funding rate|.
|
|
177
|
+
* Only includes symbols available on at least one spot exchange (HL or LT).
|
|
178
|
+
*
|
|
179
|
+
* Key safety checks:
|
|
180
|
+
* - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
|
|
181
|
+
* - Price validation: spot mid price must be within 5% of perp mark price
|
|
182
|
+
* (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
|
|
183
|
+
*/
|
|
184
|
+
export async function fetchSpotPerpSpreads(opts) {
|
|
185
|
+
// Fetch all perp funding rates
|
|
186
|
+
const [hlRates, ltRates, pacRates] = await Promise.all([
|
|
187
|
+
fetchHyperliquidRates(),
|
|
188
|
+
fetchLighterRates(),
|
|
189
|
+
fetchPacificaRates(),
|
|
190
|
+
]);
|
|
191
|
+
// Build actual spot availability maps + spot prices for cross-validation
|
|
192
|
+
// hlSpotByPerp: perpSymbol → { spotToken, exchange }
|
|
193
|
+
const hlSpotByPerp = new Map(); // perpSymbol → spotTokenName
|
|
194
|
+
const hlSpotPrices = new Map(); // perpSymbol → spot mid price
|
|
195
|
+
const ltSpotSymbols = new Set();
|
|
196
|
+
const ltSpotPrices = new Map(); // symbol → spot mid price
|
|
197
|
+
try {
|
|
198
|
+
// HL: spotMetaAndAssetCtxs → universe + prices in one call
|
|
199
|
+
const hlMetaCtx = await fetch("https://api.hyperliquid.xyz/info", {
|
|
200
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
201
|
+
body: JSON.stringify({ type: "spotMetaAndAssetCtxs" }),
|
|
202
|
+
}).then(r => r.json());
|
|
203
|
+
const meta = hlMetaCtx[0];
|
|
204
|
+
const ctxs = hlMetaCtx[1] ?? [];
|
|
205
|
+
const tokenNames = new Map();
|
|
206
|
+
for (const t of meta.tokens ?? [])
|
|
207
|
+
tokenNames.set(t.index, t.name);
|
|
208
|
+
const usdcIdx = meta.tokens?.find(t => t.name === "USDC")?.index ?? 0;
|
|
209
|
+
// We need to match universe entries to ctxs by index.
|
|
210
|
+
// API returns ctxs in same order as universe entries.
|
|
211
|
+
for (let i = 0; i < (meta.universe ?? []).length; i++) {
|
|
212
|
+
const u = meta.universe[i];
|
|
213
|
+
if (u.tokens[1] !== usdcIdx)
|
|
214
|
+
continue; // only /USDC pairs
|
|
215
|
+
const base = tokenNames.get(u.tokens[0])?.toUpperCase();
|
|
216
|
+
if (!base)
|
|
217
|
+
continue;
|
|
218
|
+
const ctx = ctxs[i];
|
|
219
|
+
const spotPrice = Number(ctx?.midPx ?? ctx?.markPx ?? 0);
|
|
220
|
+
// Map to perp symbol: UBTC→BTC, or direct if no mapping
|
|
221
|
+
const perpSymbol = SPOT_PERP_TOKEN_MAP[base] ?? base;
|
|
222
|
+
// Only keep the best-priced entry per perp symbol (prefer higher-volume)
|
|
223
|
+
if (!hlSpotByPerp.has(perpSymbol) || spotPrice > 0) {
|
|
224
|
+
hlSpotByPerp.set(perpSymbol, base);
|
|
225
|
+
if (spotPrice > 0)
|
|
226
|
+
hlSpotPrices.set(perpSymbol, spotPrice);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { /* non-critical */ }
|
|
231
|
+
try {
|
|
232
|
+
// LT: explorer API → symbols with "/"
|
|
233
|
+
const ltMarkets = await fetch("https://explorer.elliot.ai/api/markets")
|
|
234
|
+
.then(r => r.json());
|
|
235
|
+
for (const m of ltMarkets) {
|
|
236
|
+
if (m.symbol.includes("/")) {
|
|
237
|
+
ltSpotSymbols.add(m.symbol.split("/")[0].toUpperCase());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Fetch LT spot prices from orderbook for price validation
|
|
241
|
+
for (const sym of ltSpotSymbols) {
|
|
242
|
+
try {
|
|
243
|
+
const book = await fetch(`https://mainnet.zklighter.elliot.ai/api/v1/orderBookOrders?market_id=${
|
|
244
|
+
// We don't have market_id here, skip detailed price fetch for LT
|
|
245
|
+
// LT tokens (ETH, LINK, etc.) are direct names, low risk of mismatch
|
|
246
|
+
""}`);
|
|
247
|
+
// Skip — LT uses direct token names, not indices
|
|
248
|
+
}
|
|
249
|
+
catch { /* skip */ }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch { /* non-critical */ }
|
|
253
|
+
// Merge all perp rates by symbol
|
|
254
|
+
const perpRateMap = new Map();
|
|
255
|
+
for (const r of [...hlRates, ...ltRates, ...pacRates]) {
|
|
256
|
+
if (!r.symbol)
|
|
257
|
+
continue;
|
|
258
|
+
if (opts?.symbols && !opts.symbols.includes(r.symbol.toUpperCase()))
|
|
259
|
+
continue;
|
|
260
|
+
const key = r.symbol.toUpperCase();
|
|
261
|
+
if (!perpRateMap.has(key))
|
|
262
|
+
perpRateMap.set(key, []);
|
|
263
|
+
perpRateMap.get(key).push(r);
|
|
264
|
+
}
|
|
265
|
+
const spreads = [];
|
|
266
|
+
const minSpread = opts?.minSpread ?? 0;
|
|
267
|
+
for (const [symbol, rates] of perpRateMap) {
|
|
268
|
+
rates.sort((a, b) => Math.abs(b.hourlyRate) - Math.abs(a.hourlyRate));
|
|
269
|
+
const bestPerp = rates[0];
|
|
270
|
+
// Spot exchanges: only include if symbol has a verified spot market
|
|
271
|
+
const uniqueSpot = [];
|
|
272
|
+
// HL spot: check U-token mapped availability
|
|
273
|
+
if (hlSpotByPerp.has(symbol)) {
|
|
274
|
+
// Price cross-validation: spot price must be close to perp mark price
|
|
275
|
+
const spotPrice = hlSpotPrices.get(symbol);
|
|
276
|
+
const perpPrice = bestPerp.markPrice;
|
|
277
|
+
if (spotPrice && perpPrice > 0) {
|
|
278
|
+
const deviation = Math.abs(spotPrice - perpPrice) / perpPrice * 100;
|
|
279
|
+
if (deviation <= MAX_PRICE_DEVIATION_PCT) {
|
|
280
|
+
uniqueSpot.push("hyperliquid");
|
|
281
|
+
}
|
|
282
|
+
// else: same ticker, different token (e.g., HIP-1 TRUMP ≠ perp TRUMP) → skip
|
|
283
|
+
}
|
|
284
|
+
else if (spotPrice === undefined) {
|
|
285
|
+
// No price data (empty orderbook) → skip, can't verify
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// LT spot: direct token names, low mismatch risk
|
|
289
|
+
if (ltSpotSymbols.has(symbol))
|
|
290
|
+
uniqueSpot.push("lighter");
|
|
291
|
+
if (uniqueSpot.length === 0)
|
|
292
|
+
continue;
|
|
293
|
+
const annualSpreadPct = Math.abs(bestPerp.annualizedPct);
|
|
294
|
+
if (annualSpreadPct < minSpread)
|
|
295
|
+
continue;
|
|
296
|
+
const direction = bestPerp.hourlyRate > 0
|
|
297
|
+
? "long-spot-short-perp"
|
|
298
|
+
: "sell-spot-long-perp";
|
|
299
|
+
const notional = 1000;
|
|
300
|
+
const estHourlyIncomeUsd = Math.abs(bestPerp.hourlyRate) * notional;
|
|
301
|
+
spreads.push({
|
|
302
|
+
symbol,
|
|
303
|
+
perpExchange: bestPerp.exchange,
|
|
304
|
+
spotExchanges: uniqueSpot,
|
|
305
|
+
perpFundingRate: bestPerp.fundingRate,
|
|
306
|
+
perpHourlyRate: bestPerp.hourlyRate,
|
|
307
|
+
annualSpreadPct,
|
|
308
|
+
bestMarkPrice: bestPerp.markPrice,
|
|
309
|
+
direction,
|
|
310
|
+
estHourlyIncomeUsd,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
spreads.sort((a, b) => b.annualSpreadPct - a.annualSpreadPct);
|
|
314
|
+
return {
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
spreads,
|
|
317
|
+
};
|
|
318
|
+
}
|