perp-cli 0.6.3 → 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.
- package/README.md +7 -2
- package/dist/api/public/hyperliquid.d.ts +2 -2
- package/dist/api/public/hyperliquid.js +0 -10
- package/dist/api/public/lighter.d.ts +2 -1
- package/dist/api/public/pacifica.d.ts +2 -1
- package/dist/arb/index.d.ts +7 -4
- package/dist/arb/index.js +5 -4
- package/dist/arb/sizing.d.ts +1 -1
- package/dist/arb/state.d.ts +1 -1
- package/dist/arb/state.js +4 -3
- package/dist/arb/utils.d.ts +0 -10
- package/dist/arb/utils.js +0 -42
- package/dist/bot/apex/guard.d.ts +26 -0
- package/dist/bot/apex/guard.js +86 -0
- package/dist/bot/apex/index.d.ts +6 -0
- package/dist/bot/apex/index.js +4 -0
- package/dist/bot/apex/pulse.d.ts +22 -0
- package/dist/bot/apex/pulse.js +107 -0
- package/dist/bot/apex/radar.d.ts +17 -0
- package/dist/bot/apex/radar.js +150 -0
- package/dist/bot/conditions.d.ts +1 -1
- package/dist/bot/config.d.ts +5 -1
- package/dist/bot/config.js +1 -1
- package/dist/bot/engine.d.ts +23 -2
- package/dist/bot/engine.js +296 -351
- package/dist/bot/index.d.ts +16 -0
- package/dist/bot/index.js +10 -0
- package/dist/bot/indicators.d.ts +29 -0
- package/dist/bot/indicators.js +144 -0
- package/dist/bot/reflect.d.ts +34 -0
- package/dist/bot/reflect.js +155 -0
- package/dist/bot/strategies/aggressive-taker.d.ts +36 -0
- package/dist/bot/strategies/aggressive-taker.js +125 -0
- package/dist/bot/strategies/apex-strategy.d.ts +36 -0
- package/dist/bot/strategies/apex-strategy.js +185 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +27 -0
- package/dist/bot/strategies/avellaneda-mm.js +111 -0
- package/dist/bot/strategies/basis-arb.d.ts +27 -0
- package/dist/bot/strategies/basis-arb.js +97 -0
- package/dist/bot/strategies/claude-agent.d.ts +31 -0
- package/dist/bot/strategies/claude-agent.js +155 -0
- package/dist/bot/strategies/dca-strategy.d.ts +25 -0
- package/dist/bot/strategies/dca-strategy.js +59 -0
- package/dist/bot/strategies/engine-mm.d.ts +29 -0
- package/dist/bot/strategies/engine-mm.js +141 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-strategy.js +168 -0
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +222 -0
- package/dist/bot/strategies/funding-auto.d.ts +1 -0
- package/dist/bot/strategies/funding-auto.js +572 -0
- package/dist/bot/strategies/grid-mm.d.ts +25 -0
- package/dist/bot/strategies/grid-mm.js +99 -0
- package/dist/bot/strategies/grid-strategy.d.ts +39 -0
- package/dist/bot/strategies/grid-strategy.js +176 -0
- package/dist/bot/strategies/hedge-agent.d.ts +25 -0
- package/dist/bot/strategies/hedge-agent.js +79 -0
- package/dist/bot/strategies/liquidation-mm.d.ts +25 -0
- package/dist/bot/strategies/liquidation-mm.js +108 -0
- package/dist/bot/strategies/mean-reversion.d.ts +35 -0
- package/dist/bot/strategies/mean-reversion.js +118 -0
- package/dist/bot/strategies/momentum-breakout.d.ts +36 -0
- package/dist/bot/strategies/momentum-breakout.js +159 -0
- package/dist/bot/strategies/regime-mm.d.ts +25 -0
- package/dist/bot/strategies/regime-mm.js +111 -0
- package/dist/bot/strategies/rfq-agent.d.ts +25 -0
- package/dist/bot/strategies/rfq-agent.js +71 -0
- package/dist/bot/strategies/simple-mm.d.ts +25 -0
- package/dist/bot/strategies/simple-mm.js +76 -0
- package/dist/bot/strategy-registry.d.ts +9 -0
- package/dist/bot/strategy-registry.js +14 -0
- package/dist/bot/strategy-types.d.ts +64 -0
- package/dist/bot/strategy-types.js +5 -0
- package/dist/bot/trade-journal.d.ts +27 -0
- package/dist/bot/trade-journal.js +53 -0
- package/dist/bot/tui/BotDashboard.d.ts +49 -0
- package/dist/bot/tui/BotDashboard.js +93 -0
- package/dist/bot/tui/index.d.ts +6 -0
- package/dist/bot/tui/index.js +8 -0
- package/dist/bridge-engine.d.ts +0 -1
- package/dist/bridge-engine.js +0 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +0 -1
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +106 -75
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/arb/index.d.ts +1 -1
- package/dist/commands/arb/index.js +1 -1
- package/dist/commands/arb-auto.d.ts +1 -1
- package/dist/commands/arb-auto.js +1 -1
- package/dist/commands/arb.d.ts +1 -1
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +198 -14
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +33 -20
- package/dist/commands/history.d.ts +1 -1
- package/dist/commands/history.js +5 -4
- package/dist/commands/manage.d.ts +2 -3
- package/dist/commands/manage.js +11 -18
- package/dist/commands/market.d.ts +1 -1
- package/dist/commands/market.js +7 -7
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +8 -9
- package/dist/commands/risk.d.ts +1 -1
- package/dist/commands/settings.d.ts +1 -1
- package/dist/commands/settings.js +3 -1
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +39 -52
- package/dist/commands/wallet.d.ts +0 -2
- package/dist/commands/wallet.js +0 -5
- package/dist/constants.d.ts +0 -3
- package/dist/constants.js +0 -4
- package/dist/cross-chain-margin.d.ts +1 -1
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/dashboard/server.d.ts +1 -1
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ws-feeds.d.ts +1 -1
- package/dist/event-stream.d.ts +1 -1
- package/dist/exchanges/aster.d.ts +58 -0
- package/dist/exchanges/aster.js +409 -0
- package/dist/exchanges/capabilities.d.ts +74 -0
- package/dist/exchanges/capabilities.js +44 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +8 -2
- package/dist/exchanges/hyperliquid.js +21 -6
- package/dist/exchanges/index.d.ts +11 -0
- package/dist/exchanges/index.js +10 -0
- package/dist/exchanges/interface.d.ts +3 -0
- package/dist/exchanges/lighter-spot.d.ts +13 -0
- package/dist/exchanges/lighter-spot.js +98 -26
- package/dist/exchanges/lighter.d.ts +9 -3
- package/dist/exchanges/lighter.js +18 -8
- package/dist/exchanges/nado.d.ts +52 -0
- package/dist/exchanges/nado.js +223 -0
- package/dist/exchanges/pacifica.d.ts +7 -1
- package/dist/exchanges/pacifica.js +17 -2
- package/dist/exchanges/registry.d.ts +13 -0
- package/dist/exchanges/registry.js +37 -0
- package/dist/exchanges/spot-interface.d.ts +9 -0
- package/dist/funding/rates.js +1 -1
- package/dist/index.js +2 -6
- package/dist/jobs.js +1 -1
- package/dist/liquidity.d.ts +1 -2
- package/dist/liquidity.js +12 -5
- package/dist/mcp-server.js +4 -2
- package/dist/pacifica/constants.d.ts +0 -3
- package/dist/pacifica/constants.js +0 -4
- package/dist/pacifica/types/account.d.ts +0 -8
- package/dist/pacifica/types/lake.d.ts +0 -6
- package/dist/pacifica/types/position.d.ts +0 -12
- package/dist/pacifica/types/ws.d.ts +0 -24
- package/dist/pacifica/types/ws.js +1 -41
- package/dist/plan-executor.d.ts +1 -1
- package/dist/rebalance.d.ts +1 -7
- package/dist/rebalance.js +0 -6
- package/dist/risk.d.ts +1 -1
- package/dist/risk.js +3 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/strategies/dca.d.ts +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +193 -181
- package/dist/strategies/grid.d.ts +1 -1
- package/dist/strategies/grid.js +1 -1
- package/dist/strategies/index.d.ts +12 -0
- package/dist/strategies/index.js +7 -0
- package/dist/strategies/split-order.d.ts +1 -1
- package/dist/strategies/trailing-stop.d.ts +1 -1
- package/dist/strategies/twap.d.ts +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/package.json +7 -1
- package/skills/perp-cli/SKILL.md +20 -0
- 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";
|
package/dist/smart-order.d.ts
CHANGED
|
@@ -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/
|
|
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;
|
package/dist/strategies/dca.d.ts
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rateMap.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
log(`[ARB]
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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) {
|
package/dist/strategies/grid.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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.
|
|
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",
|
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -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
|