perp-cli 0.4.0 → 0.4.8

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.
Files changed (79) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/arb/utils.d.ts +2 -0
  6. package/dist/arb/utils.js +1 -1
  7. package/dist/cli-spec.d.ts +0 -4
  8. package/dist/cli-spec.js +8 -1
  9. package/dist/commands/agent.js +12 -9
  10. package/dist/commands/arb/index.js +18 -3
  11. package/dist/commands/arb-auto.js +885 -67
  12. package/dist/commands/dex.js +7 -1
  13. package/dist/commands/funding.d.ts +2 -1
  14. package/dist/commands/funding.js +211 -4
  15. package/dist/commands/manage.js +1 -1
  16. package/dist/commands/market.js +1 -1
  17. package/dist/commands/multileg.d.ts +16 -0
  18. package/dist/commands/multileg.js +174 -0
  19. package/dist/commands/pnl.d.ts +12 -0
  20. package/dist/commands/pnl.js +243 -0
  21. package/dist/commands/settings.d.ts +2 -1
  22. package/dist/commands/settings.js +133 -2
  23. package/dist/commands/stream.js +132 -55
  24. package/dist/commands/trade.js +118 -43
  25. package/dist/commands/wallet.js +1 -1
  26. package/dist/config.d.ts +5 -0
  27. package/dist/config.js +12 -0
  28. package/dist/constants.d.ts +14 -0
  29. package/dist/constants.js +40 -0
  30. package/dist/dashboard/ws-feeds.js +1 -1
  31. package/dist/equity-tracker.d.ts +57 -0
  32. package/dist/equity-tracker.js +209 -0
  33. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  34. package/dist/exchanges/hyperliquid-spot.js +317 -0
  35. package/dist/exchanges/hyperliquid.d.ts +26 -5
  36. package/dist/exchanges/hyperliquid.js +181 -92
  37. package/dist/exchanges/interface.d.ts +1 -1
  38. package/dist/exchanges/lighter-spot.d.ts +55 -0
  39. package/dist/exchanges/lighter-spot.js +417 -0
  40. package/dist/exchanges/lighter.d.ts +10 -2
  41. package/dist/exchanges/lighter.js +132 -37
  42. package/dist/exchanges/pacifica.d.ts +11 -4
  43. package/dist/exchanges/pacifica.js +52 -37
  44. package/dist/exchanges/spot-interface.d.ts +45 -0
  45. package/dist/exchanges/spot-interface.js +22 -0
  46. package/dist/execution-log.d.ts +2 -2
  47. package/dist/funding/rates.d.ts +36 -0
  48. package/dist/funding/rates.js +146 -0
  49. package/dist/index.js +148 -99
  50. package/dist/liquidity.d.ts +12 -0
  51. package/dist/liquidity.js +43 -0
  52. package/dist/settings.d.ts +6 -0
  53. package/dist/settings.js +18 -2
  54. package/dist/signer/evm-local.d.ts +12 -0
  55. package/dist/signer/evm-local.js +19 -0
  56. package/dist/signer/index.d.ts +3 -0
  57. package/dist/signer/index.js +2 -0
  58. package/dist/signer/interface.d.ts +31 -0
  59. package/dist/signer/interface.js +7 -0
  60. package/dist/signer/solana-local.d.ts +13 -0
  61. package/dist/signer/solana-local.js +19 -0
  62. package/dist/smart-order.d.ts +44 -0
  63. package/dist/smart-order.js +145 -0
  64. package/dist/trade-validator.js +2 -1
  65. package/dist/utils.js +7 -1
  66. package/dist/ws/market-feed.d.ts +74 -0
  67. package/dist/ws/market-feed.js +227 -0
  68. package/package.json +1 -1
  69. package/skills/perp-cli/SKILL.md +102 -4
  70. package/skills/perp-cli/references/commands.md +16 -1
  71. package/skills/perp-cli/references/strategies.md +1 -1
  72. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  73. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  74. package/skills/perp-cli/scripts/preflight.sh +98 -0
  75. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  76. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  77. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  78. package/skills/perp-cli/templates/open-position.sh +0 -38
  79. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Equity Tracker — time-series equity snapshots for PnL analysis.
