perp-cli 0.6.4 → 0.7.1

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 (177) hide show
  1. package/README.md +7 -2
  2. package/dist/api/public/hyperliquid.d.ts +2 -2
  3. package/dist/api/public/hyperliquid.js +0 -10
  4. package/dist/api/public/lighter.d.ts +2 -1
  5. package/dist/api/public/pacifica.d.ts +2 -1
  6. package/dist/arb/index.d.ts +7 -4
  7. package/dist/arb/index.js +5 -4
  8. package/dist/arb/sizing.d.ts +1 -1
  9. package/dist/arb/state.d.ts +1 -1
  10. package/dist/arb/state.js +4 -3
  11. package/dist/arb/utils.d.ts +0 -10
  12. package/dist/arb/utils.js +0 -42
  13. package/dist/bot/apex/guard.d.ts +26 -0
  14. package/dist/bot/apex/guard.js +86 -0
  15. package/dist/bot/apex/index.d.ts +6 -0
  16. package/dist/bot/apex/index.js +4 -0
  17. package/dist/bot/apex/pulse.d.ts +22 -0
  18. package/dist/bot/apex/pulse.js +107 -0
  19. package/dist/bot/apex/radar.d.ts +17 -0
  20. package/dist/bot/apex/radar.js +150 -0
  21. package/dist/bot/conditions.d.ts +1 -1
  22. package/dist/bot/config.d.ts +5 -1
  23. package/dist/bot/config.js +1 -1
  24. package/dist/bot/engine.d.ts +23 -2
  25. package/dist/bot/engine.js +296 -351
  26. package/dist/bot/index.d.ts +16 -0
  27. package/dist/bot/index.js +10 -0
  28. package/dist/bot/indicators.d.ts +29 -0
  29. package/dist/bot/indicators.js +144 -0
  30. package/dist/bot/reflect.d.ts +34 -0
  31. package/dist/bot/reflect.js +155 -0
  32. package/dist/bot/strategies/aggressive-taker.d.ts +36 -0
  33. package/dist/bot/strategies/aggressive-taker.js +125 -0
  34. package/dist/bot/strategies/apex-strategy.d.ts +36 -0
  35. package/dist/bot/strategies/apex-strategy.js +185 -0
  36. package/dist/bot/strategies/avellaneda-mm.d.ts +27 -0
  37. package/dist/bot/strategies/avellaneda-mm.js +111 -0
  38. package/dist/bot/strategies/basis-arb.d.ts +27 -0
  39. package/dist/bot/strategies/basis-arb.js +97 -0
  40. package/dist/bot/strategies/claude-agent.d.ts +31 -0
  41. package/dist/bot/strategies/claude-agent.js +155 -0
  42. package/dist/bot/strategies/dca-strategy.d.ts +25 -0
  43. package/dist/bot/strategies/dca-strategy.js +59 -0
  44. package/dist/bot/strategies/engine-mm.d.ts +29 -0
  45. package/dist/bot/strategies/engine-mm.js +141 -0
  46. package/dist/bot/strategies/funding-arb-strategy.d.ts +32 -0
  47. package/dist/bot/strategies/funding-arb-strategy.js +168 -0
  48. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  49. package/dist/bot/strategies/funding-arb-v2.js +222 -0
  50. package/dist/bot/strategies/funding-auto.d.ts +1 -0
  51. package/dist/bot/strategies/funding-auto.js +572 -0
  52. package/dist/bot/strategies/grid-mm.d.ts +25 -0
  53. package/dist/bot/strategies/grid-mm.js +99 -0
  54. package/dist/bot/strategies/grid-strategy.d.ts +39 -0
  55. package/dist/bot/strategies/grid-strategy.js +176 -0
  56. package/dist/bot/strategies/hedge-agent.d.ts +25 -0
  57. package/dist/bot/strategies/hedge-agent.js +79 -0
  58. package/dist/bot/strategies/liquidation-mm.d.ts +25 -0
  59. package/dist/bot/strategies/liquidation-mm.js +108 -0
  60. package/dist/bot/strategies/mean-reversion.d.ts +35 -0
  61. package/dist/bot/strategies/mean-reversion.js +118 -0
  62. package/dist/bot/strategies/momentum-breakout.d.ts +36 -0
  63. package/dist/bot/strategies/momentum-breakout.js +159 -0
  64. package/dist/bot/strategies/regime-mm.d.ts +25 -0
  65. package/dist/bot/strategies/regime-mm.js +111 -0
  66. package/dist/bot/strategies/rfq-agent.d.ts +25 -0
  67. package/dist/bot/strategies/rfq-agent.js +71 -0
  68. package/dist/bot/strategies/simple-mm.d.ts +25 -0
  69. package/dist/bot/strategies/simple-mm.js +76 -0
  70. package/dist/bot/strategy-registry.d.ts +9 -0
  71. package/dist/bot/strategy-registry.js +14 -0
  72. package/dist/bot/strategy-types.d.ts +64 -0
  73. package/dist/bot/strategy-types.js +5 -0
  74. package/dist/bot/trade-journal.d.ts +27 -0
  75. package/dist/bot/trade-journal.js +53 -0
  76. package/dist/bot/tui/BotDashboard.d.ts +49 -0
  77. package/dist/bot/tui/BotDashboard.js +93 -0
  78. package/dist/bot/tui/index.d.ts +6 -0
  79. package/dist/bot/tui/index.js +8 -0
  80. package/dist/bridge-engine.d.ts +0 -1
  81. package/dist/bridge-engine.js +0 -1
  82. package/dist/cache.d.ts +0 -1
  83. package/dist/cache.js +0 -1
  84. package/dist/commands/account.d.ts +1 -1
  85. package/dist/commands/account.js +94 -91
  86. package/dist/commands/agent.d.ts +1 -1
  87. package/dist/commands/arb/index.d.ts +1 -1
  88. package/dist/commands/arb/index.js +1 -1
  89. package/dist/commands/arb-auto.d.ts +1 -1
  90. package/dist/commands/arb-auto.js +1 -1
  91. package/dist/commands/arb.d.ts +1 -1
  92. package/dist/commands/bot.d.ts +1 -1
  93. package/dist/commands/bot.js +198 -14
  94. package/dist/commands/dashboard.d.ts +1 -1
  95. package/dist/commands/dashboard.js +1 -1
  96. package/dist/commands/funds.d.ts +1 -1
  97. package/dist/commands/funds.js +33 -20
  98. package/dist/commands/history.d.ts +1 -1
  99. package/dist/commands/history.js +5 -4
  100. package/dist/commands/manage.d.ts +2 -3
  101. package/dist/commands/manage.js +11 -18
  102. package/dist/commands/market.d.ts +1 -1
  103. package/dist/commands/market.js +7 -7
  104. package/dist/commands/plan.d.ts +1 -1
  105. package/dist/commands/rebalance.d.ts +1 -1
  106. package/dist/commands/rebalance.js +8 -9
  107. package/dist/commands/risk.d.ts +1 -1
  108. package/dist/commands/settings.d.ts +1 -1
  109. package/dist/commands/settings.js +3 -1
  110. package/dist/commands/trade.d.ts +1 -1
  111. package/dist/commands/trade.js +39 -52
  112. package/dist/commands/wallet.d.ts +0 -2
  113. package/dist/commands/wallet.js +0 -5
  114. package/dist/constants.d.ts +0 -3
  115. package/dist/constants.js +0 -4
  116. package/dist/cross-chain-margin.d.ts +1 -1
  117. package/dist/dashboard/index.d.ts +4 -0
  118. package/dist/dashboard/index.js +3 -0
  119. package/dist/dashboard/server.d.ts +1 -1
  120. package/dist/dashboard/server.js +1 -1
  121. package/dist/dashboard/ws-feeds.d.ts +1 -1
  122. package/dist/event-stream.d.ts +1 -1
  123. package/dist/exchanges/aster.d.ts +58 -0
  124. package/dist/exchanges/aster.js +409 -0
  125. package/dist/exchanges/capabilities.d.ts +74 -0
  126. package/dist/exchanges/capabilities.js +44 -0
  127. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  128. package/dist/exchanges/hyperliquid-spot.js +20 -12
  129. package/dist/exchanges/hyperliquid.d.ts +8 -2
  130. package/dist/exchanges/hyperliquid.js +21 -6
  131. package/dist/exchanges/index.d.ts +11 -0
  132. package/dist/exchanges/index.js +10 -0
  133. package/dist/exchanges/interface.d.ts +3 -0
  134. package/dist/exchanges/lighter-spot.d.ts +13 -0
  135. package/dist/exchanges/lighter-spot.js +98 -26
  136. package/dist/exchanges/lighter.d.ts +9 -3
  137. package/dist/exchanges/lighter.js +18 -8
  138. package/dist/exchanges/nado.d.ts +52 -0
  139. package/dist/exchanges/nado.js +223 -0
  140. package/dist/exchanges/pacifica.d.ts +7 -1
  141. package/dist/exchanges/pacifica.js +17 -2
  142. package/dist/exchanges/registry.d.ts +13 -0
  143. package/dist/exchanges/registry.js +37 -0
  144. package/dist/exchanges/spot-interface.d.ts +9 -0
  145. package/dist/funding/rates.js +1 -1
  146. package/dist/index.js +2 -6
  147. package/dist/jobs.js +1 -1
  148. package/dist/liquidity.d.ts +1 -2
  149. package/dist/liquidity.js +12 -5
  150. package/dist/mcp-server.js +4 -2
  151. package/dist/pacifica/constants.d.ts +0 -3
  152. package/dist/pacifica/constants.js +0 -4
  153. package/dist/pacifica/types/account.d.ts +0 -8
  154. package/dist/pacifica/types/lake.d.ts +0 -6
  155. package/dist/pacifica/types/position.d.ts +0 -12
  156. package/dist/pacifica/types/ws.d.ts +0 -24
  157. package/dist/pacifica/types/ws.js +1 -41
  158. package/dist/plan-executor.d.ts +1 -1
  159. package/dist/rebalance.d.ts +1 -7
  160. package/dist/rebalance.js +0 -6
  161. package/dist/risk.d.ts +1 -1
  162. package/dist/risk.js +3 -1
  163. package/dist/smart-order.d.ts +1 -1
  164. package/dist/strategies/dca.d.ts +1 -1
  165. package/dist/strategies/funding-arb.d.ts +1 -1
  166. package/dist/strategies/funding-arb.js +193 -181
  167. package/dist/strategies/grid.d.ts +1 -1
  168. package/dist/strategies/grid.js +1 -1
  169. package/dist/strategies/index.d.ts +12 -0
  170. package/dist/strategies/index.js +7 -0
  171. package/dist/strategies/split-order.d.ts +1 -1
  172. package/dist/strategies/trailing-stop.d.ts +1 -1
  173. package/dist/strategies/twap.d.ts +1 -1
  174. package/dist/trade-validator.d.ts +1 -1
  175. package/package.json +7 -1
  176. package/skills/perp-cli/SKILL.md +20 -0
  177. package/skills/perp-cli/references/commands.md +22 -8
