@nexstone/rift-cli 0.1.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/LICENSE +201 -0
- package/bin/run.js +22 -0
- package/dist/commands/algo.d.ts +32 -0
- package/dist/commands/algo.js +719 -0
- package/dist/commands/audit.d.ts +13 -0
- package/dist/commands/audit.js +37 -0
- package/dist/commands/auth-status.d.ts +14 -0
- package/dist/commands/auth-status.js +118 -0
- package/dist/commands/auth.d.ts +14 -0
- package/dist/commands/auth.js +275 -0
- package/dist/commands/backtest.d.ts +26 -0
- package/dist/commands/backtest.js +283 -0
- package/dist/commands/collect/start.d.ts +11 -0
- package/dist/commands/collect/start.js +78 -0
- package/dist/commands/collect/status.d.ts +6 -0
- package/dist/commands/collect/status.js +60 -0
- package/dist/commands/compare.d.ts +16 -0
- package/dist/commands/compare.js +130 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +143 -0
- package/dist/commands/cost.d.ts +20 -0
- package/dist/commands/cost.js +104 -0
- package/dist/commands/cross-asset.d.ts +14 -0
- package/dist/commands/cross-asset.js +39 -0
- package/dist/commands/data/fetch.d.ts +15 -0
- package/dist/commands/data/fetch.js +82 -0
- package/dist/commands/data/list.d.ts +6 -0
- package/dist/commands/data/list.js +28 -0
- package/dist/commands/data-inventory.d.ts +9 -0
- package/dist/commands/data-inventory.js +24 -0
- package/dist/commands/deposit.d.ts +10 -0
- package/dist/commands/deposit.js +222 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +87 -0
- package/dist/commands/funding-browser.d.ts +12 -0
- package/dist/commands/funding-browser.js +33 -0
- package/dist/commands/guide.d.ts +6 -0
- package/dist/commands/guide.js +15 -0
- package/dist/commands/home.d.ts +23 -0
- package/dist/commands/home.js +210 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +122 -0
- package/dist/commands/install.d.ts +9 -0
- package/dist/commands/install.js +89 -0
- package/dist/commands/interactive.d.ts +17 -0
- package/dist/commands/interactive.js +179 -0
- package/dist/commands/lessons.d.ts +12 -0
- package/dist/commands/lessons.js +33 -0
- package/dist/commands/montecarlo.d.ts +19 -0
- package/dist/commands/montecarlo.js +168 -0
- package/dist/commands/more.d.ts +11 -0
- package/dist/commands/more.js +227 -0
- package/dist/commands/new.d.ts +14 -0
- package/dist/commands/new.js +306 -0
- package/dist/commands/pairs.d.ts +22 -0
- package/dist/commands/pairs.js +147 -0
- package/dist/commands/perp/close.d.ts +12 -0
- package/dist/commands/perp/close.js +57 -0
- package/dist/commands/perp/long.d.ts +14 -0
- package/dist/commands/perp/long.js +38 -0
- package/dist/commands/perp/short.d.ts +14 -0
- package/dist/commands/perp/short.js +27 -0
- package/dist/commands/perp/status.d.ts +9 -0
- package/dist/commands/perp/status.js +26 -0
- package/dist/commands/portfolio/alerts.d.ts +6 -0
- package/dist/commands/portfolio/alerts.js +47 -0
- package/dist/commands/portfolio/backtest.d.ts +12 -0
- package/dist/commands/portfolio/backtest.js +178 -0
- package/dist/commands/portfolio/create.d.ts +7 -0
- package/dist/commands/portfolio/create.js +195 -0
- package/dist/commands/portfolio/start.d.ts +9 -0
- package/dist/commands/portfolio/start.js +64 -0
- package/dist/commands/portfolio/status.d.ts +6 -0
- package/dist/commands/portfolio/status.js +128 -0
- package/dist/commands/portfolio/stop.d.ts +6 -0
- package/dist/commands/portfolio/stop.js +81 -0
- package/dist/commands/portfolio-backtest.d.ts +13 -0
- package/dist/commands/portfolio-backtest.js +37 -0
- package/dist/commands/portfolio-matrix.d.ts +12 -0
- package/dist/commands/portfolio-matrix.js +30 -0
- package/dist/commands/quick-test.d.ts +17 -0
- package/dist/commands/quick-test.js +45 -0
- package/dist/commands/research.d.ts +57 -0
- package/dist/commands/research.js +1976 -0
- package/dist/commands/scout.d.ts +14 -0
- package/dist/commands/scout.js +184 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.js +1176 -0
- package/dist/commands/setup/proxy.d.ts +10 -0
- package/dist/commands/setup/proxy.js +267 -0
- package/dist/commands/spot/buy.d.ts +14 -0
- package/dist/commands/spot/buy.js +38 -0
- package/dist/commands/spot/sell.d.ts +14 -0
- package/dist/commands/spot/sell.js +39 -0
- package/dist/commands/strategies/list.d.ts +6 -0
- package/dist/commands/strategies/list.js +34 -0
- package/dist/commands/sweep.d.ts +19 -0
- package/dist/commands/sweep.js +137 -0
- package/dist/commands/sync.d.ts +17 -0
- package/dist/commands/sync.js +54 -0
- package/dist/commands/test-trade.d.ts +6 -0
- package/dist/commands/test-trade.js +97 -0
- package/dist/commands/trade.d.ts +26 -0
- package/dist/commands/trade.js +274 -0
- package/dist/commands/transfer.d.ts +13 -0
- package/dist/commands/transfer.js +65 -0
- package/dist/commands/verify.d.ts +16 -0
- package/dist/commands/verify.js +38 -0
- package/dist/commands/walkforward.d.ts +20 -0
- package/dist/commands/walkforward.js +191 -0
- package/dist/commands/withdraw.d.ts +12 -0
- package/dist/commands/withdraw.js +55 -0
- package/dist/commands/workbench-create.d.ts +13 -0
- package/dist/commands/workbench-create.js +39 -0
- package/dist/lib/account-mode.d.ts +44 -0
- package/dist/lib/account-mode.js +96 -0
- package/dist/lib/analyzer.d.ts +4 -0
- package/dist/lib/analyzer.js +62 -0
- package/dist/lib/base-command.d.ts +35 -0
- package/dist/lib/base-command.js +49 -0
- package/dist/lib/credentials.d.ts +46 -0
- package/dist/lib/credentials.js +137 -0
- package/dist/lib/engine-passthrough.d.ts +28 -0
- package/dist/lib/engine-passthrough.js +60 -0
- package/dist/lib/fees.d.ts +52 -0
- package/dist/lib/fees.js +97 -0
- package/dist/lib/python-bridge.d.ts +24 -0
- package/dist/lib/python-bridge.js +182 -0
- package/dist/lib/setup-status.d.ts +32 -0
- package/dist/lib/setup-status.js +121 -0
- package/dist/lib/status-footer.d.ts +35 -0
- package/dist/lib/status-footer.js +101 -0
- package/dist/lib/tui.d.ts +130 -0
- package/dist/lib/tui.js +300 -0
- package/dist/lib/walletconnect.d.ts +70 -0
- package/dist/lib/walletconnect.js +407 -0
- package/package.json +49 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { Flags, Args } from '@oclif/core';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
6
|
+
import { spawnDaemon, runEngine, getAlgoSessionsDir, getAlgoPidsDir } from '../lib/python-bridge.js';
|
|
7
|
+
import { loadCredentials, hasFullSetup, maskAddress, getAccountAddress } from '../lib/credentials.js';
|
|
8
|
+
import { BUILDER_FEE_DISPLAY } from '../lib/fees.js';
|
|
9
|
+
import { green, red, yellow, cyan, bold, dim, greenBg, visLen, padEndVis, colorPnl, sparkline, proximityBar, fundingCountdown, maskAddress as maskAddr, createDashboardState, getLocalCountdown, } from '../lib/tui.js';
|
|
10
|
+
function ask(question) {
|
|
11
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function sessionKey(strategy, pair) {
|
|
17
|
+
const coin = pair.replace(/-PERP/i, '').toUpperCase();
|
|
18
|
+
return `${strategy}_${coin}`;
|
|
19
|
+
}
|
|
20
|
+
function isSessionRunning(key) {
|
|
21
|
+
const pidFile = path.join(getAlgoPidsDir(), `${key}.pid`);
|
|
22
|
+
if (!fs.existsSync(pidFile))
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
26
|
+
process.kill(pid, 0); // test if alive
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Stale PID — clean up
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(pidFile);
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readSessionSnapshot(key) {
|
|
39
|
+
const stateFile = path.join(getAlgoSessionsDir(), `${key}.json`);
|
|
40
|
+
if (!fs.existsSync(stateFile))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ─── COMMAND ───
|
|
50
|
+
export default class Algo extends GatedCommand {
|
|
51
|
+
static description = 'Algo trading — run automated strategies on Hyperliquid with real orders';
|
|
52
|
+
static examples = [
|
|
53
|
+
'$ rift algo trend_follow --pair BTC --tf 4h',
|
|
54
|
+
'$ rift algo status',
|
|
55
|
+
'$ rift algo stop',
|
|
56
|
+
];
|
|
57
|
+
static args = {
|
|
58
|
+
strategy: Args.string({ description: 'Strategy name, or "status"/"stop"', required: false }),
|
|
59
|
+
};
|
|
60
|
+
static flags = {
|
|
61
|
+
pair: Flags.string({ description: 'Ticker symbol (e.g. BTC, ETH, SOL)', default: 'BTC' }),
|
|
62
|
+
tf: Flags.string({ description: 'Timeframe' }),
|
|
63
|
+
equity: Flags.integer({ description: 'Starting equity (0 = auto)', default: 0 }),
|
|
64
|
+
all: Flags.boolean({ description: 'Stop all running sessions', default: false }),
|
|
65
|
+
'size-usd': Flags.string({
|
|
66
|
+
description: "Override the strategy's risk-model-derived position size with a fixed USD value " +
|
|
67
|
+
"(default 0 = no override). Useful for small-account setup verification — default " +
|
|
68
|
+
"strategy sizing is often below HL's $10 minimum on balances under ~$1000. Volume " +
|
|
69
|
+
"cap and per-strategy gate still apply.",
|
|
70
|
+
default: '',
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
sessionStart = 0;
|
|
74
|
+
dashboardActive = false;
|
|
75
|
+
tickTimer = null;
|
|
76
|
+
ds = createDashboardState();
|
|
77
|
+
viewerRunning = false;
|
|
78
|
+
static DASHBOARD_HEIGHT = 17;
|
|
79
|
+
clearDashboard() {
|
|
80
|
+
if (this.dashboardActive) {
|
|
81
|
+
const h = Algo.DASHBOARD_HEIGHT;
|
|
82
|
+
process.stdout.write(`\x1b[${h}A`);
|
|
83
|
+
for (let i = 0; i < h; i++)
|
|
84
|
+
process.stdout.write('\x1b[2K\n');
|
|
85
|
+
process.stdout.write(`\x1b[${h}A`);
|
|
86
|
+
this.dashboardActive = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async run() {
|
|
90
|
+
const { args, flags } = await this.parse(Algo);
|
|
91
|
+
// ─── Subcommands ───
|
|
92
|
+
if (args.strategy === 'status') {
|
|
93
|
+
await this.showStatus();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (args.strategy === 'stop') {
|
|
97
|
+
await this.stopSession(flags.pair, flags.all);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// ─── Pre-flight checks ───
|
|
101
|
+
if (!hasFullSetup()) {
|
|
102
|
+
this.log('');
|
|
103
|
+
this.log(` ${red('✘')} Algo trading requires account setup.`);
|
|
104
|
+
this.log(` Run: ${cyan('rift auth setup')}`);
|
|
105
|
+
this.log('');
|
|
106
|
+
const doSetup = await ask(` ${cyan('Run setup now?')} ${dim('(yes/no)')}: `);
|
|
107
|
+
if (doSetup.toLowerCase() === 'yes' || doSetup.toLowerCase() === 'y') {
|
|
108
|
+
await this.config.runCommand('auth', ['setup']);
|
|
109
|
+
if (!hasFullSetup()) {
|
|
110
|
+
this.log(`\n ${red('✘')} Setup incomplete. Try again.\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const creds = loadCredentials();
|
|
119
|
+
if (!creds) {
|
|
120
|
+
this.log(`\n ${red('✘')} No credentials. Run: ${cyan('rift auth setup')}\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Strategy selection
|
|
124
|
+
let strategy = args.strategy;
|
|
125
|
+
if (!strategy) {
|
|
126
|
+
strategy = await this.interactiveStrategyPicker();
|
|
127
|
+
if (!strategy)
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const key = sessionKey(strategy, flags.pair);
|
|
131
|
+
// If already running, attach the viewer
|
|
132
|
+
if (isSessionRunning(key)) {
|
|
133
|
+
this.log('');
|
|
134
|
+
this.log(` ${greenBg(' ● ALGO ')} Session already running: ${bold(strategy)} on ${flags.pair}`);
|
|
135
|
+
this.log(` ${dim('Attaching dashboard viewer... Press Ctrl+C to detach (trading continues).')}`);
|
|
136
|
+
this.log('');
|
|
137
|
+
await this.attachViewer(key);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Risk disclaimer + confirmation gate
|
|
141
|
+
this.log('');
|
|
142
|
+
this.log(` ${bold('╔══════════════════════════════════════════════════════════════╗')}`);
|
|
143
|
+
this.log(` ${bold('║')} ${greenBg(' ● ALGO ')} ${bold('REAL MONEY — REAL ORDERS')} ${bold('║')}`);
|
|
144
|
+
this.log(` ${bold('╠══════════════════════════════════════════════════════════════╣')}`);
|
|
145
|
+
const bw = 62;
|
|
146
|
+
const cRow = (content) => {
|
|
147
|
+
const pad = Math.max(0, bw - visLen(content));
|
|
148
|
+
return ` ${bold('║')}${content}${' '.repeat(pad)}${bold('║')}`;
|
|
149
|
+
};
|
|
150
|
+
this.log(cRow(` Strategy: ${bold(strategy)}`));
|
|
151
|
+
this.log(cRow(` Pair: ${flags.pair} PERP`));
|
|
152
|
+
this.log(cRow(` Wallet: ${maskAddress(getAccountAddress(creds))}`));
|
|
153
|
+
this.log(cRow(` Fee: ${BUILDER_FEE_DISPLAY} per side`));
|
|
154
|
+
this.log(` ${bold('╠══════════════════════════════════════════════════════════════╣')}`);
|
|
155
|
+
this.log(cRow(` ${red('RISK DISCLAIMER')}`));
|
|
156
|
+
this.log(cRow(``));
|
|
157
|
+
this.log(cRow(` ${dim('Trading perpetual futures involves substantial risk of')}`));
|
|
158
|
+
this.log(cRow(` ${dim('loss. Past performance and backtested results do NOT')}`));
|
|
159
|
+
this.log(cRow(` ${dim('guarantee future returns. You may lose some or all of')}`));
|
|
160
|
+
this.log(cRow(` ${dim('your deposited funds.')}`));
|
|
161
|
+
this.log(cRow(``));
|
|
162
|
+
this.log(cRow(` ${dim('RIFT is provided "as is" without warranty of any kind.')}`));
|
|
163
|
+
this.log(cRow(` ${dim('Nexstone and its contributors are NOT liable for any')}`));
|
|
164
|
+
this.log(cRow(` ${dim('trading losses, missed executions, software errors, or')}`));
|
|
165
|
+
this.log(cRow(` ${dim('exchange outages. You are solely responsible for your')}`));
|
|
166
|
+
this.log(cRow(` ${dim('trading decisions and capital.')}`));
|
|
167
|
+
this.log(cRow(``));
|
|
168
|
+
this.log(cRow(` ${dim('By typing "LIVE" you acknowledge these risks and agree')}`));
|
|
169
|
+
this.log(cRow(` ${dim('that you trade entirely at your own risk.')}`));
|
|
170
|
+
this.log(` ${bold('╚══════════════════════════════════════════════════════════════╝')}`);
|
|
171
|
+
this.log('');
|
|
172
|
+
const confirm = await ask(` ${red('Type "LIVE" to accept risk & start')}: `);
|
|
173
|
+
if (confirm !== 'LIVE') {
|
|
174
|
+
this.log(dim('\n Cancelled.\n'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.log('');
|
|
178
|
+
await this.startAlgo(strategy, flags.pair, flags.tf, flags.equity, creds, flags['size-usd']);
|
|
179
|
+
}
|
|
180
|
+
async interactiveStrategyPicker() {
|
|
181
|
+
// Discover strategies dynamically from engine
|
|
182
|
+
let strategies = [];
|
|
183
|
+
try {
|
|
184
|
+
await new Promise((resolve, reject) => {
|
|
185
|
+
runEngine('strategies', [], (msg) => {
|
|
186
|
+
if (msg.type === 'result') {
|
|
187
|
+
const strats = msg.strategies || [];
|
|
188
|
+
strategies = strats.map((s) => ({ name: s.name, desc: s.doc || s.class, grade: '' }));
|
|
189
|
+
}
|
|
190
|
+
}).then(resolve).catch(reject);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
strategies = [{ name: 'trend_follow', desc: 'Bidirectional EMA-crossover trend follower (OSS demo strategy)', grade: 'C' }];
|
|
195
|
+
}
|
|
196
|
+
this.log('');
|
|
197
|
+
this.log(` ${bold('Select a strategy for algo trading:')}`);
|
|
198
|
+
this.log('');
|
|
199
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
200
|
+
const s = strategies[i];
|
|
201
|
+
const gc = s.grade === 'A' ? green : cyan;
|
|
202
|
+
this.log(` ${cyan(String(i + 1))} ${bold(s.name.padEnd(22))} ${gc(s.grade)} ${dim(s.desc)}`);
|
|
203
|
+
}
|
|
204
|
+
this.log(` ${cyan(String(strategies.length + 1))} ${dim('Enter custom name')}`);
|
|
205
|
+
this.log('');
|
|
206
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
207
|
+
const idx = parseInt(choice) - 1;
|
|
208
|
+
if (idx >= 0 && idx < strategies.length)
|
|
209
|
+
return strategies[idx].name;
|
|
210
|
+
return await ask(` ${cyan('Strategy name')}: `) || undefined;
|
|
211
|
+
}
|
|
212
|
+
// ─── DAEMON LAUNCH ───
|
|
213
|
+
async startAlgo(strategy, pair, tf, equity, creds, sizeUsdOverride = '') {
|
|
214
|
+
const key = sessionKey(strategy, pair);
|
|
215
|
+
const engineArgs = [
|
|
216
|
+
'--strategy', strategy,
|
|
217
|
+
'--pair', pair,
|
|
218
|
+
'--equity', String(equity),
|
|
219
|
+
'--account', getAccountAddress(creds),
|
|
220
|
+
];
|
|
221
|
+
// Note: do NOT forward --tf. The engine `algo` command takes its
|
|
222
|
+
// interval from the strategy's `default_interval` (e.g. trend_follow
|
|
223
|
+
// declares "4h"). Passing --tf would crash the daemon at argparse
|
|
224
|
+
// ("No such option: --tf") and — because spawnDaemon redirects stdio
|
|
225
|
+
// to /dev/null — the failure would be silent. The viewer would then
|
|
226
|
+
// render an empty session-complete banner without any actual run.
|
|
227
|
+
// The `tf` arg from the wrapper is informational only (used by the
|
|
228
|
+
// viewer for header display).
|
|
229
|
+
void tf; // intentionally unused at engine call site
|
|
230
|
+
// Optional size override (small-account setup verification). Only
|
|
231
|
+
// forward when the user explicitly passed a value — empty string ==
|
|
232
|
+
// no override, engine's default of 0.0 means "use risk model".
|
|
233
|
+
if (sizeUsdOverride && sizeUsdOverride !== '0') {
|
|
234
|
+
engineArgs.push('--size-usd', sizeUsdOverride);
|
|
235
|
+
}
|
|
236
|
+
// Spawn the engine as a background daemon
|
|
237
|
+
const { pid } = spawnDaemon('algo', engineArgs, {
|
|
238
|
+
HYPERLIQUID_PRIVATE_KEY: creds.private_key,
|
|
239
|
+
});
|
|
240
|
+
this.log(` ${greenBg(' ● ALGO ')} Daemon started ${dim(`(PID ${pid})`)}`);
|
|
241
|
+
this.log(` ${dim('Trading runs in background. Press Ctrl+C to detach (trading continues).')}`);
|
|
242
|
+
this.log('');
|
|
243
|
+
// Wait briefly for the daemon to initialize and write first state
|
|
244
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
245
|
+
// Attach the viewer
|
|
246
|
+
await this.attachViewer(key);
|
|
247
|
+
}
|
|
248
|
+
// ─── FILE-BASED VIEWER ───
|
|
249
|
+
async attachViewer(key) {
|
|
250
|
+
this.sessionStart = Date.now();
|
|
251
|
+
this.dashboardActive = false;
|
|
252
|
+
this.ds = createDashboardState();
|
|
253
|
+
this.ds.isLive = true;
|
|
254
|
+
this.viewerRunning = true;
|
|
255
|
+
// Ctrl+C detaches the viewer — does NOT stop the daemon
|
|
256
|
+
const sigintHandler = () => {
|
|
257
|
+
this.viewerRunning = false;
|
|
258
|
+
};
|
|
259
|
+
process.on('SIGINT', sigintHandler);
|
|
260
|
+
// Tick at 200ms — read state file and render
|
|
261
|
+
this.tickTimer = setInterval(() => {
|
|
262
|
+
if (!this.viewerRunning)
|
|
263
|
+
return;
|
|
264
|
+
// Check if daemon is still alive
|
|
265
|
+
if (!isSessionRunning(key)) {
|
|
266
|
+
this.viewerRunning = false;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const snapshot = readSessionSnapshot(key);
|
|
270
|
+
if (snapshot && snapshot.state) {
|
|
271
|
+
const state = snapshot.state;
|
|
272
|
+
// Update dashboard state from snapshot
|
|
273
|
+
this.ds.price = state.last_price ?? this.ds.price;
|
|
274
|
+
this.ds.priceDelta = state.price_delta ?? 0;
|
|
275
|
+
this.ds.sessionHigh = state.session_high ?? this.ds.sessionHigh;
|
|
276
|
+
this.ds.sessionLow = state.session_low ?? this.ds.sessionLow;
|
|
277
|
+
this.ds.equity = state.equity ?? this.ds.equity;
|
|
278
|
+
this.ds.totalEquity = state.total_equity ?? this.ds.totalEquity;
|
|
279
|
+
this.ds.totalPnlPct = state.total_pnl_pct ?? this.ds.totalPnlPct;
|
|
280
|
+
this.ds.unrealizedPnl = state.unrealized_pnl ?? this.ds.unrealizedPnl;
|
|
281
|
+
this.ds.numTrades = state.num_trades ?? this.ds.numTrades;
|
|
282
|
+
this.ds.winRate = state.win_rate ?? this.ds.winRate;
|
|
283
|
+
this.ds.totalFunding = state.total_funding ?? this.ds.totalFunding;
|
|
284
|
+
this.ds.fundingRate = state.last_funding_rate ?? this.ds.fundingRate;
|
|
285
|
+
this.ds.position = state.position ?? this.ds.position;
|
|
286
|
+
this.ds.stopProximity = state.stop_proximity ?? this.ds.stopProximity;
|
|
287
|
+
this.ds.priceHistory = state.price_history ?? this.ds.priceHistory;
|
|
288
|
+
this.ds.recentTrades = state.recent_trades ?? this.ds.recentTrades;
|
|
289
|
+
this.ds.strategy = state.strategy ?? this.ds.strategy;
|
|
290
|
+
this.ds.pair = state.pair ?? this.ds.pair;
|
|
291
|
+
this.ds.interval = state.interval ?? this.ds.interval;
|
|
292
|
+
this.ds.peakEquity = state.peak_equity ?? this.ds.peakEquity;
|
|
293
|
+
this.ds.wallet = state.wallet ?? this.ds.wallet;
|
|
294
|
+
// From snapshot envelope
|
|
295
|
+
this.ds.fundingCountdownMin = snapshot.funding_countdown_min ?? this.ds.fundingCountdownMin;
|
|
296
|
+
this.ds.predictedFunding = snapshot.predicted_funding ?? this.ds.predictedFunding;
|
|
297
|
+
this.ds.candleRemainingBaseline = snapshot.candle_remaining_sec ?? this.ds.candleRemainingBaseline;
|
|
298
|
+
this.ds.candleProgressBaseline = snapshot.candle_progress ?? this.ds.candleProgressBaseline;
|
|
299
|
+
this.ds.heartbeatTime = Date.now();
|
|
300
|
+
// Reasoning/indicators from snapshot
|
|
301
|
+
const reasoning = snapshot.reasoning;
|
|
302
|
+
if (reasoning) {
|
|
303
|
+
this.ds.reasoning = reasoning;
|
|
304
|
+
this.ds.conditions = reasoning.conditions ?? this.ds.conditions;
|
|
305
|
+
this.ds.signalStatus = reasoning.signal_status ?? this.ds.signalStatus;
|
|
306
|
+
this.ds.summary = reasoning.summary ?? this.ds.summary;
|
|
307
|
+
}
|
|
308
|
+
if (snapshot.indicators) {
|
|
309
|
+
this.ds.indicators = snapshot.indicators;
|
|
310
|
+
}
|
|
311
|
+
this.renderAlgoDashboard();
|
|
312
|
+
}
|
|
313
|
+
}, 200);
|
|
314
|
+
// Block until viewer exits
|
|
315
|
+
await new Promise(resolve => {
|
|
316
|
+
const check = setInterval(() => {
|
|
317
|
+
if (!this.viewerRunning) {
|
|
318
|
+
clearInterval(check);
|
|
319
|
+
resolve();
|
|
320
|
+
}
|
|
321
|
+
}, 100);
|
|
322
|
+
});
|
|
323
|
+
// Cleanup
|
|
324
|
+
if (this.tickTimer) {
|
|
325
|
+
clearInterval(this.tickTimer);
|
|
326
|
+
this.tickTimer = null;
|
|
327
|
+
}
|
|
328
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
329
|
+
if (this.dashboardActive)
|
|
330
|
+
this.clearDashboard();
|
|
331
|
+
// Check if daemon is still running
|
|
332
|
+
if (isSessionRunning(key)) {
|
|
333
|
+
this.log('');
|
|
334
|
+
this.log(` ${dim('Dashboard detached. Trading continues in background.')}`);
|
|
335
|
+
this.log(` ${dim(`Run ${cyan('rift algo status')} to check or ${cyan('rift algo stop')} to end.`)}`);
|
|
336
|
+
this.log('');
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// Daemon exited — show final state
|
|
340
|
+
const snapshot = readSessionSnapshot(key);
|
|
341
|
+
if (snapshot?.state) {
|
|
342
|
+
this.log('');
|
|
343
|
+
this.renderSessionSummary(snapshot.state, snapshot);
|
|
344
|
+
this.log('');
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
this.log('');
|
|
348
|
+
this.log(` ${dim('Session ended.')}`);
|
|
349
|
+
this.log('');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ─── STATUS ───
|
|
354
|
+
async showStatus() {
|
|
355
|
+
const pidsDir = getAlgoPidsDir();
|
|
356
|
+
if (!fs.existsSync(pidsDir)) {
|
|
357
|
+
this.log('');
|
|
358
|
+
this.log(` ${dim('No algo sessions running.')}`);
|
|
359
|
+
this.log('');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const pidFiles = fs.readdirSync(pidsDir).filter(f => f.endsWith('.pid'));
|
|
363
|
+
if (pidFiles.length === 0) {
|
|
364
|
+
this.log('');
|
|
365
|
+
this.log(` ${dim('No algo sessions running.')}`);
|
|
366
|
+
this.log('');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.log('');
|
|
370
|
+
this.log(` ${bold('Algo Trading Sessions')}`);
|
|
371
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
372
|
+
let found = 0;
|
|
373
|
+
for (const pidFile of pidFiles) {
|
|
374
|
+
const key = pidFile.replace('.pid', '');
|
|
375
|
+
if (!isSessionRunning(key))
|
|
376
|
+
continue;
|
|
377
|
+
found++;
|
|
378
|
+
const snapshot = readSessionSnapshot(key);
|
|
379
|
+
const state = snapshot?.state;
|
|
380
|
+
const strategy = state?.strategy ?? key.split('_')[0];
|
|
381
|
+
const pair = state?.pair ?? key.split('_').slice(1).join('_');
|
|
382
|
+
const equity = state?.total_equity ?? 0;
|
|
383
|
+
const pnlPct = state?.total_pnl_pct ?? 0;
|
|
384
|
+
const trades = state?.num_trades ?? 0;
|
|
385
|
+
const winRate = state?.win_rate ?? 0;
|
|
386
|
+
const position = state?.position;
|
|
387
|
+
const startedAt = state?.started_at ?? '';
|
|
388
|
+
const pid = parseInt(fs.readFileSync(path.join(pidsDir, pidFile), 'utf-8').trim());
|
|
389
|
+
const posStr = position
|
|
390
|
+
? `${position.side === 'long' ? green('LONG') : red('SHORT')} ${position.size?.toFixed(4)} @ $${position.entry_price}`
|
|
391
|
+
: dim('FLAT');
|
|
392
|
+
this.log('');
|
|
393
|
+
this.log(` ${greenBg(' ● ALGO ')} ${bold(strategy)} on ${pair} ${dim(`PID ${pid}`)}`);
|
|
394
|
+
this.log(` Equity: ${bold('$' + equity.toLocaleString())} ${colorPnl(pnlPct, '%')} Trades: ${trades} Win: ${winRate}%`);
|
|
395
|
+
this.log(` Position: ${posStr}`);
|
|
396
|
+
if (startedAt)
|
|
397
|
+
this.log(` Started: ${dim(startedAt)}`);
|
|
398
|
+
}
|
|
399
|
+
if (found === 0) {
|
|
400
|
+
this.log(` ${dim('No algo sessions running.')}`);
|
|
401
|
+
}
|
|
402
|
+
this.log('');
|
|
403
|
+
}
|
|
404
|
+
// ─── STOP ───
|
|
405
|
+
async stopSession(pair, all) {
|
|
406
|
+
const pidsDir = getAlgoPidsDir();
|
|
407
|
+
if (!fs.existsSync(pidsDir)) {
|
|
408
|
+
this.log(`\n ${dim('No algo sessions running.')}\n`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const pidFiles = fs.readdirSync(pidsDir).filter(f => f.endsWith('.pid'));
|
|
412
|
+
const running = pidFiles.filter(f => isSessionRunning(f.replace('.pid', '')));
|
|
413
|
+
if (running.length === 0) {
|
|
414
|
+
this.log(`\n ${dim('No algo sessions running.')}\n`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (all) {
|
|
418
|
+
for (const pidFile of running) {
|
|
419
|
+
const key = pidFile.replace('.pid', '');
|
|
420
|
+
await this.stopByKey(key);
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (running.length === 1) {
|
|
425
|
+
const key = running[0].replace('.pid', '');
|
|
426
|
+
await this.stopByKey(key);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Multiple sessions — let user pick
|
|
430
|
+
this.log('');
|
|
431
|
+
this.log(` ${bold('Select session to stop:')}`);
|
|
432
|
+
this.log('');
|
|
433
|
+
for (let i = 0; i < running.length; i++) {
|
|
434
|
+
const key = running[i].replace('.pid', '');
|
|
435
|
+
const snapshot = readSessionSnapshot(key);
|
|
436
|
+
const strategy = snapshot?.state?.strategy ?? key;
|
|
437
|
+
const p = snapshot?.state?.pair ?? '';
|
|
438
|
+
this.log(` ${cyan(String(i + 1))} ${bold(strategy)} on ${p}`);
|
|
439
|
+
}
|
|
440
|
+
this.log(` ${cyan(String(running.length + 1))} ${dim('Stop all')}`);
|
|
441
|
+
this.log('');
|
|
442
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
443
|
+
const idx = parseInt(choice) - 1;
|
|
444
|
+
if (idx === running.length) {
|
|
445
|
+
// Stop all
|
|
446
|
+
for (const pidFile of running) {
|
|
447
|
+
await this.stopByKey(pidFile.replace('.pid', ''));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else if (idx >= 0 && idx < running.length) {
|
|
451
|
+
await this.stopByKey(running[idx].replace('.pid', ''));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async stopByKey(key) {
|
|
455
|
+
const pidFile = path.join(getAlgoPidsDir(), `${key}.pid`);
|
|
456
|
+
if (!fs.existsSync(pidFile))
|
|
457
|
+
return;
|
|
458
|
+
let pid;
|
|
459
|
+
try {
|
|
460
|
+
pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.log(` ${dim('Stopping')} ${bold(key)} ${dim(`(PID ${pid})...`)}`);
|
|
466
|
+
// Send SIGTERM for graceful shutdown
|
|
467
|
+
try {
|
|
468
|
+
process.kill(pid, 'SIGTERM');
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
this.log(` ${dim('Process already exited.')}`);
|
|
472
|
+
try {
|
|
473
|
+
fs.unlinkSync(pidFile);
|
|
474
|
+
}
|
|
475
|
+
catch { }
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// Wait for graceful shutdown (up to 30s)
|
|
479
|
+
for (let i = 0; i < 60; i++) {
|
|
480
|
+
try {
|
|
481
|
+
process.kill(pid, 0); // test alive
|
|
482
|
+
await new Promise(r => setTimeout(r, 500));
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
break; // process exited
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Show final state
|
|
489
|
+
const snapshot = readSessionSnapshot(key);
|
|
490
|
+
if (snapshot?.state) {
|
|
491
|
+
this.log('');
|
|
492
|
+
this.renderSessionSummary(snapshot.state, snapshot);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
this.log(` ${green('✔')} Session stopped.`);
|
|
496
|
+
}
|
|
497
|
+
// Clean up PID file
|
|
498
|
+
try {
|
|
499
|
+
fs.unlinkSync(pidFile);
|
|
500
|
+
}
|
|
501
|
+
catch { }
|
|
502
|
+
this.log('');
|
|
503
|
+
}
|
|
504
|
+
// ─── Dashboard (identical visual output) ───
|
|
505
|
+
renderAlgoDashboard() {
|
|
506
|
+
const ds = this.ds;
|
|
507
|
+
const pos = ds.position;
|
|
508
|
+
const { remaining: candleRemaining, progress: candleProgress } = getLocalCountdown(ds);
|
|
509
|
+
const elapsed = Math.floor((Date.now() - this.sessionStart) / 1000);
|
|
510
|
+
const hours = Math.floor(elapsed / 3600);
|
|
511
|
+
const mins = Math.floor((elapsed % 3600) / 60);
|
|
512
|
+
const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
513
|
+
const lines = [];
|
|
514
|
+
// Header — LIVE badge + wallet + market type
|
|
515
|
+
const walletShort = ds.wallet ? maskAddr(ds.wallet) : '';
|
|
516
|
+
const mktTypes = {};
|
|
517
|
+
const mkt = mktTypes[ds.strategy] || '';
|
|
518
|
+
const mktTag = mkt ? ` ${dim('│')} ${mkt === 'All Conditions' ? cyan(mkt) : dim(mkt)}` : '';
|
|
519
|
+
lines.push(` ${greenBg(' ● ALGO ')} ${dim(walletShort)} ${dim('│')} ${dim(ds.strategy)}${mktTag} ${dim('│')} ${dim(duration)}`);
|
|
520
|
+
// Price + chart
|
|
521
|
+
const priceFixed = ds.price > 0 ? `$${ds.price.toFixed(2)}` : '$...';
|
|
522
|
+
const priceCol = padEndVis(bold(priceFixed), 14);
|
|
523
|
+
const deltaRaw = ds.priceDelta > 0 ? green(`▲ +$${ds.priceDelta.toFixed(0)}`)
|
|
524
|
+
: ds.priceDelta < 0 ? red(`▼ -$${Math.abs(ds.priceDelta).toFixed(0)}`)
|
|
525
|
+
: dim('─ $0');
|
|
526
|
+
const deltaCol = padEndVis(deltaRaw, 10);
|
|
527
|
+
const chart = sparkline(ds.priceHistory, 28);
|
|
528
|
+
lines.push(` ${priceCol} ${deltaCol} ${chart}`);
|
|
529
|
+
// Session high/low
|
|
530
|
+
if (ds.sessionHigh > 0 && ds.sessionLow > 0 && ds.sessionLow < ds.sessionHigh) {
|
|
531
|
+
lines.push(` ${dim('Session:')} H ${dim('$' + ds.sessionHigh.toLocaleString())} L ${dim('$' + ds.sessionLow.toLocaleString())}`);
|
|
532
|
+
}
|
|
533
|
+
// Strategy reasoning
|
|
534
|
+
if (ds.conditions.length > 0) {
|
|
535
|
+
lines.push('');
|
|
536
|
+
lines.push(` ${dim('STRATEGY')}`);
|
|
537
|
+
for (const c of ds.conditions) {
|
|
538
|
+
const pct = c.pct ?? 0;
|
|
539
|
+
const met = c.met;
|
|
540
|
+
const name = c.name.padEnd(14);
|
|
541
|
+
const detail = c.detail || '';
|
|
542
|
+
const barWidth = 20;
|
|
543
|
+
const filled = Math.round(Math.min(1, pct) * barWidth);
|
|
544
|
+
const empty = barWidth - filled;
|
|
545
|
+
let barColor;
|
|
546
|
+
if (met)
|
|
547
|
+
barColor = green;
|
|
548
|
+
else if (pct >= 0.8)
|
|
549
|
+
barColor = yellow;
|
|
550
|
+
else if (pct >= 0.5)
|
|
551
|
+
barColor = cyan;
|
|
552
|
+
else
|
|
553
|
+
barColor = dim;
|
|
554
|
+
const gauge = barColor('━'.repeat(filled)) + dim('╌'.repeat(empty));
|
|
555
|
+
const icon = met ? green('✔') : dim('·');
|
|
556
|
+
lines.push(` ${icon} ${dim(name)} ${gauge} ${dim(String(c.value ?? ''))} ${dim(detail)}`);
|
|
557
|
+
}
|
|
558
|
+
const summaryColor = ds.signalStatus === 'entry_near' ? yellow
|
|
559
|
+
: ds.signalStatus === 'warming' ? cyan
|
|
560
|
+
: ds.signalStatus === 'in_position' ? green
|
|
561
|
+
: ds.signalStatus === 'exit_near' ? red : dim;
|
|
562
|
+
lines.push(` ${summaryColor('▸')} ${summaryColor(ds.summary)}`);
|
|
563
|
+
}
|
|
564
|
+
lines.push('');
|
|
565
|
+
// Position or FLAT
|
|
566
|
+
if (pos) {
|
|
567
|
+
const sideColor = pos.side === 'long' ? green : red;
|
|
568
|
+
lines.push(` ${sideColor('●')} ${sideColor(String(pos.side).toUpperCase())} ${pos.size?.toFixed(4)} @ $${pos.entry_price?.toLocaleString()} ${dim('│')} Unreal: ${colorPnl(ds.unrealizedPnl)} ${dim('│')} Hold: ${pos.candles_held}`);
|
|
569
|
+
if (ds.stopProximity > 0.1) {
|
|
570
|
+
const proxLabel = ds.stopProximity > 0.8 ? red('DANGER') : ds.stopProximity > 0.5 ? yellow('CAUTION') : dim('safe');
|
|
571
|
+
lines.push(` ${dim('SL')} ${proximityBar(ds.stopProximity)} ${proxLabel}`);
|
|
572
|
+
}
|
|
573
|
+
if (ds.fundingCountdownMin > 0) {
|
|
574
|
+
lines.push(` ${fundingCountdown(ds.fundingCountdownMin, ds.predictedFunding)}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
if (ds.recentCandles.length > 0) {
|
|
579
|
+
lines.push(` ${dim('RECENT CANDLES')}`);
|
|
580
|
+
for (const c of ds.recentCandles.slice(-4)) {
|
|
581
|
+
const changeColor = c.change_pct >= 0 ? green : red;
|
|
582
|
+
const arrow = c.change_pct >= 0 ? '▲' : '▼';
|
|
583
|
+
lines.push(` ${dim(String(c.time))} ${dim('$')}${String(c.close).padEnd(10)} ${changeColor(arrow)} ${changeColor(String(c.change_pct) + '%')}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Candle countdown
|
|
588
|
+
if (candleRemaining > 0) {
|
|
589
|
+
const countdownWidth = 30;
|
|
590
|
+
const filled = Math.round(candleProgress * countdownWidth);
|
|
591
|
+
const empty = countdownWidth - filled;
|
|
592
|
+
const countdownBar = cyan('█'.repeat(filled)) + dim('░'.repeat(empty));
|
|
593
|
+
const remainMin = Math.floor(candleRemaining / 60);
|
|
594
|
+
const remainSec = candleRemaining % 60;
|
|
595
|
+
const remainStr = remainMin > 0 ? `${remainMin}:${String(remainSec).padStart(2, '0')}` : `${remainSec}s`;
|
|
596
|
+
lines.push(` ${dim('NEXT CANDLE')} ${countdownBar} ${dim(remainStr)}`);
|
|
597
|
+
}
|
|
598
|
+
// Session stats
|
|
599
|
+
lines.push('');
|
|
600
|
+
lines.push(` ${dim('EQUITY')} ${bold('$' + ds.totalEquity.toLocaleString())} ${colorPnl(ds.totalPnlPct, '%')} ${dim('│')} ${dim('TRADES')} ${ds.numTrades} ${dim('WIN')} ${ds.winRate}% ${dim('│')} ${dim('FUNDING')} ${colorPnl(ds.totalFunding)}`);
|
|
601
|
+
// Redraw with fixed height
|
|
602
|
+
const H = Algo.DASHBOARD_HEIGHT;
|
|
603
|
+
while (lines.length < H)
|
|
604
|
+
lines.push('');
|
|
605
|
+
if (lines.length > H)
|
|
606
|
+
lines.length = H;
|
|
607
|
+
if (this.dashboardActive) {
|
|
608
|
+
process.stdout.write(`\x1b[${H}A`);
|
|
609
|
+
}
|
|
610
|
+
for (const line of lines) {
|
|
611
|
+
process.stdout.write(`\x1b[2K${line}\n`);
|
|
612
|
+
}
|
|
613
|
+
this.dashboardActive = true;
|
|
614
|
+
}
|
|
615
|
+
// ─── Session Summary ───
|
|
616
|
+
renderSessionSummary(state, msg) {
|
|
617
|
+
const narrative = msg.narrative;
|
|
618
|
+
const recentTrades = state.recent_trades || [];
|
|
619
|
+
const wallet = state.wallet || '';
|
|
620
|
+
const iw = 58;
|
|
621
|
+
const row = (content) => {
|
|
622
|
+
const pad = Math.max(0, iw - visLen(content));
|
|
623
|
+
return ` ${bold('║')}${content}${' '.repeat(pad)}${bold('║')}`;
|
|
624
|
+
};
|
|
625
|
+
const blank = row('');
|
|
626
|
+
const divider = ` ${bold('╠' + '═'.repeat(iw) + '╣')}`;
|
|
627
|
+
this.log(` ${bold('╔' + '═'.repeat(iw) + '╗')}`);
|
|
628
|
+
this.log(` ${bold('║')} ${greenBg(' ● ALGO SESSION COMPLETE ')}${' '.repeat(iw - 28)}${bold('║')}`);
|
|
629
|
+
this.log(divider);
|
|
630
|
+
if (wallet) {
|
|
631
|
+
this.log(row(` Wallet: ${wallet}`));
|
|
632
|
+
}
|
|
633
|
+
const initialEquity = state.initial_equity ?? 0;
|
|
634
|
+
const totalEquity = state.total_equity ?? initialEquity;
|
|
635
|
+
const totalPnlPct = state.total_pnl_pct ?? 0;
|
|
636
|
+
const numTrades = state.num_trades ?? 0;
|
|
637
|
+
const winRate = state.win_rate ?? 0;
|
|
638
|
+
const totalFunding = state.total_funding ?? 0;
|
|
639
|
+
const peakEquity = state.peak_equity ?? initialEquity;
|
|
640
|
+
const candlesProcessed = state.candles_processed ?? 0;
|
|
641
|
+
if (narrative) {
|
|
642
|
+
const story = narrative.story || '';
|
|
643
|
+
const insight = narrative.insight || '';
|
|
644
|
+
const projection = narrative.projection || {};
|
|
645
|
+
if (story) {
|
|
646
|
+
this.log(blank);
|
|
647
|
+
this.log(row(` ${bold('THE STORY:')}`));
|
|
648
|
+
for (const line of this.wordWrap(story, iw - 4))
|
|
649
|
+
this.log(row(` ${line}`));
|
|
650
|
+
}
|
|
651
|
+
if (insight) {
|
|
652
|
+
this.log(blank);
|
|
653
|
+
this.log(row(` ${bold('KEY INSIGHT:')}`));
|
|
654
|
+
for (const line of this.wordWrap(insight, iw - 4))
|
|
655
|
+
this.log(row(` ${line}`));
|
|
656
|
+
}
|
|
657
|
+
this.log(blank);
|
|
658
|
+
this.log(divider);
|
|
659
|
+
const resultRow = (label, value) => {
|
|
660
|
+
const lp = ` ${label}:`;
|
|
661
|
+
const gap = Math.max(1, iw - lp.length - visLen(value) - 1);
|
|
662
|
+
return row(`${lp}${' '.repeat(gap)}${value}`);
|
|
663
|
+
};
|
|
664
|
+
this.log(resultRow('Starting Equity', `$${initialEquity.toLocaleString()}`));
|
|
665
|
+
this.log(resultRow('Final Equity', `$${totalEquity.toLocaleString()}`));
|
|
666
|
+
this.log(resultRow('Total P&L', colorPnl(totalPnlPct, '%')));
|
|
667
|
+
this.log(resultRow('Trades', String(numTrades)));
|
|
668
|
+
this.log(resultRow('Win Rate', `${winRate}%`));
|
|
669
|
+
this.log(resultRow('Funding Collected', colorPnl(totalFunding)));
|
|
670
|
+
this.log(resultRow('Peak Equity', `$${peakEquity.toLocaleString()}`));
|
|
671
|
+
this.log(resultRow('Duration', `${candlesProcessed} candles`));
|
|
672
|
+
if (projection && projection.daily) {
|
|
673
|
+
this.log(blank);
|
|
674
|
+
this.log(divider);
|
|
675
|
+
this.log(row(` ${bold('PROJECTION')} ${dim('(if session repeated)')}`));
|
|
676
|
+
this.log(row(` Daily: $${String(projection.daily).padEnd(10)} Monthly: $${projection.monthly}`));
|
|
677
|
+
this.log(row(` Annual: $${String(projection.annual).padEnd(9)} APY: ${projection.apy}%`));
|
|
678
|
+
this.log(row(` ${dim(`(based on ${projection.hours_observed}h — not a guarantee)`)}`));
|
|
679
|
+
}
|
|
680
|
+
this.log(blank);
|
|
681
|
+
}
|
|
682
|
+
if (recentTrades.length > 0) {
|
|
683
|
+
this.log(divider);
|
|
684
|
+
this.log(row(` ${bold('TRADE LOG')}`));
|
|
685
|
+
this.log(divider);
|
|
686
|
+
for (const t of recentTrades) {
|
|
687
|
+
const sideColor = t.side === 'long' ? green : red;
|
|
688
|
+
const pnlStr = colorPnl(t.pnl ?? 0);
|
|
689
|
+
const reason = (t.exit_reason || '').replace(/_/g, ' ');
|
|
690
|
+
const oid = t.oid ? dim(` oid:${t.oid}`) : '';
|
|
691
|
+
this.log(row(` ${sideColor('●')} ${sideColor(String(t.side).toUpperCase().padEnd(6))} ${padEndVis(pnlStr, 12)} ${dim(String(t.candles_held ?? 0) + 'c')} ${dim(reason)}${oid}`));
|
|
692
|
+
}
|
|
693
|
+
this.log(blank);
|
|
694
|
+
}
|
|
695
|
+
if (wallet) {
|
|
696
|
+
this.log(divider);
|
|
697
|
+
this.log(row(` ${dim('Verified on Hyperliquid │ Wallet: ' + maskAddress(wallet))}`));
|
|
698
|
+
}
|
|
699
|
+
this.log(` ${bold('╚' + '═'.repeat(iw) + '╝')}`);
|
|
700
|
+
this.log('');
|
|
701
|
+
}
|
|
702
|
+
wordWrap(text, maxWidth) {
|
|
703
|
+
const words = text.split(' ');
|
|
704
|
+
const lines = [];
|
|
705
|
+
let current = '';
|
|
706
|
+
for (const word of words) {
|
|
707
|
+
if (current.length + word.length + 1 > maxWidth) {
|
|
708
|
+
lines.push(current);
|
|
709
|
+
current = word;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
current = current ? current + ' ' + word : word;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (current)
|
|
716
|
+
lines.push(current);
|
|
717
|
+
return lines;
|
|
718
|
+
}
|
|
719
|
+
}
|