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.
- 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 +94 -91
- 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/bot/engine.js
CHANGED
|
@@ -1,67 +1,167 @@
|
|
|
1
1
|
import { getMarketSnapshot, evaluateAllConditions } from "./conditions.js";
|
|
2
2
|
import { updateJobState } from "../jobs.js";
|
|
3
|
-
import {
|
|
4
|
-
import { computeMatchedSize, reconcileArbFills } from "../arb-sizing.js";
|
|
3
|
+
import { getStrategy } from "./strategy-registry.js";
|
|
5
4
|
import chalk from "chalk";
|
|
6
|
-
|
|
5
|
+
// Auto-register built-in strategies
|
|
6
|
+
import "./strategies/grid-strategy.js";
|
|
7
|
+
import "./strategies/dca-strategy.js";
|
|
8
|
+
import "./strategies/funding-arb-strategy.js";
|
|
9
|
+
import "./strategies/simple-mm.js";
|
|
10
|
+
import "./strategies/engine-mm.js";
|
|
11
|
+
import "./strategies/avellaneda-mm.js";
|
|
12
|
+
import "./strategies/momentum-breakout.js";
|
|
13
|
+
import "./strategies/mean-reversion.js";
|
|
14
|
+
import "./strategies/aggressive-taker.js";
|
|
15
|
+
import "./strategies/funding-arb-v2.js";
|
|
16
|
+
import "./strategies/basis-arb.js";
|
|
17
|
+
import "./strategies/regime-mm.js";
|
|
18
|
+
import "./strategies/grid-mm.js";
|
|
19
|
+
import "./strategies/liquidation-mm.js";
|
|
20
|
+
import "./strategies/hedge-agent.js";
|
|
21
|
+
import "./strategies/rfq-agent.js";
|
|
22
|
+
import "./strategies/claude-agent.js";
|
|
23
|
+
import "./strategies/apex-strategy.js";
|
|
24
|
+
import "./strategies/funding-auto.js";
|
|
25
|
+
// ── TUI state emitter ──
|
|
26
|
+
class TuiEmitter {
|
|
27
|
+
stateListeners = [];
|
|
28
|
+
logListeners = [];
|
|
29
|
+
subscribe(onState, onLog) {
|
|
30
|
+
this.stateListeners.push(onState);
|
|
31
|
+
this.logListeners.push(onLog);
|
|
32
|
+
return () => {
|
|
33
|
+
this.stateListeners = this.stateListeners.filter((l) => l !== onState);
|
|
34
|
+
this.logListeners = this.logListeners.filter((l) => l !== onLog);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
emitState(s) {
|
|
38
|
+
for (const l of this.stateListeners)
|
|
39
|
+
l(s);
|
|
40
|
+
}
|
|
41
|
+
emitLog(msg) {
|
|
42
|
+
const entry = { time: new Date().toLocaleTimeString(), message: msg };
|
|
43
|
+
for (const l of this.logListeners)
|
|
44
|
+
l(entry);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runBot(adapter, config, jobId, log = defaultLog, extraAdapters, // for funding-arb (multi-exchange)
|
|
48
|
+
mode = "headless") {
|
|
7
49
|
const state = {
|
|
8
50
|
phase: "monitoring",
|
|
9
51
|
startTime: Date.now(),
|
|
10
52
|
equity: 0,
|
|
11
53
|
peakEquity: 0,
|
|
12
54
|
dailyPnl: 0,
|
|
55
|
+
dailyStartEquity: 0,
|
|
56
|
+
dailyStartDate: new Date().toISOString().slice(0, 10),
|
|
13
57
|
fills: 0,
|
|
14
58
|
totalPnl: 0,
|
|
15
59
|
rebalanceCount: 0,
|
|
16
60
|
lastRebalance: 0,
|
|
17
61
|
strategyActive: false,
|
|
18
|
-
gridOrders: new Map(),
|
|
19
|
-
gridUpper: 0,
|
|
20
|
-
gridLower: 0,
|
|
21
|
-
dcaOrdersPlaced: 0,
|
|
22
|
-
dcaLastOrder: 0,
|
|
23
|
-
arbRunning: false,
|
|
24
|
-
arbPositions: 0,
|
|
25
62
|
};
|
|
63
|
+
// Resolve strategy from registry
|
|
64
|
+
const strategyFactory = getStrategy(config.strategy.type);
|
|
65
|
+
if (!strategyFactory)
|
|
66
|
+
throw new Error(`Unknown strategy: ${config.strategy.type}`);
|
|
67
|
+
const strategy = strategyFactory(config.strategy);
|
|
68
|
+
const strategyCtx = {
|
|
69
|
+
adapter,
|
|
70
|
+
symbol: config.symbol,
|
|
71
|
+
config: config.strategy,
|
|
72
|
+
state: new Map(),
|
|
73
|
+
tick: 0,
|
|
74
|
+
log,
|
|
75
|
+
};
|
|
76
|
+
// Pass extra adapters to strategy context for funding-arb
|
|
77
|
+
if (extraAdapters) {
|
|
78
|
+
strategyCtx.state.set("extraAdapters", extraAdapters);
|
|
79
|
+
}
|
|
80
|
+
const isDryRun = false; // TODO: wire up from config when dry-run mode is added
|
|
26
81
|
let running = true;
|
|
82
|
+
// ── TUI / JSON mode setup ──
|
|
83
|
+
const tuiEmitter = new TuiEmitter();
|
|
84
|
+
let tuiUnmount;
|
|
85
|
+
let paused = false;
|
|
86
|
+
// Wrap log for TUI/JSON modes
|
|
87
|
+
const effectiveLog = mode === "json"
|
|
88
|
+
? () => { } // JSON mode suppresses console log; NDJSON emitted per tick
|
|
89
|
+
: mode === "tui"
|
|
90
|
+
? (msg) => tuiEmitter.emitLog(msg)
|
|
91
|
+
: log;
|
|
92
|
+
// Update strategy context to use effective log
|
|
93
|
+
strategyCtx.log = effectiveLog;
|
|
94
|
+
if (mode === "tui") {
|
|
95
|
+
try {
|
|
96
|
+
const { startDashboard } = await import("./tui/index.js");
|
|
97
|
+
const initialTuiState = buildTuiState(state, config, { price: 0, volatility24h: 0, fundingRate: 0, volume24h: 0 }, [], [], strategyCtx);
|
|
98
|
+
const result = startDashboard({
|
|
99
|
+
initialState: initialTuiState,
|
|
100
|
+
onQuit: () => { running = false; state.phase = "stopped"; },
|
|
101
|
+
onPause: () => { paused = !paused; },
|
|
102
|
+
subscribe: (onState, onLog) => tuiEmitter.subscribe(onState, onLog),
|
|
103
|
+
});
|
|
104
|
+
tuiUnmount = result.unmount;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Fallback to headless if TUI fails (e.g., non-TTY)
|
|
108
|
+
// mode stays as-is but we just use the original log
|
|
109
|
+
}
|
|
110
|
+
}
|
|
27
111
|
const shutdown = () => { running = false; state.phase = "stopped"; };
|
|
28
112
|
process.on("SIGINT", shutdown);
|
|
29
113
|
process.on("SIGTERM", shutdown);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
114
|
+
if (mode !== "tui") {
|
|
115
|
+
// Header (headless / json modes print header to console)
|
|
116
|
+
log(chalk.cyan.bold(`\n ╔═══════════════════════════════════════╗`));
|
|
117
|
+
log(chalk.cyan.bold(` ║ ${config.name.padEnd(37)}║`));
|
|
118
|
+
log(chalk.cyan.bold(` ╚═══════════════════════════════════════╝\n`));
|
|
119
|
+
log(` Exchange: ${chalk.white(config.exchange)}`);
|
|
120
|
+
log(` Symbol: ${chalk.white(config.symbol)}`);
|
|
121
|
+
log(` Strategy: ${chalk.white(config.strategy.type)}`);
|
|
122
|
+
log(` Risk: max drawdown $${config.risk.max_drawdown} | max daily loss $${config.risk.max_daily_loss}`);
|
|
123
|
+
log(` Entry: ${config.entry_conditions.map(c => c.type).join(" + ")}`);
|
|
124
|
+
log(` Exit: ${config.exit_conditions.map(c => c.type).join(" | ") || "manual"}`);
|
|
125
|
+
log("");
|
|
126
|
+
}
|
|
41
127
|
// Get initial equity
|
|
42
128
|
try {
|
|
43
129
|
const bal = await adapter.getBalance();
|
|
44
130
|
state.equity = parseFloat(bal.equity);
|
|
45
131
|
state.peakEquity = state.equity;
|
|
46
|
-
|
|
132
|
+
state.dailyStartEquity = state.equity;
|
|
133
|
+
effectiveLog(` Starting equity: $${state.equity.toFixed(2)}`);
|
|
47
134
|
}
|
|
48
135
|
catch {
|
|
49
|
-
|
|
136
|
+
effectiveLog(chalk.yellow(` Could not fetch initial balance`));
|
|
137
|
+
}
|
|
138
|
+
if (mode !== "tui") {
|
|
139
|
+
log(chalk.gray(`\n Monitoring conditions... (Ctrl+C to stop)\n`));
|
|
50
140
|
}
|
|
51
|
-
log(chalk.gray(`\n Monitoring conditions... (Ctrl+C to stop)\n`));
|
|
52
141
|
try {
|
|
53
142
|
while (running) {
|
|
143
|
+
// Honor pause toggle from TUI
|
|
144
|
+
if (paused) {
|
|
145
|
+
await sleep(500);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
54
148
|
const loopStart = Date.now();
|
|
55
149
|
try {
|
|
56
|
-
// Get market data
|
|
57
|
-
const snapshot = await
|
|
150
|
+
// Get market data (enriched for strategy use)
|
|
151
|
+
const snapshot = await getEnrichedSnapshot(adapter, config.symbol);
|
|
58
152
|
// Update equity
|
|
59
153
|
try {
|
|
60
154
|
const bal = await adapter.getBalance();
|
|
61
155
|
state.equity = parseFloat(bal.equity);
|
|
62
156
|
if (state.equity > state.peakEquity)
|
|
63
157
|
state.peakEquity = state.equity;
|
|
64
|
-
|
|
158
|
+
// Reset daily baseline at day boundary
|
|
159
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
160
|
+
if (today !== state.dailyStartDate) {
|
|
161
|
+
state.dailyStartEquity = state.equity;
|
|
162
|
+
state.dailyStartDate = today;
|
|
163
|
+
}
|
|
164
|
+
state.dailyPnl = state.equity - state.dailyStartEquity;
|
|
65
165
|
}
|
|
66
166
|
catch { /* non-critical */ }
|
|
67
167
|
const context = {
|
|
@@ -74,24 +174,30 @@ export async function runBot(adapter, config, jobId, log = defaultLog, extraAdap
|
|
|
74
174
|
if (state.phase === "monitoring") {
|
|
75
175
|
const shouldEnter = evaluateAllConditions(config.entry_conditions, snapshot, context, "all");
|
|
76
176
|
if (shouldEnter) {
|
|
77
|
-
|
|
177
|
+
effectiveLog(` Entry conditions met @ $${snapshot.price.toFixed(2)}`);
|
|
78
178
|
state.phase = "entering";
|
|
79
179
|
}
|
|
80
180
|
else {
|
|
81
|
-
logStatus(
|
|
181
|
+
logStatus(effectiveLog, state, snapshot, "waiting");
|
|
82
182
|
}
|
|
83
183
|
}
|
|
84
184
|
// ── Phase: Entering (start strategy) ──
|
|
85
185
|
if (state.phase === "entering") {
|
|
86
186
|
try {
|
|
87
|
-
await
|
|
187
|
+
await strategy.init(strategyCtx, snapshot);
|
|
188
|
+
// Execute any initial actions (e.g., grid placement on first tick)
|
|
189
|
+
strategyCtx.tick = 0;
|
|
190
|
+
const initActions = await strategy.onTick(strategyCtx, snapshot);
|
|
191
|
+
await executeActions(adapter, config.symbol, initActions, effectiveLog, isDryRun);
|
|
192
|
+
state.fills += countFills(initActions);
|
|
193
|
+
strategyCtx.tick++;
|
|
88
194
|
state.phase = "running";
|
|
89
195
|
state.strategyActive = true;
|
|
90
|
-
|
|
196
|
+
effectiveLog(` Strategy started`);
|
|
91
197
|
}
|
|
92
198
|
catch (err) {
|
|
93
199
|
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
-
|
|
200
|
+
effectiveLog(` Strategy start failed: ${msg}`);
|
|
95
201
|
state.phase = "monitoring"; // retry
|
|
96
202
|
}
|
|
97
203
|
}
|
|
@@ -102,37 +208,54 @@ export async function runBot(adapter, config, jobId, log = defaultLog, extraAdap
|
|
|
102
208
|
&& evaluateAllConditions(config.exit_conditions, snapshot, context, "any");
|
|
103
209
|
// Check risk limits
|
|
104
210
|
const drawdown = state.peakEquity - state.equity;
|
|
105
|
-
const
|
|
211
|
+
const dailyLossBreached = state.dailyPnl < -config.risk.max_daily_loss;
|
|
212
|
+
const riskBreached = drawdown > config.risk.max_drawdown || dailyLossBreached;
|
|
106
213
|
if (shouldExit || riskBreached) {
|
|
107
|
-
const reason =
|
|
108
|
-
? `
|
|
109
|
-
:
|
|
110
|
-
|
|
214
|
+
const reason = dailyLossBreached
|
|
215
|
+
? `daily loss $${Math.abs(state.dailyPnl).toFixed(2)} > limit $${config.risk.max_daily_loss}`
|
|
216
|
+
: drawdown > config.risk.max_drawdown
|
|
217
|
+
? `drawdown $${drawdown.toFixed(2)} > limit $${config.risk.max_drawdown}`
|
|
218
|
+
: "exit condition met";
|
|
219
|
+
effectiveLog(` Exiting: ${reason}`);
|
|
111
220
|
state.phase = "exiting";
|
|
112
221
|
}
|
|
113
222
|
else {
|
|
114
223
|
// Manage running strategy
|
|
115
|
-
await
|
|
116
|
-
|
|
224
|
+
const actions = await strategy.onTick(strategyCtx, snapshot);
|
|
225
|
+
await executeActions(adapter, config.symbol, actions, effectiveLog, isDryRun);
|
|
226
|
+
state.fills += countFills(actions);
|
|
227
|
+
strategyCtx.tick++;
|
|
228
|
+
logStatus(effectiveLog, state, snapshot, "running");
|
|
117
229
|
}
|
|
118
230
|
}
|
|
119
231
|
// ── Phase: Exiting (close everything) ──
|
|
120
232
|
if (state.phase === "exiting") {
|
|
121
|
-
|
|
233
|
+
const stopActions = await strategy.onStop(strategyCtx);
|
|
234
|
+
await executeActions(adapter, config.symbol, stopActions, effectiveLog, isDryRun);
|
|
122
235
|
state.strategyActive = false;
|
|
123
236
|
if (config.risk.pause_after_loss_sec > 0 && state.dailyPnl < 0) {
|
|
124
|
-
|
|
237
|
+
effectiveLog(` Pausing ${config.risk.pause_after_loss_sec}s after loss`);
|
|
125
238
|
state.phase = "paused";
|
|
126
239
|
await sleep(config.risk.pause_after_loss_sec * 1000);
|
|
127
240
|
state.phase = "monitoring";
|
|
128
|
-
|
|
241
|
+
effectiveLog(` Resuming monitoring...`);
|
|
129
242
|
}
|
|
130
243
|
else {
|
|
131
244
|
// If exit due to risk breach, stop completely
|
|
132
|
-
|
|
245
|
+
effectiveLog(` Bot stopped.`);
|
|
133
246
|
break;
|
|
134
247
|
}
|
|
135
248
|
}
|
|
249
|
+
// ── Emit state update for TUI / JSON ──
|
|
250
|
+
const positions = await fetchPositions(adapter, config.symbol);
|
|
251
|
+
const openOrders = await fetchOpenOrders(adapter, config.symbol);
|
|
252
|
+
const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx);
|
|
253
|
+
if (mode === "tui") {
|
|
254
|
+
tuiEmitter.emitState(tuiState);
|
|
255
|
+
}
|
|
256
|
+
else if (mode === "json") {
|
|
257
|
+
console.log(JSON.stringify(tuiState));
|
|
258
|
+
}
|
|
136
259
|
// Update job state
|
|
137
260
|
if (jobId) {
|
|
138
261
|
updateJobState(jobId, {
|
|
@@ -150,7 +273,7 @@ export async function runBot(adapter, config, jobId, log = defaultLog, extraAdap
|
|
|
150
273
|
}
|
|
151
274
|
catch (err) {
|
|
152
275
|
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
-
|
|
276
|
+
effectiveLog(` Loop error: ${msg}`);
|
|
154
277
|
}
|
|
155
278
|
// Wait for next interval
|
|
156
279
|
const elapsed = Date.now() - loopStart;
|
|
@@ -164,334 +287,83 @@ export async function runBot(adapter, config, jobId, log = defaultLog, extraAdap
|
|
|
164
287
|
process.removeListener("SIGTERM", shutdown);
|
|
165
288
|
// Cleanup: cancel any remaining orders
|
|
166
289
|
if (state.strategyActive) {
|
|
167
|
-
|
|
290
|
+
effectiveLog(` Cleaning up orders...`);
|
|
168
291
|
try {
|
|
169
292
|
await adapter.cancelAllOrders(config.symbol);
|
|
170
293
|
}
|
|
171
294
|
catch { /* best effort */ }
|
|
172
295
|
}
|
|
296
|
+
// Unmount TUI if active
|
|
297
|
+
if (tuiUnmount) {
|
|
298
|
+
tuiUnmount();
|
|
299
|
+
}
|
|
173
300
|
}
|
|
174
301
|
const runtime = Math.floor((Date.now() - state.startTime) / 1000);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
302
|
+
if (mode === "json") {
|
|
303
|
+
console.log(JSON.stringify({ event: "done", fills: state.fills, totalPnl: state.totalPnl, runtime, equity: state.equity, peakEquity: state.peakEquity }));
|
|
304
|
+
}
|
|
305
|
+
else if (mode !== "tui") {
|
|
306
|
+
log(chalk.cyan.bold(`\n Bot "${config.name}" finished`));
|
|
307
|
+
log(` Runtime: ${formatDuration(runtime)} | Fills: ${state.fills} | PnL: $${state.totalPnl.toFixed(2)}`);
|
|
308
|
+
log(` Equity: $${state.equity.toFixed(2)} (peak: $${state.peakEquity.toFixed(2)})\n`);
|
|
309
|
+
}
|
|
178
310
|
if (jobId) {
|
|
179
311
|
updateJobState(jobId, { status: "done" });
|
|
180
312
|
}
|
|
181
313
|
return { fills: state.fills, totalPnl: state.totalPnl, runtime };
|
|
182
314
|
}
|
|
183
|
-
// ──
|
|
184
|
-
async function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else if (strat.type === "dca") {
|
|
190
|
-
state.dcaOrdersPlaced = 0;
|
|
191
|
-
state.dcaLastOrder = 0;
|
|
192
|
-
log(` [DCA] Ready: ${strat.amount} ${config.symbol} every ${strat.interval_sec}s`);
|
|
193
|
-
}
|
|
194
|
-
else if (strat.type === "funding-arb") {
|
|
195
|
-
state.arbRunning = true;
|
|
196
|
-
state.arbPositions = 0;
|
|
197
|
-
log(` [ARB] Funding arb ready | spread >= ${strat.min_spread}% | size $${strat.size_usd}`);
|
|
198
|
-
log(` [ARB] Exchanges: ${strat.exchanges.join(", ")}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async function manageStrategy(adapter, config, state, snapshot, log, extraAdapters) {
|
|
202
|
-
const strat = config.strategy;
|
|
203
|
-
if (strat.type === "grid") {
|
|
204
|
-
await manageGrid(adapter, config.symbol, strat, state, snapshot, log);
|
|
205
|
-
}
|
|
206
|
-
else if (strat.type === "dca") {
|
|
207
|
-
await manageDCA(adapter, config.symbol, strat, state, snapshot, log);
|
|
208
|
-
}
|
|
209
|
-
else if (strat.type === "funding-arb") {
|
|
210
|
-
await manageFundingArb(adapter, config.symbol, strat, state, snapshot, log, extraAdapters);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
async function stopStrategy(adapter, config, state, log) {
|
|
214
|
-
log(` Cancelling all orders...`);
|
|
215
|
-
try {
|
|
216
|
-
await adapter.cancelAllOrders(config.symbol);
|
|
217
|
-
}
|
|
218
|
-
catch { /* best effort */ }
|
|
219
|
-
state.gridOrders.clear();
|
|
220
|
-
}
|
|
221
|
-
// ── Grid strategy ──
|
|
222
|
-
async function startGrid(adapter, symbol, params, state, snapshot, log) {
|
|
223
|
-
// Determine price range
|
|
224
|
-
if (params.range_mode === "auto") {
|
|
225
|
-
const pct = params.range_pct ?? 3;
|
|
226
|
-
state.gridUpper = snapshot.price * (1 + pct / 100);
|
|
227
|
-
state.gridLower = snapshot.price * (1 - pct / 100);
|
|
228
|
-
log(` [GRID] Auto range: $${state.gridLower.toFixed(2)} - $${state.gridUpper.toFixed(2)} (±${pct}%)`);
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
if (!params.upper || !params.lower)
|
|
232
|
-
throw new Error("Fixed grid requires upper and lower");
|
|
233
|
-
state.gridUpper = params.upper;
|
|
234
|
-
state.gridLower = params.lower;
|
|
235
|
-
}
|
|
236
|
-
// Set leverage
|
|
237
|
-
if (params.leverage) {
|
|
238
|
-
try {
|
|
239
|
-
await adapter.setLeverage(symbol, params.leverage);
|
|
240
|
-
log(` [GRID] Leverage: ${params.leverage}x`);
|
|
241
|
-
}
|
|
242
|
-
catch { /* non-critical */ }
|
|
243
|
-
}
|
|
244
|
-
// Place grid orders
|
|
245
|
-
await placeGridOrders(adapter, symbol, params, state, snapshot.price, log);
|
|
246
|
-
}
|
|
247
|
-
async function placeGridOrders(adapter, symbol, params, state, currentPrice, log) {
|
|
248
|
-
const step = (state.gridUpper - state.gridLower) / (params.grids - 1);
|
|
249
|
-
const sizePerGrid = params.size / params.grids;
|
|
250
|
-
let placed = 0;
|
|
251
|
-
// Cancel existing orders first
|
|
252
|
-
if (state.gridOrders.size > 0) {
|
|
253
|
-
try {
|
|
254
|
-
await adapter.cancelAllOrders(symbol);
|
|
255
|
-
}
|
|
256
|
-
catch { /* */ }
|
|
257
|
-
state.gridOrders.clear();
|
|
258
|
-
}
|
|
259
|
-
for (let i = 0; i < params.grids; i++) {
|
|
260
|
-
const price = state.gridLower + step * i;
|
|
261
|
-
// Determine side based on current price
|
|
262
|
-
let side;
|
|
263
|
-
if (params.side === "long")
|
|
264
|
-
side = "buy";
|
|
265
|
-
else if (params.side === "short")
|
|
266
|
-
side = "sell";
|
|
267
|
-
else
|
|
268
|
-
side = price < currentPrice ? "buy" : "sell";
|
|
269
|
-
try {
|
|
270
|
-
const result = await adapter.limitOrder(symbol, side, price.toFixed(2), String(sizePerGrid));
|
|
271
|
-
const orderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
272
|
-
state.gridOrders.set(i, orderId);
|
|
273
|
-
placed++;
|
|
315
|
+
// ── Action executor ──
|
|
316
|
+
async function executeActions(adapter, symbol, actions, log, dryRun = false) {
|
|
317
|
+
for (const action of actions) {
|
|
318
|
+
if (dryRun) {
|
|
319
|
+
log(` [DRY-RUN] ${action.type}: ${JSON.stringify(action)}`);
|
|
320
|
+
continue;
|
|
274
321
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
log(` [GRID] Placed ${placed}/${params.grids} orders (step: $${step.toFixed(2)}, size: ${sizePerGrid.toFixed(6)})`);
|
|
280
|
-
}
|
|
281
|
-
async function manageGrid(adapter, symbol, params, state, snapshot, log) {
|
|
282
|
-
const step = (state.gridUpper - state.gridLower) / (params.grids - 1);
|
|
283
|
-
const sizePerGrid = params.size / params.grids;
|
|
284
|
-
// Check for fills
|
|
285
|
-
try {
|
|
286
|
-
const openOrders = await adapter.getOpenOrders();
|
|
287
|
-
const openIds = new Set(openOrders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase()).map(o => o.orderId));
|
|
288
|
-
let newFills = 0;
|
|
289
|
-
for (const [idx, orderId] of state.gridOrders.entries()) {
|
|
290
|
-
if (!openIds.has(orderId)) {
|
|
291
|
-
// Order filled — place opposite order
|
|
292
|
-
newFills++;
|
|
293
|
-
state.fills++;
|
|
294
|
-
state.totalPnl += step * sizePerGrid;
|
|
295
|
-
// Determine new side and price
|
|
296
|
-
const oldPrice = state.gridLower + step * idx;
|
|
297
|
-
const wasBuy = oldPrice < snapshot.price;
|
|
298
|
-
const newSide = wasBuy ? "sell" : "buy";
|
|
299
|
-
const newPrice = wasBuy ? oldPrice + step : oldPrice - step;
|
|
300
|
-
if (newPrice >= state.gridLower && newPrice <= state.gridUpper) {
|
|
301
|
-
try {
|
|
302
|
-
const result = await adapter.limitOrder(symbol, newSide, newPrice.toFixed(2), String(sizePerGrid));
|
|
303
|
-
const newOrderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
304
|
-
state.gridOrders.set(idx, newOrderId);
|
|
305
|
-
}
|
|
306
|
-
catch { /* skip */ }
|
|
322
|
+
switch (action.type) {
|
|
323
|
+
case "place_order":
|
|
324
|
+
if (action.orderType === "market") {
|
|
325
|
+
await adapter.marketOrder(symbol, action.side, action.size);
|
|
307
326
|
}
|
|
308
327
|
else {
|
|
309
|
-
|
|
328
|
+
await adapter.limitOrder(symbol, action.side, action.price, action.size, {
|
|
329
|
+
reduceOnly: action.reduceOnly,
|
|
330
|
+
tif: action.tif,
|
|
331
|
+
});
|
|
310
332
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
state.rebalanceCount++;
|
|
327
|
-
state.lastRebalance = Date.now();
|
|
328
|
-
log(chalk.yellow(` [GRID] Rebalance #${state.rebalanceCount}: price $${snapshot.price.toFixed(2)} outside range → new $${state.gridLower.toFixed(2)} - $${state.gridUpper.toFixed(2)}`));
|
|
329
|
-
await placeGridOrders(adapter, symbol, params, state, snapshot.price, log);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// ── DCA strategy ──
|
|
334
|
-
async function manageDCA(adapter, symbol, params, state, snapshot, log) {
|
|
335
|
-
// Check if it's time for next order
|
|
336
|
-
const timeSinceLast = (Date.now() - state.dcaLastOrder) / 1000;
|
|
337
|
-
if (state.dcaLastOrder > 0 && timeSinceLast < params.interval_sec)
|
|
338
|
-
return;
|
|
339
|
-
// Check order limit
|
|
340
|
-
if (params.total_orders > 0 && state.dcaOrdersPlaced >= params.total_orders)
|
|
341
|
-
return;
|
|
342
|
-
// Check price limit
|
|
343
|
-
if (params.price_limit) {
|
|
344
|
-
// For DCA, we assume buying — skip if price above limit
|
|
345
|
-
if (snapshot.price > params.price_limit) {
|
|
346
|
-
return;
|
|
333
|
+
break;
|
|
334
|
+
case "cancel_order":
|
|
335
|
+
await adapter.cancelOrder(symbol, action.orderId);
|
|
336
|
+
break;
|
|
337
|
+
case "cancel_all":
|
|
338
|
+
await adapter.cancelAllOrders(symbol);
|
|
339
|
+
break;
|
|
340
|
+
case "edit_order":
|
|
341
|
+
await adapter.editOrder(symbol, action.orderId, action.price, action.size);
|
|
342
|
+
break;
|
|
343
|
+
case "set_leverage":
|
|
344
|
+
await adapter.setLeverage(symbol, action.leverage, action.marginMode);
|
|
345
|
+
break;
|
|
346
|
+
case "noop":
|
|
347
|
+
break;
|
|
347
348
|
}
|
|
348
349
|
}
|
|
349
|
-
// Place market order
|
|
350
|
-
try {
|
|
351
|
-
const side = "buy"; // DCA is typically buying
|
|
352
|
-
await adapter.marketOrder(symbol, side, String(params.amount));
|
|
353
|
-
state.dcaOrdersPlaced++;
|
|
354
|
-
state.dcaLastOrder = Date.now();
|
|
355
|
-
state.fills++;
|
|
356
|
-
const progress = params.total_orders > 0 ? ` (${state.dcaOrdersPlaced}/${params.total_orders})` : "";
|
|
357
|
-
log(chalk.green(` [DCA] Order #${state.dcaOrdersPlaced}${progress}: buy ${params.amount} ${symbol} @ $${snapshot.price.toFixed(2)}`));
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
361
|
-
log(chalk.red(` [DCA] Order failed: ${msg}`));
|
|
362
|
-
}
|
|
363
350
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return [];
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
async function manageFundingArb(adapter, symbol, params, state, _snapshot, log, extraAdapters) {
|
|
378
|
-
// Build adapter map from primary + extras
|
|
379
|
-
const adapters = new Map();
|
|
380
|
-
adapters.set(adapter.name.toLowerCase(), adapter);
|
|
381
|
-
if (extraAdapters) {
|
|
382
|
-
for (const [name, a] of extraAdapters)
|
|
383
|
-
adapters.set(name, a);
|
|
384
|
-
}
|
|
385
|
-
if (adapters.size < 2) {
|
|
386
|
-
log(chalk.yellow(` [ARB] Need 2+ exchanges, have ${adapters.size}. Skipping.`));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
// Fetch rates from all exchanges
|
|
390
|
-
const ratesByExchange = new Map();
|
|
391
|
-
for (const [name, a] of adapters) {
|
|
392
|
-
const rates = await fetchRatesFromAdapter(a, name);
|
|
393
|
-
const map = new Map();
|
|
394
|
-
for (const r of rates)
|
|
395
|
-
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
|
|
396
|
-
ratesByExchange.set(name, map);
|
|
397
|
-
}
|
|
398
|
-
// Find opportunities: largest spread between any two exchanges
|
|
399
|
-
const exchangeNames = [...ratesByExchange.keys()];
|
|
400
|
-
const opportunities = [];
|
|
401
|
-
// Collect all symbols
|
|
402
|
-
const allSymbols = new Set();
|
|
403
|
-
for (const [, map] of ratesByExchange) {
|
|
404
|
-
for (const sym of map.keys())
|
|
405
|
-
allSymbols.add(sym);
|
|
406
|
-
}
|
|
407
|
-
for (const sym of allSymbols) {
|
|
408
|
-
let minRate = Infinity, maxRate = -Infinity;
|
|
409
|
-
let minExchange = "", maxExchange = "";
|
|
410
|
-
for (const exName of exchangeNames) {
|
|
411
|
-
const rate = ratesByExchange.get(exName)?.get(sym)?.rate;
|
|
412
|
-
if (rate === undefined)
|
|
413
|
-
continue;
|
|
414
|
-
if (rate < minRate) {
|
|
415
|
-
minRate = rate;
|
|
416
|
-
minExchange = exName;
|
|
417
|
-
}
|
|
418
|
-
if (rate > maxRate) {
|
|
419
|
-
maxRate = rate;
|
|
420
|
-
maxExchange = exName;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (minExchange && maxExchange && minExchange !== maxExchange) {
|
|
424
|
-
const { computeAnnualSpread: cas } = await import("../funding.js");
|
|
425
|
-
const spread = cas(maxRate, maxExchange, minRate, minExchange);
|
|
426
|
-
if (spread >= params.min_spread) {
|
|
427
|
-
opportunities.push({
|
|
428
|
-
symbol: sym,
|
|
429
|
-
longExchange: minExchange, // long where rate is low (we receive funding)
|
|
430
|
-
shortExchange: maxExchange, // short where rate is high (we pay less or receive)
|
|
431
|
-
longRate: minRate,
|
|
432
|
-
shortRate: maxRate,
|
|
433
|
-
spread,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Sort by spread descending
|
|
439
|
-
opportunities.sort((a, b) => b.spread - a.spread);
|
|
440
|
-
if (opportunities.length > 0) {
|
|
441
|
-
const top = opportunities.slice(0, 3);
|
|
442
|
-
for (const opp of top) {
|
|
443
|
-
log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
|
|
444
|
-
}
|
|
445
|
-
// Auto-execute if under position limit
|
|
446
|
-
if (state.arbPositions < params.max_positions && opportunities.length > 0) {
|
|
447
|
-
const best = opportunities[0];
|
|
448
|
-
const longAdapter = adapters.get(best.longExchange);
|
|
449
|
-
const shortAdapter = adapters.get(best.shortExchange);
|
|
450
|
-
if (longAdapter && shortAdapter) {
|
|
451
|
-
// Calculate size from USD
|
|
452
|
-
const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
|
|
453
|
-
if (price > 0) {
|
|
454
|
-
// Liquidity check & size adjustment
|
|
455
|
-
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, params.size_usd, 0.5, (msg) => log(chalk.yellow(` ${msg}`)));
|
|
456
|
-
if (!liq.viable)
|
|
457
|
-
return;
|
|
458
|
-
// Compute matched size for both legs
|
|
459
|
-
const matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
|
|
460
|
-
if (!matched) {
|
|
461
|
-
log(chalk.yellow(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`));
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
log(chalk.cyan(` [ARB] Opening: ${matched.size} ${best.symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)`));
|
|
466
|
-
await Promise.all([
|
|
467
|
-
longAdapter.marketOrder(best.symbol, "buy", matched.size),
|
|
468
|
-
shortAdapter.marketOrder(best.symbol, "sell", matched.size),
|
|
469
|
-
]);
|
|
470
|
-
// Verify fills match, correct if needed
|
|
471
|
-
try {
|
|
472
|
-
const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => log(chalk.yellow(` ${msg}`)));
|
|
473
|
-
if (!recon.matched) {
|
|
474
|
-
log(chalk.red(` [ARB] WARNING: fills not matched after correction attempt`));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
catch { /* non-critical */ }
|
|
478
|
-
state.arbPositions++;
|
|
479
|
-
state.fills += 2;
|
|
480
|
-
log(chalk.green(` [ARB] Position opened! (${state.arbPositions}/${params.max_positions})`));
|
|
481
|
-
}
|
|
482
|
-
catch (err) {
|
|
483
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
484
|
-
log(chalk.red(` [ARB] Execution failed: ${msg}`));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
log(chalk.gray(` [ARB] No opportunities >= ${params.min_spread}% spread`));
|
|
492
|
-
}
|
|
351
|
+
// ── Enriched snapshot ──
|
|
352
|
+
async function getEnrichedSnapshot(adapter, symbol) {
|
|
353
|
+
const base = await getMarketSnapshot(adapter, symbol);
|
|
354
|
+
const [orderbook, klines, markets] = await Promise.all([
|
|
355
|
+
adapter.getOrderbook(symbol).catch(() => ({ bids: [], asks: [] })),
|
|
356
|
+
adapter.getKlines(symbol, "1h", Date.now() - 24 * 60 * 60 * 1000, Date.now()).catch(() => []),
|
|
357
|
+
adapter.getMarkets().catch(() => []),
|
|
358
|
+
]);
|
|
359
|
+
const market = markets.find(m => m.symbol.toUpperCase().includes(symbol.toUpperCase()));
|
|
360
|
+
return { ...base, klines, orderbook, openInterest: market?.openInterest ?? "0" };
|
|
493
361
|
}
|
|
494
362
|
// ── Utils ──
|
|
363
|
+
/** Count fill-producing actions (market orders and limit orders) */
|
|
364
|
+
function countFills(actions) {
|
|
365
|
+
return actions.filter(a => a.type === "place_order" && a.orderType === "market").length;
|
|
366
|
+
}
|
|
495
367
|
function logStatus(log, state, snapshot, phase) {
|
|
496
368
|
const runtime = formatDuration(Math.floor((Date.now() - state.startTime) / 1000));
|
|
497
369
|
const vol = snapshot.volatility24h.toFixed(1);
|
|
@@ -517,3 +389,76 @@ function formatDuration(sec) {
|
|
|
517
389
|
function sleep(ms) {
|
|
518
390
|
return new Promise(r => setTimeout(r, ms));
|
|
519
391
|
}
|
|
392
|
+
// ── TUI helpers ──
|
|
393
|
+
async function fetchPositions(adapter, symbol) {
|
|
394
|
+
try {
|
|
395
|
+
const positions = await adapter.getPositions();
|
|
396
|
+
return positions
|
|
397
|
+
.filter(p => p.symbol.toUpperCase().includes(symbol.toUpperCase()) && parseFloat(p.size) !== 0)
|
|
398
|
+
.map(p => ({
|
|
399
|
+
symbol: p.symbol,
|
|
400
|
+
side: p.side,
|
|
401
|
+
size: p.size,
|
|
402
|
+
entryPrice: p.entryPrice,
|
|
403
|
+
markPrice: p.markPrice,
|
|
404
|
+
unrealizedPnl: p.unrealizedPnl,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function fetchOpenOrders(adapter, symbol) {
|
|
412
|
+
try {
|
|
413
|
+
const orders = await adapter.getOpenOrders();
|
|
414
|
+
return orders
|
|
415
|
+
.filter(o => o.symbol.toUpperCase().includes(symbol.toUpperCase()))
|
|
416
|
+
.map(o => ({
|
|
417
|
+
orderId: o.orderId,
|
|
418
|
+
side: o.side,
|
|
419
|
+
price: o.price,
|
|
420
|
+
size: o.size,
|
|
421
|
+
type: o.type,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx) {
|
|
429
|
+
// Extract serializable strategy state from the Map
|
|
430
|
+
const strategyState = {};
|
|
431
|
+
for (const [k, v] of strategyCtx.state) {
|
|
432
|
+
// Skip non-serializable values (adapters, functions)
|
|
433
|
+
if (typeof v === "function" || (typeof v === "object" && v !== null && !(Array.isArray(v)) && v instanceof Map))
|
|
434
|
+
continue;
|
|
435
|
+
try {
|
|
436
|
+
JSON.stringify(v);
|
|
437
|
+
strategyState[k] = v;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// skip non-serializable
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
phase: state.phase,
|
|
445
|
+
equity: state.equity,
|
|
446
|
+
peakEquity: state.peakEquity,
|
|
447
|
+
dailyPnl: state.dailyPnl,
|
|
448
|
+
fills: state.fills,
|
|
449
|
+
totalPnl: state.totalPnl,
|
|
450
|
+
runtime: Math.floor((Date.now() - state.startTime) / 1000),
|
|
451
|
+
strategy: config.strategy.type,
|
|
452
|
+
symbol: config.symbol,
|
|
453
|
+
exchange: config.exchange,
|
|
454
|
+
price: snapshot.price,
|
|
455
|
+
volume24h: snapshot.volume24h ?? 0,
|
|
456
|
+
openInterest: "0",
|
|
457
|
+
fundingRate: snapshot.fundingRate,
|
|
458
|
+
volatility24h: snapshot.volatility24h,
|
|
459
|
+
positions,
|
|
460
|
+
openOrders,
|
|
461
|
+
strategyState,
|
|
462
|
+
tick: strategyCtx.tick,
|
|
463
|
+
};
|
|
464
|
+
}
|