package/dist/risk.js CHANGED
@@ -59,7 +59,9 @@ export function getLiquidationDistances(positions, limits) {
59
59
  const liquidationPrice = Number(p.liquidationPrice);
60
60
  const distancePct = calcLiquidationDistance(markPrice, liquidationPrice, p.side);
61
61
  let status = "safe";
62
- if (distancePct < lim.minLiquidationDistance)
62
+ if (distancePct < lim.minLiquidationDistance * 0.3)
63
+ status = "critical";
64
+ else if (distancePct < lim.minLiquidationDistance)
63
65
  status = "danger";
64
66
  else if (distancePct < lim.minLiquidationDistance * 1.5)
65
67
  status = "warning";
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Fallback: if the IOC limit fails, falls back to market order.
20
20
  */
21
- import type { ExchangeAdapter } from "./exchanges/interface.js";
21
+ import type { ExchangeAdapter } from "./exchanges/index.js";
22
22
  export interface SmartOrderOpts {
23
23
  /** Extra ticks of tolerance beyond best price. Default: 1 */
24
24
  tickTolerance?: number;
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "../exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  export interface DCAParams {
3
3
  symbol: string;
4
4
  side: "buy" | "sell";
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "../exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  export interface FundingArbParams {
3
3
  minSpread: number;
4
4
  closeSpread: number;
@@ -41,6 +41,10 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
41
41
  const positions = [];
42
42
  let cycleCount = 0;
43
43
  let lastRebalanceCheck = 0;
44
+ let running = true;
45
+ const shutdown = () => { running = false; log(`[ARB] Shutdown signal received, finishing current cycle...`); };
46
+ process.on("SIGINT", shutdown);
47
+ process.on("SIGTERM", shutdown);
44
48
  log(`[ARB] Funding rate arbitrage started`);
45
49
  log(`[ARB] Entry spread: >= ${params.minSpread}% | Close spread: <= ${closeSpread}%`);
46
50
  log(`[ARB] Size: ${params.size} | Interval: ${params.intervalSec}s`);
@@ -52,211 +56,219 @@ export async function runFundingArb(adapters, params, jobId, log = console.log)
52
56
  log(`[ARB] Max drawdown: $${params.maxDrawdown}`);
53
57
  if (params.symbols?.length)
54
58
  log(`[ARB] Symbols: ${params.symbols.join(", ")}`);
55
- while (true) {
56
- cycleCount++;
57
- try {
58
- const rates = await fetchAllRates();
59
- // Group by symbol
60
- const rateMap = new Map();
61
- for (const r of rates) {
62
- if (params.symbols?.length && !params.symbols.includes(r.symbol))
63
- continue;
64
- if (!rateMap.has(r.symbol))
65
- rateMap.set(r.symbol, []);
66
- rateMap.get(r.symbol).push(r);
67
- }
68
- // ── Phase 1: Check positions for close conditions ──
69
- for (let i = positions.length - 1; i >= 0; i--) {
70
- const pos = positions[i];
71
- const symbolRates = rateMap.get(pos.symbol);
72
- if (!symbolRates)
73
- continue;
74
- const longRate = symbolRates.find((r) => r.exchange === pos.longExchange);
75
- const shortRate = symbolRates.find((r) => r.exchange === pos.shortExchange);
76
- if (!longRate || !shortRate)
77
- continue;
78
- const currentSpread = computeAnnualSpread(shortRate.fundingRate, shortRate.exchange, longRate.fundingRate, longRate.exchange);
79
- if (currentSpread <= closeSpread) {
80
- log(`[ARB] CLOSE signal: ${pos.symbol} spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`);
81
- if (params.autoExecute) {
82
- const longAdapter = adapters.get(pos.longExchange);
83
- const shortAdapter = adapters.get(pos.shortExchange);
84
- if (longAdapter && shortAdapter) {
85
- try {
86
- await Promise.all([
87
- longAdapter.marketOrder(pos.symbol, "sell", pos.size),
88
- shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
89
- ]);
90
- log(`[ARB] CLOSED ${pos.symbol} — both legs unwound`);
91
- // Estimate P&L from funding collected
92
- const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
93
- // entrySpread is annualized %; convert to per-hour rate
94
- const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
95
- const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
96
- log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
97
- }
98
- catch (err) {
99
- const msg = err instanceof Error ? err.message : String(err);
100
- log(`[ARB] Close error: ${msg}`);
101
- continue; // don't remove position if close failed
59
+ try {
60
+ while (running) {
61
+ cycleCount++;
62
+ try {
63
+ const rates = await fetchAllRates();
64
+ // Group by symbol
65
+ const rateMap = new Map();
66
+ for (const r of rates) {
67
+ if (params.symbols?.length && !params.symbols.includes(r.symbol))
68
+ continue;
69
+ if (!rateMap.has(r.symbol))
70
+ rateMap.set(r.symbol, []);
71
+ rateMap.get(r.symbol).push(r);
72
+ }
73
+ // ── Phase 1: Check positions for close conditions ──
74
+ for (let i = positions.length - 1; i >= 0; i--) {
75
+ const pos = positions[i];
76
+ const symbolRates = rateMap.get(pos.symbol);
77
+ if (!symbolRates)
78
+ continue;
79
+ const longRate = symbolRates.find((r) => r.exchange === pos.longExchange);
80
+ const shortRate = symbolRates.find((r) => r.exchange === pos.shortExchange);
81
+ if (!longRate || !shortRate)
82
+ continue;
83
+ const currentSpread = computeAnnualSpread(shortRate.fundingRate, shortRate.exchange, longRate.fundingRate, longRate.exchange);
84
+ if (currentSpread <= closeSpread) {
85
+ log(`[ARB] CLOSE signal: ${pos.symbol} spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`);
86
+ if (params.autoExecute) {
87
+ const longAdapter = adapters.get(pos.longExchange);
88
+ const shortAdapter = adapters.get(pos.shortExchange);
89
+ if (longAdapter && shortAdapter) {
90
+ try {
91
+ await Promise.all([
92
+ longAdapter.marketOrder(pos.symbol, "sell", pos.size),
93
+ shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
94
+ ]);
95
+ log(`[ARB] CLOSED ${pos.symbol} — both legs unwound`);
96
+ // Estimate P&L from funding collected
97
+ const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
98
+ // entrySpread is annualized %; convert to per-hour rate
99
+ const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
100
+ const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
101
+ log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
102
+ }
103
+ catch (err) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ log(`[ARB] Close error: ${msg}`);
106
+ continue; // don't remove position if close failed
107
+ }
102
108
  }
103
109
  }
110
+ positions.splice(i, 1);
104
111
  }
105
- positions.splice(i, 1);
106
112
  }
107
- }
108
- // ── Phase 2: Max drawdown check ──
109
- if (params.maxDrawdown && params.autoExecute && positions.length > 0) {
110
- try {
111
- const snapshots = await fetchAllBalances(adapters);
112
- const totalPnl = snapshots.reduce((s, e) => s + e.unrealizedPnl, 0);
113
- if (totalPnl < -params.maxDrawdown) {
114
- log(`[ARB] MAX DRAWDOWN hit: uPnL $${totalPnl.toFixed(2)} < -$${params.maxDrawdown}`);
115
- log(`[ARB] Closing all ${positions.length} positions...`);
116
- await closeAllPositions(positions, adapters, log);
117
- positions.length = 0;
113
+ // ── Phase 2: Max drawdown check ──
114
+ if (params.maxDrawdown && params.autoExecute && positions.length > 0) {
115
+ try {
116
+ const snapshots = await fetchAllBalances(adapters);
117
+ const totalPnl = snapshots.reduce((s, e) => s + e.unrealizedPnl, 0);
118
+ if (totalPnl < -params.maxDrawdown) {
119
+ log(`[ARB] MAX DRAWDOWN hit: uPnL $${totalPnl.toFixed(2)} < -$${params.maxDrawdown}`);
120
+ log(`[ARB] Closing all ${positions.length} positions...`);
121
+ await closeAllPositions(positions, adapters, log);
122
+ positions.length = 0;
123
+ }
118
124
  }
125
+ catch { /* non-critical */ }
119
126
  }
120
- catch { /* non-critical */ }
121
- }
122
- // ── Phase 3: Balance check + rebalance trigger ──
123
- let balanceSnapshots = null;
124
- const shouldCheckBalance = params.autoRebalance && (Date.now() - lastRebalanceCheck > 300_000); // every 5 min
125
- if (shouldCheckBalance || (params.autoExecute && positions.length < maxPos)) {
126
- try {
127
- balanceSnapshots = await fetchAllBalances(adapters);
128
- lastRebalanceCheck = Date.now();
129
- if (params.autoRebalance) {
130
- const threshold = params.rebalanceThreshold ?? 100;
131
- const lowExchanges = balanceSnapshots.filter((s) => s.available < threshold);
132
- if (lowExchanges.length > 0) {
133
- const plan = computeRebalancePlan(balanceSnapshots, { minMove: 50, reserve: 20 });
134
- if (plan.moves.length > 0) {
135
- log(`[ARB] Rebalance needed: ${plan.summary}`);
136
- for (const move of plan.moves) {
137
- log(`[ARB] $${move.amount} ${move.from} → ${move.to}`);
127
+ // ── Phase 3: Balance check + rebalance trigger ──
128
+ let balanceSnapshots = null;
129
+ const shouldCheckBalance = params.autoRebalance && (Date.now() - lastRebalanceCheck > 300_000); // every 5 min
130
+ if (shouldCheckBalance || (params.autoExecute && positions.length < maxPos)) {
131
+ try {
132
+ balanceSnapshots = await fetchAllBalances(adapters);
133
+ lastRebalanceCheck = Date.now();
134
+ if (params.autoRebalance) {
135
+ const threshold = params.rebalanceThreshold ?? 100;
136
+ const lowExchanges = balanceSnapshots.filter((s) => s.available < threshold);
137
+ if (lowExchanges.length > 0) {
138
+ const plan = computeRebalancePlan(balanceSnapshots, { minMove: 50, reserve: 20 });
139
+ if (plan.moves.length > 0) {
140
+ log(`[ARB] Rebalance needed: ${plan.summary}`);
141
+ for (const move of plan.moves) {
142
+ log(`[ARB] $${move.amount} ${move.from} → ${move.to}`);
143
+ }
144
+ log(`[ARB] Run 'perp rebalance execute' to rebalance.`);
138
145
  }
139
- log(`[ARB] Run 'perp rebalance execute' to rebalance.`);
140
146
  }
141
147
  }
142
148
  }
149
+ catch { /* non-critical */ }
143
150
  }
144
- catch { /* non-critical */ }
145
- }
146
- // ── Phase 4: Find new entry opportunities ──
147
- if (positions.length < maxPos) {
148
- for (const [symbol, symbolRates] of rateMap) {
149
- if (symbolRates.length < 2)
150
- continue;
151
- if (positions.find((p) => symbolMatch(p.symbol, symbol)))
152
- continue;
153
- if (positions.length >= maxPos)
154
- break;
155
- // Only consider exchanges we have adapters for
156
- const available = symbolRates.filter((r) => adapters.has(r.exchange));
157
- if (available.length < 2)
158
- continue;
159
- available.sort((a, b) => a.fundingRate - b.fundingRate);
160
- const lowest = available[0];
161
- const highest = available[available.length - 1];
162
- if (lowest.exchange === highest.exchange)
163
- continue;
164
- const annualSpread = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
165
- if (annualSpread >= params.minSpread) {
166
- const longEx = lowest.exchange;
167
- const shortEx = highest.exchange;
168
- log(`[ARB] ENTRY signal: ${symbol} spread ${annualSpread.toFixed(1)}% — long ${longEx} (${(lowest.fundingRate * 100).toFixed(4)}%) / short ${shortEx} (${(highest.fundingRate * 100).toFixed(4)}%)`);
169
- if (params.autoExecute) {
170
- const longAdapter = adapters.get(longEx);
171
- const shortAdapter = adapters.get(shortEx);
172
- if (!longAdapter || !shortAdapter) {
173
- log(`[ARB] Skip: adapter not available for ${longEx} or ${shortEx}`);
174
- continue;
175
- }
176
- // Check orderbook liquidity & adjust size
177
- const requestedUsd = params.sizeUsd ?? Number(params.size) * highest.markPrice;
178
- const liq = await checkArbLiquidity(longAdapter, shortAdapter, symbol, requestedUsd, 0.5, log);
179
- if (!liq.viable)
180
- continue;
181
- // Compute matched size (same for both legs)
182
- const matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
183
- if (!matched) {
184
- log(`[ARB] Skip ${symbol}: can't compute matched size (min notional or precision issue)`);
185
- continue;
186
- }
187
- // Check balances before trading
188
- if (balanceSnapshots) {
189
- if (!hasEnoughBalance(balanceSnapshots, longEx, matched.notional) ||
190
- !hasEnoughBalance(balanceSnapshots, shortEx, matched.notional)) {
191
- log(`[ARB] Skip ${symbol}: insufficient balance on ${longEx} or ${shortEx} (need ~$${matched.notional.toFixed(0)} per leg)`);
151
+ // ── Phase 4: Find new entry opportunities ──
152
+ if (positions.length < maxPos) {
153
+ for (const [symbol, symbolRates] of rateMap) {
154
+ if (symbolRates.length < 2)
155
+ continue;
156
+ if (positions.find((p) => symbolMatch(p.symbol, symbol)))
157
+ continue;
158
+ if (positions.length >= maxPos)
159
+ break;
160
+ // Only consider exchanges we have adapters for
161
+ const available = symbolRates.filter((r) => adapters.has(r.exchange));
162
+ if (available.length < 2)
163
+ continue;
164
+ available.sort((a, b) => a.fundingRate - b.fundingRate);
165
+ const lowest = available[0];
166
+ const highest = available[available.length - 1];
167
+ if (lowest.exchange === highest.exchange)
168
+ continue;
169
+ const annualSpread = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
170
+ if (annualSpread >= params.minSpread) {
171
+ const longEx = lowest.exchange;
172
+ const shortEx = highest.exchange;
173
+ log(`[ARB] ENTRY signal: ${symbol} spread ${annualSpread.toFixed(1)}% — long ${longEx} (${(lowest.fundingRate * 100).toFixed(4)}%) / short ${shortEx} (${(highest.fundingRate * 100).toFixed(4)}%)`);
174
+ if (params.autoExecute) {
175
+ const longAdapter = adapters.get(longEx);
176
+ const shortAdapter = adapters.get(shortEx);
177
+ if (!longAdapter || !shortAdapter) {
178
+ log(`[ARB] Skip: adapter not available for ${longEx} or ${shortEx}`);
192
179
  continue;
193
180
  }
194
- }
195
- try {
196
- log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
197
- await Promise.all([
198
- longAdapter.marketOrder(symbol, "buy", matched.size),
199
- shortAdapter.marketOrder(symbol, "sell", matched.size),
200
- ]);
201
- // Verify fills match, correct if needed
181
+ // Check orderbook liquidity & adjust size
182
+ const requestedUsd = params.sizeUsd ?? Number(params.size) * highest.markPrice;
183
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, symbol, requestedUsd, 0.5, log);
184
+ if (!liq.viable)
185
+ continue;
186
+ // Compute matched size (same for both legs)
187
+ const matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
188
+ if (!matched) {
189
+ log(`[ARB] Skip ${symbol}: can't compute matched size (min notional or precision issue)`);
190
+ continue;
191
+ }
192
+ // Check balances before trading
193
+ if (balanceSnapshots) {
194
+ if (!hasEnoughBalance(balanceSnapshots, longEx, matched.notional) ||
195
+ !hasEnoughBalance(balanceSnapshots, shortEx, matched.notional)) {
196
+ log(`[ARB] Skip ${symbol}: insufficient balance on ${longEx} or ${shortEx} (need ~$${matched.notional.toFixed(0)} per leg)`);
197
+ continue;
198
+ }
199
+ }
202
200
  try {
203
- const recon = await reconcileArbFills(longAdapter, shortAdapter, symbol, log);
204
- if (!recon.matched) {
205
- log(`[ARB] WARNING: fills not matched after correction attempt`);
201
+ log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
202
+ await Promise.all([
203
+ longAdapter.marketOrder(symbol, "buy", matched.size),
204
+ shortAdapter.marketOrder(symbol, "sell", matched.size),
205
+ ]);
206
+ // Verify fills match, correct if needed
207
+ try {
208
+ const recon = await reconcileArbFills(longAdapter, shortAdapter, symbol, log);
209
+ if (!recon.matched) {
210
+ log(`[ARB] WARNING: fills not matched after correction attempt`);
211
+ }
206
212
  }
213
+ catch { /* non-critical */ }
214
+ positions.push({
215
+ symbol,
216
+ longExchange: longEx,
217
+ shortExchange: shortEx,
218
+ size: matched.size,
219
+ entrySpread: annualSpread,
220
+ openedAt: Date.now(),
221
+ entryPrices: { long: lowest.markPrice, short: highest.markPrice },
222
+ });
223
+ log(`[ARB] OPENED ${symbol} delta-neutral (${positions.length}/${maxPos})`);
224
+ }
225
+ catch (err) {
226
+ const msg = err instanceof Error ? err.message : String(err);
227
+ log(`[ARB] Entry error: ${msg}`);
207
228
  }
208
- catch { /* non-critical */ }
209
- positions.push({
210
- symbol,
211
- longExchange: longEx,
212
- shortExchange: shortEx,
213
- size: matched.size,
214
- entrySpread: annualSpread,
215
- openedAt: Date.now(),
216
- entryPrices: { long: lowest.markPrice, short: highest.markPrice },
217
- });
218
- log(`[ARB] OPENED ${symbol} delta-neutral (${positions.length}/${maxPos})`);
219
- }
220
- catch (err) {
221
- const msg = err instanceof Error ? err.message : String(err);
222
- log(`[ARB] Entry error: ${msg}`);
223
229
  }
224
230
  }
225
231
  }
226
232
  }
233
+ // ── Update job state ──
234
+ if (jobId) {
235
+ updateJobState(jobId, {
236
+ result: {
237
+ cycle: cycleCount,
238
+ activePositions: positions.length,
239
+ positions: positions.map((p) => ({
240
+ symbol: p.symbol,
241
+ long: p.longExchange,
242
+ short: p.shortExchange,
243
+ size: p.size,
244
+ entrySpread: p.entrySpread,
245
+ hoursOpen: ((Date.now() - p.openedAt) / 3_600_000).toFixed(1),
246
+ })),
247
+ lastCheck: new Date().toISOString(),
248
+ },
249
+ });
250
+ }
251
+ // Periodic status log
252
+ if (cycleCount % 10 === 0) {
253
+ const posInfo = positions.length > 0
254
+ ? positions.map((p) => `${p.symbol}(${p.entrySpread.toFixed(0)}%)`).join(", ")
255
+ : "none";
256
+ log(`[ARB] Cycle ${cycleCount} | ${positions.length} positions: ${posInfo} | ${rateMap.size} symbols`);
257
+ }
227
258
  }
228
- // ── Update job state ──
229
- if (jobId) {
230
- updateJobState(jobId, {
231
- result: {
232
- cycle: cycleCount,
233
- activePositions: positions.length,
234
- positions: positions.map((p) => ({
235
- symbol: p.symbol,
236
- long: p.longExchange,
237
- short: p.shortExchange,
238
- size: p.size,
239
- entrySpread: p.entrySpread,
240
- hoursOpen: ((Date.now() - p.openedAt) / 3_600_000).toFixed(1),
241
- })),
242
- lastCheck: new Date().toISOString(),
243
- },
244
- });
245
- }
246
- // Periodic status log
247
- if (cycleCount % 10 === 0) {
248
- const posInfo = positions.length > 0
249
- ? positions.map((p) => `${p.symbol}(${p.entrySpread.toFixed(0)}%)`).join(", ")
250
- : "none";
251
- log(`[ARB] Cycle ${cycleCount} | ${positions.length} positions: ${posInfo} | ${rateMap.size} symbols`);
259
+ catch (err) {
260
+ const msg = err instanceof Error ? err.message : String(err);
261
+ log(`[ARB] Cycle error: ${msg}`);
252
262
  }
263
+ if (running)
264
+ await sleep(params.intervalSec * 1000);
253
265
  }
254
- catch (err) {
255
- const msg = err instanceof Error ? err.message : String(err);
256
- log(`[ARB] Cycle error: ${msg}`);
257
- }
258
- await sleep(params.intervalSec * 1000);
259
266
  }
267
+ finally {
268
+ process.removeListener("SIGINT", shutdown);
269
+ process.removeListener("SIGTERM", shutdown);
270
+ }
271
+ log(`[ARB] Funding rate arbitrage stopped. Positions: ${positions.length}`);
260
272
  }
261
273
  async function closeAllPositions(positions, adapters, log) {
262
274
  for (const pos of positions) {
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "../exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  export interface GridParams {
3
3
  symbol: string;
4
4
  side: "long" | "short" | "neutral";
@@ -131,7 +131,7 @@ export async function runGrid(adapter, params, jobId, log = console.log) {
131
131
  const equity = parseFloat(bal.equity);
132
132
  if (equity > state.peakEquity)
133
133
  state.peakEquity = equity;
134
- const drawdown = ((state.peakEquity - equity) / state.peakEquity) * 100;
134
+ const drawdown = state.peakEquity > 0 ? ((state.peakEquity - equity) / state.peakEquity) * 100 : 0;
135
135
  if (drawdown > params.trailingStop) {
136
136
  log(`[GRID] Trailing stop triggered (${drawdown.toFixed(1)}% drawdown). Stopping.`);
137
137
  break;
@@ -0,0 +1,12 @@
1
+ export { runGrid } from "./grid.js";
2
+ export type { GridParams, GridState } from "./grid.js";
3
+ export { runDCA } from "./dca.js";
4
+ export type { DCAParams, DCAState } from "./dca.js";
5
+ export { runTWAP } from "./twap.js";
6
+ export type { TWAPParams, TWAPState } from "./twap.js";
7
+ export { runFundingArb } from "./funding-arb.js";
8
+ export type { FundingArbParams } from "./funding-arb.js";
9
+ export { runTrailingStop } from "./trailing-stop.js";
10
+ export type { TrailingStopParams, TrailingStopResult } from "./trailing-stop.js";
11
+ export { runSplitOrder } from "./split-order.js";
12
+ export type { SplitOrderParams, SplitSlice, SplitOrderResult } from "./split-order.js";
@@ -0,0 +1,7 @@
1
+ // strategies/ barrel — public API
2
+ export { runGrid } from "./grid.js";
3
+ export { runDCA } from "./dca.js";
4
+ export { runTWAP } from "./twap.js";
5
+ export { runFundingArb } from "./funding-arb.js";
6
+ export { runTrailingStop } from "./trailing-stop.js";
7
+ export { runSplitOrder } from "./split-order.js";
@@ -12,7 +12,7 @@
12
12
  * 3. Place IOC limit at best price + slippage tolerance
13
13
  * 4. Wait for book to refresh, then repeat
14
14
  */
15
- import type { ExchangeAdapter } from "../exchanges/interface.js";
15
+ import type { ExchangeAdapter } from "../exchanges/index.js";
16
16
  export interface SplitOrderParams {
17
17
  symbol: string;
18
18
  side: "buy" | "sell";
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "../exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  export interface TrailingStopParams {
3
3
  symbol: string;
4
4
  trailPct: number;
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "../exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  export interface TWAPParams {
3
3
  symbol: string;
4
4
  side: "buy" | "sell";
@@ -1,4 +1,4 @@
1
- import type { ExchangeAdapter } from "./exchanges/interface.js";
1
+ import type { ExchangeAdapter } from "./exchanges/index.js";
2
2
  export interface CheckResult {
3
3
  check: "symbol_valid" | "balance_sufficient" | "price_fresh" | "liquidity_ok" | "risk_limits" | "position_exists";
4
4
  passed: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -46,6 +46,7 @@
46
46
  "@modelcontextprotocol/sdk": "^1.27.1",
47
47
  "@msgpack/msgpack": "^3.1.3",
48
48
  "@solana/web3.js": "^1.98.0",
49
+ "@types/react": "^19.2.14",
49
50
  "bs58": "^6.0.0",
50
51
  "chalk": "^5.4.1",
51
52
  "cli-table3": "^0.6.5",
@@ -53,7 +54,11 @@
53
54
  "dotenv": "^16.5.0",
54
55
  "ethers": "^6.13.2",
55
56
  "hyperliquid": "^1.7.7",
57
+ "ink": "^6.8.0",
58
+ "ink-spinner": "^5.0.0",
59
+ "ink-table": "^3.1.0",
56
60
  "lighter-ts-sdk": "^1.0.10",
61
+ "react": "^19.2.4",
57
62
  "tweetnacl": "^1.0.3",
58
63
  "ws": "^8.19.0",
59
64
  "yaml": "^2.8.2",
@@ -62,6 +67,7 @@
62
67
  "devDependencies": {
63
68
  "@types/node": "^25.3.3",
64
69
  "@types/ws": "^8.18.1",
70
+ "ts-morph": "^27.0.2",
65
71
  "tsup": "^8.4.0",
66
72
  "tsx": "^4.21.0",
67
73
  "typescript": "^5.7.0",
@@ -98,6 +98,26 @@ perp --json arb close ETH # sells spot + buys back perp
98
98
 
99
99
  Spot exchanges: HL, LT. Pacifica is perp-only.
100
100
 
101
+ ## Bot Engine (19 Strategies)
102
+
103
+ ```bash
104
+ perp bot list-strategies # list all 19 strategies
105
+ perp bot run <strategy> [symbol] # run any registered strategy
106
+ perp bot run funding-auto # multi-exchange funding arb
107
+ perp bot apex [symbol] # APEX orchestrator (Radar+Pulse+Guard)
108
+ perp bot reflect # performance analysis (win rate, fees, PnL)
109
+ perp bot preset-list # list strategy presets
110
+ perp bot preset <name> [symbol] # run from preset
111
+ perp bot start <config.yaml> # start from YAML/JSON config
112
+ perp bot quick-grid <symbol> # quick grid bot
113
+ perp bot quick-dca <symbol> <side> <amount> <interval>
114
+ perp bot quick-arb # quick funding arb
115
+ ```
116
+
117
+ **Strategies:** simple-mm, engine-mm, avellaneda-mm, regime-mm, grid-mm, liquidation-mm, momentum-breakout, mean-reversion, aggressive-taker, funding-arb, funding-auto, basis-arb, hedge-agent, rfq-agent, claude-agent, grid, dca, twap, apex
118
+
119
+ **Flags:** `--dry-run` (simulate without executing), `--headless` (no TUI, log only)
120
+
101
121
  ## Core Workflow: Single Trade
102
122
 
103
123
  ```bash