3
+ *
4
+ * Stores snapshots in ~/.perp/equity-history/YYYY-MM.jsonl (monthly files).
5
+ * Computes daily PnL, Sharpe ratio, max drawdown, win/loss stats.
6
+ */
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ import { mkdirSync, appendFileSync, existsSync, readdirSync, readFileSync } from "fs";
10
+ const BASE_DIR = join(homedir(), ".perp", "equity-history");
11
+ // ── Storage ──
12
+ function getFilePath(date) {
13
+ const ym = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
14
+ return join(BASE_DIR, `${ym}.jsonl`);
15
+ }
16
+ export function saveEquitySnapshot(snap) {
17
+ mkdirSync(BASE_DIR, { recursive: true });
18
+ const file = getFilePath(new Date(snap.ts));
19
+ appendFileSync(file, JSON.stringify(snap) + "\n");
20
+ }
21
+ export function readEquityHistory(opts) {
22
+ if (!existsSync(BASE_DIR))
23
+ return [];
24
+ const files = readdirSync(BASE_DIR)
25
+ .filter(f => f.endsWith(".jsonl"))
26
+ .sort();
27
+ const results = [];
28
+ for (const file of files) {
29
+ // Skip files clearly before range by filename (YYYY-MM.jsonl)
30
+ if (opts?.since) {
31
+ const ym = file.replace(".jsonl", "");
32
+ const fileMonth = new Date(ym + "-01");
33
+ const sinceMonth = new Date(opts.since.getFullYear(), opts.since.getMonth(), 1);
34
+ if (fileMonth < sinceMonth)
35
+ continue;
36
+ }
37
+ const content = readFileSync(join(BASE_DIR, file), "utf-8");
38
+ for (const line of content.split("\n")) {
39
+ if (!line.trim())
40
+ continue;
41
+ try {
42
+ const snap = JSON.parse(line);
43
+ if (opts?.exchange && snap.exchange !== opts.exchange)
44
+ continue;
45
+ const ts = new Date(snap.ts);
46
+ if (opts?.since && ts < opts.since)
47
+ continue;
48
+ if (opts?.until && ts > opts.until)
49
+ continue;
50
+ results.push(snap);
51
+ }
52
+ catch { /* skip malformed lines */ }
53
+ }
54
+ }
55
+ return results;
56
+ }
57
+ // ── Analysis ──
58
+ /** Group snapshots by date, take first and last per day for daily PnL. */
59
+ export function computeDailyPnl(snapshots, exchange) {
60
+ // If exchange filter provided, apply it
61
+ const filtered = exchange
62
+ ? snapshots.filter(s => s.exchange === exchange)
63
+ : snapshots;
64
+ // Aggregate across exchanges if no specific exchange requested
65
+ const byTimestamp = new Map();
66
+ for (const s of filtered) {
67
+ const key = s.ts;
68
+ byTimestamp.set(key, (byTimestamp.get(key) ?? 0) + s.equity);
69
+ }
70
+ // Group by date
71
+ const byDate = new Map();
72
+ for (const s of filtered) {
73
+ const date = s.ts.slice(0, 10); // YYYY-MM-DD
74
+ const eq = exchange ? s.equity : (byTimestamp.get(s.ts) ?? s.equity);
75
+ const existing = byDate.get(date);
76
+ if (!existing) {
77
+ byDate.set(date, { first: { ts: s.ts, eq }, last: { ts: s.ts, eq } });
78
+ }
79
+ else {
80
+ if (s.ts < existing.first.ts)
81
+ existing.first = { ts: s.ts, eq };
82
+ if (s.ts > existing.last.ts)
83
+ existing.last = { ts: s.ts, eq };
84
+ }
85
+ }
86
+ const daily = [];
87
+ const dates = [...byDate.keys()].sort();
88
+ for (let i = 0; i < dates.length; i++) {
89
+ const date = dates[i];
90
+ const day = byDate.get(date);
91
+ // Use previous day's end as start, or this day's first if no prev
92
+ const startEquity = i > 0 ? byDate.get(dates[i - 1]).last.eq : day.first.eq;
93
+ const endEquity = day.last.eq;
94
+ const pnl = endEquity - startEquity;
95
+ const pnlPct = startEquity > 0 ? (pnl / startEquity) * 100 : 0;
96
+ daily.push({
97
+ date,
98
+ exchange: exchange ?? "all",
99
+ startEquity,
100
+ endEquity,
101
+ pnl,
102
+ pnlPct,
103
+ });
104
+ }
105
+ return daily;
106
+ }
107
+ /** Compute comprehensive PnL metrics from equity snapshots. */
108
+ export function computePnlMetrics(snapshots, exchange) {
109
+ const daily = computeDailyPnl(snapshots, exchange);
110
+ if (daily.length === 0) {
111
+ return {
112
+ totalReturn: 0, totalReturnPct: 0, dailyReturns: [],
113
+ sharpeRatio: 0, maxDrawdown: 0, maxDrawdownPct: 0,
114
+ winDays: 0, lossDays: 0, winRate: 0,
115
+ peakEquity: 0, currentDrawdown: 0, currentDrawdownPct: 0,
116
+ bestDay: null, worstDay: null, avgDailyPnl: 0,
117
+ period: { from: "", to: "", days: 0 },
118
+ };
119
+ }
120
+ const firstEquity = daily[0].startEquity;
121
+ const lastEquity = daily[daily.length - 1].endEquity;
122
+ const totalReturn = lastEquity - firstEquity;
123
+ const totalReturnPct = firstEquity > 0 ? (totalReturn / firstEquity) * 100 : 0;
124
+ // Win/loss stats
125
+ const winDays = daily.filter(d => d.pnl > 0).length;
126
+ const lossDays = daily.filter(d => d.pnl < 0).length;
127
+ const winRate = daily.length > 0 ? (winDays / daily.length) * 100 : 0;
128
+ // Best/worst day
129
+ const bestDay = daily.reduce((a, b) => (b.pnl > a.pnl ? b : a), daily[0]);
130
+ const worstDay = daily.reduce((a, b) => (b.pnl < a.pnl ? b : a), daily[0]);
131
+ // Average daily PnL
132
+ const avgDailyPnl = daily.reduce((sum, d) => sum + d.pnl, 0) / daily.length;
133
+ // Sharpe ratio (annualized, assuming 365 trading days for crypto)
134
+ const dailyReturns = daily.map(d => d.pnlPct / 100);
135
+ const meanReturn = dailyReturns.reduce((s, r) => s + r, 0) / dailyReturns.length;
136
+ const variance = dailyReturns.reduce((s, r) => s + (r - meanReturn) ** 2, 0) / dailyReturns.length;
137
+ const stdDev = Math.sqrt(variance);
138
+ const sharpeRatio = stdDev > 0 ? (meanReturn / stdDev) * Math.sqrt(365) : 0;
139
+ // Max drawdown
140
+ let peakEquity = 0;
141
+ let maxDrawdown = 0;
142
+ let maxDrawdownPct = 0;
143
+ for (const d of daily) {
144
+ if (d.endEquity > peakEquity)
145
+ peakEquity = d.endEquity;
146
+ const dd = peakEquity - d.endEquity;
147
+ const ddPct = peakEquity > 0 ? (dd / peakEquity) * 100 : 0;
148
+ if (dd > maxDrawdown) {
149
+ maxDrawdown = dd;
150
+ maxDrawdownPct = ddPct;
151
+ }
152
+ }
153
+ const currentDrawdown = peakEquity - lastEquity;
154
+ const currentDrawdownPct = peakEquity > 0 ? (currentDrawdown / peakEquity) * 100 : 0;
155
+ return {
156
+ totalReturn,
157
+ totalReturnPct,
158
+ dailyReturns: daily,
159
+ sharpeRatio,
160
+ maxDrawdown,
161
+ maxDrawdownPct,
162
+ winDays,
163
+ lossDays,
164
+ winRate,
165
+ peakEquity,
166
+ currentDrawdown,
167
+ currentDrawdownPct,
168
+ bestDay,
169
+ worstDay,
170
+ avgDailyPnl,
171
+ period: {
172
+ from: daily[0].date,
173
+ to: daily[daily.length - 1].date,
174
+ days: daily.length,
175
+ },
176
+ };
177
+ }
178
+ /** Compute weekly PnL from daily PnL data. */
179
+ export function aggregateWeekly(daily) {
180
+ if (daily.length === 0)
181
+ return [];
182
+ const weeks = new Map();
183
+ for (const d of daily) {
184
+ // ISO week start (Monday)
185
+ const dt = new Date(d.date);
186
+ const day = dt.getDay();
187
+ const monday = new Date(dt);
188
+ monday.setDate(dt.getDate() - ((day + 6) % 7));
189
+ const weekKey = monday.toISOString().slice(0, 10);
190
+ const existing = weeks.get(weekKey);
191
+ if (!existing) {
192
+ weeks.set(weekKey, { start: d, end: d });
193
+ }
194
+ else {
195
+ if (d.date < existing.start.date)
196
+ existing.start = d;
197
+ if (d.date > existing.end.date)
198
+ existing.end = d;
199
+ }
200
+ }
201
+ return [...weeks.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([weekStart, { start, end }]) => ({
202
+ date: weekStart,
203
+ exchange: start.exchange,
204
+ startEquity: start.startEquity,
205
+ endEquity: end.endEquity,
206
+ pnl: end.endEquity - start.startEquity,
207
+ pnlPct: start.startEquity > 0 ? ((end.endEquity - start.startEquity) / start.startEquity) * 100 : 0,
208
+ }));
209
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Hyperliquid Spot Adapter.
3
+ *
4
+ * Uses the same exchange.placeOrder() endpoint as perps — the only difference
5
+ * is the asset index: spot assets use `10000 + spotIndex`.
6
+ *
7
+ * Composes with HyperliquidAdapter for signing and API access.
8
+ */
9
+ import type { SpotAdapter, SpotMarketInfo, SpotBalance } from "./spot-interface.js";
10
+ import type { HyperliquidAdapter } from "./hyperliquid.js";
11
+ export declare class HyperliquidSpotAdapter implements SpotAdapter {
12
+ readonly name = "hyperliquid";
13
+ private _hl;
14
+ private _spotAssetMap;
15
+ private _spotAssetReverse;
16
+ private _spotDecimals;
17
+ private _initialized;
18
+ constructor(hlAdapter: HyperliquidAdapter);
19
+ /** Load spot metadata from the Hyperliquid spot API */
20
+ init(): Promise<void>;
21
+ private _loadSpotMeta;
22
+ getSpotMarkets(): Promise<SpotMarketInfo[]>;
23
+ getSpotOrderbook(symbol: string): Promise<{
24
+ bids: [string, string][];
25
+ asks: [string, string][];
26
+ }>;
27
+ getSpotBalances(): Promise<SpotBalance[]>;
28
+ spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
29
+ spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
30
+ tif?: string;
31
+ }): Promise<unknown>;
32
+ spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
33
+ /**
34
+ * Transfer USDC between perp and spot accounts.
35
+ * HL uses separate balances — spot buys require USDC in the spot account.
36
+ * Uses SDK's transferBetweenSpotAndPerp (userSignedAction, not L1 action).
37
+ */
38
+ transferUsdcToSpot(amount: number): Promise<unknown>;
39
+ transferUsdcToPerp(amount: number): Promise<unknown>;
40
+ /** Get the spot asset index (without the 10000 offset) */
41
+ getSpotIndex(base: string): number | undefined;
42
+ /** Get spot size decimals for a base token */
43
+ getSpotDecimals(base: string): {
44
+ size: number;
45
+ price: number;
46
+ };
47
+ /**
48
+ * Resolve a symbol to its actual spot base token and index.
49
+ * Handles U-token mapping: "BTC" → "UBTC" (Unit protocol bridged token).
50
+ * Throws if no mapping found.
51
+ */
52
+ private _resolveBase;
53
+ /**
54
+ * Get the perp symbol for a spot token.
55
+ * e.g., "UBTC" → "BTC", "ETH" → "ETH"
56
+ */
57
+ getUnderlying(spotBase: string): string;
58
+ /**
59
+ * Validate that an order actually filled.
60
+ * HL API returns status "ok" even for 0-fill IoC orders.
61
+ * Response shape: { status: "ok", response: { type: "order", data: { statuses: [...] } } }
62
+ * Each status is one of: { filled: { totalSz, avgPx, oid } }, { resting: { oid } }, { error: "msg" }
63
+ */
64
+ private _validateOrderFill;
65
+ private _rawPlaceSpotOrder;
66
+ private _signAndSendAction;
67
+ private _infoPost;
68
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Hyperliquid Spot Adapter.
3
+ *
4
+ * Uses the same exchange.placeOrder() endpoint as perps — the only difference
5
+ * is the asset index: spot assets use `10000 + spotIndex`.
6
+ *
7
+ * Composes with HyperliquidAdapter for signing and API access.
8
+ */
9
+ import { PERP_TO_SPOT_MAP, SPOT_PERP_TOKEN_MAP } from "./spot-interface.js";
10
+ /** HL official price rounding: 5 significant figures, max 8 decimals for spot */
11
+ function hlRoundPrice(px, szDecimals) {
12
+ const MAX_DECIMALS = 8; // spot uses 8 (perp uses 6)
13
+ if (px > 100_000)
14
+ return String(Math.round(px));
15
+ const sig5 = Number(px.toPrecision(5));
16
+ const priceDec = Math.max(0, MAX_DECIMALS - szDecimals);
17
+ return Number(sig5.toFixed(priceDec)).toString();
18
+ }
19
+ /** HL official size rounding */
20
+ function hlRoundSize(sz, szDecimals) {
21
+ return sz.toFixed(szDecimals);
22
+ }
23
+ export class HyperliquidSpotAdapter {
24
+ name = "hyperliquid";
25
+ _hl;
26
+ _spotAssetMap = new Map(); // "ETH" → spotIndex (not +10000)
27
+ _spotAssetReverse = new Map(); // spotIndex → "ETH"
28
+ _spotDecimals = new Map();
29
+ _initialized = false;
30
+ constructor(hlAdapter) {
31
+ this._hl = hlAdapter;
32
+ }
33
+ /** Load spot metadata from the Hyperliquid spot API */
34
+ async init() {
35
+ if (this._initialized)
36
+ return;
37
+ await this._loadSpotMeta();
38
+ this._initialized = true;
39
+ }
40
+ async _loadSpotMeta() {
41
+ try {
42
+ const meta = await this._infoPost({ type: "spotMeta" });
43
+ if (!meta?.universe)
44
+ return;
45
+ // Token index → name map
46
+ const tokenNames = new Map();
47
+ for (const t of meta.tokens ?? []) {
48
+ tokenNames.set(t.index, t.name);
49
+ }
50
+ // Find USDC token index for filtering
51
+ const usdcTokenIndex = meta.tokens?.find(t => t.name === "USDC")?.index ?? 0;
52
+ for (const u of meta.universe) {
53
+ const baseToken = tokenNames.get(u.tokens[0]) ?? "";
54
+ if (!baseToken)
55
+ continue;
56
+ const key = baseToken.toUpperCase();
57
+ // Prefer /USDC pairs (quote token = USDC). Skip non-USDC pairs if we already have one.
58
+ const isUsdcPair = u.tokens[1] === usdcTokenIndex;
59
+ if (this._spotAssetMap.has(key) && !isUsdcPair)
60
+ continue;
61
+ this._spotAssetMap.set(key, u.index);
62
+ this._spotAssetReverse.set(u.index, key);
63
+ }
64
+ // Get spot decimals from metaAndAssetCtxs
65
+ const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
66
+ if (metaCtx?.[0]?.universe) {
67
+ for (const u of metaCtx[0].universe) {
68
+ const baseToken = tokenNames.get(u.tokens[0]) ?? "";
69
+ if (!baseToken)
70
+ continue;
71
+ const key = baseToken.toUpperCase();
72
+ const isUsdcPair = u.tokens[1] === usdcTokenIndex;
73
+ if (this._spotDecimals.has(key) && !isUsdcPair)
74
+ continue;
75
+ this._spotDecimals.set(key, {
76
+ size: u.szDecimals ?? 2,
77
+ price: 6, // HL spot prices use up to 6 significant digits
78
+ });
79
+ }
80
+ }
81
+ }
82
+ catch (e) {
83
+ console.error("[hl-spot] Failed to load spot meta:", e instanceof Error ? e.message : e);
84
+ }
85
+ }
86
+ async getSpotMarkets() {
87
+ await this.init();
88
+ try {
89
+ const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
90
+ if (!metaCtx?.[0]?.universe)
91
+ return [];
92
+ const tokenNames = new Map();
93
+ for (const t of metaCtx[0].tokens ?? []) {
94
+ tokenNames.set(t.index, t.name);
95
+ }
96
+ const ctxs = metaCtx[1] ?? [];
97
+ const markets = [];
98
+ for (let i = 0; i < metaCtx[0].universe.length; i++) {
99
+ const u = metaCtx[0].universe[i];
100
+ const ctx = ctxs[i] ?? {};
101
+ const baseToken = tokenNames.get(u.tokens[0]) ?? "";
102
+ const quoteToken = tokenNames.get(u.tokens[1]) ?? "USDC";
103
+ if (!baseToken)
104
+ continue;
105
+ markets.push({
106
+ symbol: `${baseToken}/USDC`,
107
+ baseToken,
108
+ quoteToken,
109
+ markPrice: String(ctx.markPx ?? ctx.midPx ?? "0"),
110
+ volume24h: String(ctx.dayNtlVlm ?? "0"),
111
+ sizeDecimals: u.szDecimals ?? 2,
112
+ priceDecimals: 6,
113
+ });
114
+ }
115
+ return markets;
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ }
121
+ async getSpotOrderbook(symbol) {
122
+ await this.init();
123
+ const { spotIndex } = this._resolveBase(symbol);
124
+ // HL spot L2 book uses the universe name, which is like "ETH/USDC" internally
125
+ // But the API takes coin=<name>, where name is from spotMeta universe
126
+ const book = await this._infoPost({
127
+ type: "l2Book",
128
+ coin: `@${spotIndex}`,
129
+ });
130
+ const levels = book?.levels ?? [[], []];
131
+ return {
132
+ bids: (levels[0] ?? []).map((l) => [
133
+ String(l.px ?? "0"),
134
+ String(l.sz ?? "0"),
135
+ ]),
136
+ asks: (levels[1] ?? []).map((l) => [
137
+ String(l.px ?? "0"),
138
+ String(l.sz ?? "0"),
139
+ ]),
140
+ };
141
+ }
142
+ async getSpotBalances() {
143
+ try {
144
+ const state = await this._hl.client.info.spot.getSpotClearinghouseState(this._hl.address);
145
+ const balances = state?.balances ?? [];
146
+ return balances.map((b) => ({
147
+ token: String(b.coin ?? ""),
148
+ total: String(b.total ?? "0"),
149
+ available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
150
+ held: String(b.hold ?? "0"),
151
+ }));
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ }
157
+ async spotMarketOrder(symbol, side, size) {
158
+ await this.init();
159
+ const { base, spotIndex } = this._resolveBase(symbol);
160
+ const assetIndex = 10000 + spotIndex;
161
+ // Get mid price for slippage calculation
162
+ const book = await this.getSpotOrderbook(symbol);
163
+ const bestAsk = book.asks.length > 0 ? Number(book.asks[0][0]) : 0;
164
+ const bestBid = book.bids.length > 0 ? Number(book.bids[0][0]) : 0;
165
+ const midPrice = (bestAsk + bestBid) / 2;
166
+ if (midPrice <= 0)
167
+ throw new Error(`Cannot get price for spot ${base}`);
168
+ // 5% slippage for market orders
169
+ const slippage = 0.05;
170
+ const isBuy = side === "buy";
171
+ const slippagePrice = isBuy ? midPrice * (1 + slippage) : midPrice * (1 - slippage);
172
+ const dec = this._spotDecimals.get(base);
173
+ const szDec = dec?.size ?? 2;
174
+ const result = await this._rawPlaceSpotOrder({
175
+ assetIndex,
176
+ isBuy,
177
+ price: hlRoundPrice(slippagePrice, szDec),
178
+ size: hlRoundSize(Number(size), szDec),
179
+ orderType: { limit: { tif: "Ioc" } },
180
+ });
181
+ // Validate fill — HL returns status "ok" even for 0-fill IoC orders
182
+ this._validateOrderFill(result, base, side, size);
183
+ return result;
184
+ }
185
+ async spotLimitOrder(symbol, side, price, size, opts) {
186
+ await this.init();
187
+ const { base, spotIndex } = this._resolveBase(symbol);
188
+ const assetIndex = 10000 + spotIndex;
189
+ const rawTif = opts?.tif ?? "Gtc";
190
+ const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
191
+ const tif = tifMap[rawTif.toUpperCase()] ?? rawTif;
192
+ const dec = this._spotDecimals.get(base);
193
+ const szDec = dec?.size ?? 2;
194
+ return this._rawPlaceSpotOrder({
195
+ assetIndex,
196
+ isBuy: side === "buy",
197
+ price: hlRoundPrice(Number(price), szDec),
198
+ size: hlRoundSize(Number(size), szDec),
199
+ orderType: { limit: { tif } },
200
+ });
201
+ }
202
+ async spotCancelOrder(symbol, orderId) {
203
+ await this.init();
204
+ const { spotIndex } = this._resolveBase(symbol);
205
+ const assetIndex = 10000 + spotIndex;
206
+ return this._signAndSendAction({
207
+ type: "cancel",
208
+ cancels: [{ a: assetIndex, o: parseInt(orderId) }],
209
+ });
210
+ }
211
+ /**
212
+ * Transfer USDC between perp and spot accounts.
213
+ * HL uses separate balances — spot buys require USDC in the spot account.
214
+ * Uses SDK's transferBetweenSpotAndPerp (userSignedAction, not L1 action).
215
+ */
216
+ async transferUsdcToSpot(amount) {
217
+ return this._hl.client.exchange.transferBetweenSpotAndPerp(amount, false);
218
+ }
219
+ async transferUsdcToPerp(amount) {
220
+ return this._hl.client.exchange.transferBetweenSpotAndPerp(amount, true);
221
+ }
222
+ /** Get the spot asset index (without the 10000 offset) */
223
+ getSpotIndex(base) {
224
+ return this._spotAssetMap.get(base.toUpperCase());
225
+ }
226
+ /** Get spot size decimals for a base token */
227
+ getSpotDecimals(base) {
228
+ return this._spotDecimals.get(base.toUpperCase()) ?? { size: 2, price: 6 };
229
+ }
230
+ /**
231
+ * Resolve a symbol to its actual spot base token and index.
232
+ * Handles U-token mapping: "BTC" → "UBTC" (Unit protocol bridged token).
233
+ * Throws if no mapping found.
234
+ */
235
+ _resolveBase(symbol) {
236
+ const raw = symbol.split("/")[0].toUpperCase();
237
+ // 1. Direct match (e.g., "ETH" → ETH spot, or "UBTC" → UBTC spot)
238
+ const directIndex = this._spotAssetMap.get(raw);
239
+ if (directIndex !== undefined)
240
+ return { base: raw, spotIndex: directIndex };
241
+ // 2. U-token mapping: "BTC" → "UBTC" (perp name → spot token name)
242
+ const uToken = PERP_TO_SPOT_MAP[raw];
243
+ if (uToken) {
244
+ const mappedIndex = this._spotAssetMap.get(uToken);
245
+ if (mappedIndex !== undefined)
246
+ return { base: uToken, spotIndex: mappedIndex };
247
+ }
248
+ throw new Error(`Unknown HL spot asset: ${raw} (no direct match or U-token mapping)`);
249
+ }
250
+ /**
251
+ * Get the perp symbol for a spot token.
252
+ * e.g., "UBTC" → "BTC", "ETH" → "ETH"
253
+ */
254
+ getUnderlying(spotBase) {
255
+ const upper = spotBase.toUpperCase();
256
+ return SPOT_PERP_TOKEN_MAP[upper] ?? upper;
257
+ }
258
+ // ── Private helpers ──
259
+ /**
260
+ * Validate that an order actually filled.
261
+ * HL API returns status "ok" even for 0-fill IoC orders.
262
+ * Response shape: { status: "ok", response: { type: "order", data: { statuses: [...] } } }
263
+ * Each status is one of: { filled: { totalSz, avgPx, oid } }, { resting: { oid } }, { error: "msg" }
264
+ */
265
+ _validateOrderFill(result, base, side, requestedSize) {
266
+ const r = result;
267
+ const statuses = r?.response?.data?.statuses;
268
+ if (!statuses || statuses.length === 0) {
269
+ throw new Error(`Spot ${side} ${base}: no order status in response`);
270
+ }
271
+ const st = statuses[0];
272
+ if (st.error) {
273
+ throw new Error(`Spot ${side} ${base}: ${st.error}`);
274
+ }
275
+ const filled = st.filled;
276
+ if (!filled || Number(filled.totalSz ?? 0) === 0) {
277
+ // Resting or 0-fill → for IoC market orders this means no fill
278
+ throw new Error(`Spot ${side} ${base}: order not filled (0 of ${requestedSize} ${base}). Check price/liquidity.`);
279
+ }
280
+ }
281
+ async _rawPlaceSpotOrder(opts) {
282
+ const trimZeros = (s) => {
283
+ if (!s.includes("."))
284
+ return s;
285
+ const n = s.replace(/\.?0+$/, "");
286
+ return n === "-0" ? "0" : n;
287
+ };
288
+ const orderWire = {
289
+ a: opts.assetIndex,
290
+ b: opts.isBuy,
291
+ p: trimZeros(opts.price),
292
+ s: trimZeros(opts.size),
293
+ r: false,
294
+ t: opts.orderType,
295
+ };
296
+ const action = {
297
+ type: "order",
298
+ orders: [orderWire],
299
+ grouping: "na",
300
+ };
301
+ return this._signAndSendAction(action);
302
+ }
303
+ async _signAndSendAction(action) {
304
+ return this._hl.exchangeAction(action);
305
+ }
306
+ async _infoPost(body) {
307
+ const baseUrl = this._hl.isTestnet
308
+ ? "https://api.hyperliquid-testnet.xyz"
309
+ : "https://api.hyperliquid.xyz";
310
+ const res = await fetch(`${baseUrl}/info`, {
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/json" },
313
+ body: JSON.stringify(body),
314
+ });
315
+ return res.json();
316
+ }
317
+ }
@@ -1,22 +1,27 @@
1
1
  import { Hyperliquid } from "hyperliquid";
2
2
  import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
3
+ import type { EvmSigner } from "../signer/interface.js";
3
4
  export declare class HyperliquidAdapter implements ExchangeAdapter {
4
5
  readonly name = "hyperliquid";
5
6
  private sdk;
6
7
  private _address;
7
8
  private _privateKey;
8
9
  private _testnet;
10
+ private _evmSigner?;
9
11
  private _assetMap;
10
12
  private _assetMapReverse;
13
+ private _szDecimalsMap;
11
14
  /** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
12
15
  private _dex;
13
- constructor(privateKey: string, testnet?: boolean);
16
+ constructor(privateKey?: string, testnet?: boolean);
14
17
  /** Set the HIP-3 deployed perp dex to query/trade on. */
15
18
  setDex(dex: string): void;
16
19
  get dex(): string;
17
20
  get client(): Hyperliquid;
18
21
  get address(): string;
19
22
  get isTestnet(): boolean;
23
+ /** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
24
+ setSigner(signer: EvmSigner): void;
20
25
  init(): Promise<void>;
21
26
  /** Load asset index map — supports native and HIP-3 dex. */
22
27
  private _loadAssetMap;
@@ -25,25 +30,36 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
25
30
  * Handles: "ICP" → "ICP-PERP", "BTC-PERP" → "BTC-PERP", "km:GOOGL" → "km:GOOGL"
26
31
  */
27
32
  resolveSymbol(symbol: string): string;
28
- getAssetIndex(symbol: string): number;
33
+ /** Get szDecimals for a symbol (cached from init/getMeta) */
34
+ getSzDecimals(symbol: string): number;
35
+ /** HL official price rounding: 5 significant figures */
36
+ private _hlRoundPrice;
37
+ getAssetIndex(symbol: string): Promise<number>;
29
38
  getMarkets(): Promise<ExchangeMarketInfo[]>;
30
39
  getOrderbook(symbol: string): Promise<{
31
40
  bids: [string, string][];
32
41
  asks: [string, string][];
33
42
  }>;
43
+ private ensureAddress;
34
44
  /** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
35
45
  private _getClearinghouseState;
36
46
  getBalance(): Promise<ExchangeBalance>;
37
47
  getPositions(): Promise<ExchangePosition[]>;
38
48
  getOpenOrders(): Promise<ExchangeOrder[]>;
39
49
  private _invalidateAccountCache;
40
- marketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<any>;
50
+ marketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
41
51
  /**
42
52
  * Place a market order on a HIP-3 deployed dex.
43
53
  * Bypasses SDK's symbolConversion (which only knows native perps)
44
54
  * and constructs + signs the order action directly.
45
55
  */
46
56
  private _dexMarketOrder;
57
+ /**
58
+ * Sign and send any exchange action via raw EIP-712 signing.
59
+ * Bypasses the SDK entirely — works for order, batchModify, twapOrder, etc.
60
+ */
61
+ private ensureSigner;
62
+ private _signAndSendAction;
47
63
  /**
48
64
  * Place an order via raw exchange API with EIP-712 signing.
49
65
  * This bypasses the SDK's symbolConversion and supports dex-specific orders.
@@ -52,7 +68,7 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
52
68
  limitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
53
69
  reduceOnly?: boolean;
54
70
  tif?: string;
55
- }): Promise<any>;
71
+ }): Promise<unknown>;
56
72
  cancelOrder(symbol: string, orderId: string): Promise<import("hyperliquid").CancelOrderResponse>;
57
73
  cancelAllOrders(symbol?: string): Promise<import("hyperliquid").CancelOrderResponse[]>;
58
74
  editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
@@ -69,7 +85,7 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
69
85
  getFundingHistory(symbol: string, limit?: number): Promise<{
70
86
  time: number;
71
87
  rate: string;
72
- price: string;
88
+ price: string | null;
73
89
  }[]>;
74
90
  /**
75
91
  * Place a trigger order (stop loss / take profit).
@@ -209,6 +225,11 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
209
225
  * POST to /info endpoint.
210
226
  */
211
227
  private _infoPost;
228
+ /**
229
+ * Public entry point for sending signed exchange actions.
230
+ * Used by HyperliquidSpotAdapter to delegate signing.
231
+ */
232
+ exchangeAction(action: Record<string, unknown>): Promise<unknown>;
212
233
  /**
213
234
  * Send a raw exchange action through the SDK.
214
235
  * Used for methods not directly exposed by the SDK.