@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.
Files changed (137) hide show
  1. package/LICENSE +201 -0
  2. package/bin/run.js +22 -0
  3. package/dist/commands/algo.d.ts +32 -0
  4. package/dist/commands/algo.js +719 -0
  5. package/dist/commands/audit.d.ts +13 -0
  6. package/dist/commands/audit.js +37 -0
  7. package/dist/commands/auth-status.d.ts +14 -0
  8. package/dist/commands/auth-status.js +118 -0
  9. package/dist/commands/auth.d.ts +14 -0
  10. package/dist/commands/auth.js +275 -0
  11. package/dist/commands/backtest.d.ts +26 -0
  12. package/dist/commands/backtest.js +283 -0
  13. package/dist/commands/collect/start.d.ts +11 -0
  14. package/dist/commands/collect/start.js +78 -0
  15. package/dist/commands/collect/status.d.ts +6 -0
  16. package/dist/commands/collect/status.js +60 -0
  17. package/dist/commands/compare.d.ts +16 -0
  18. package/dist/commands/compare.js +130 -0
  19. package/dist/commands/config.d.ts +16 -0
  20. package/dist/commands/config.js +143 -0
  21. package/dist/commands/cost.d.ts +20 -0
  22. package/dist/commands/cost.js +104 -0
  23. package/dist/commands/cross-asset.d.ts +14 -0
  24. package/dist/commands/cross-asset.js +39 -0
  25. package/dist/commands/data/fetch.d.ts +15 -0
  26. package/dist/commands/data/fetch.js +82 -0
  27. package/dist/commands/data/list.d.ts +6 -0
  28. package/dist/commands/data/list.js +28 -0
  29. package/dist/commands/data-inventory.d.ts +9 -0
  30. package/dist/commands/data-inventory.js +24 -0
  31. package/dist/commands/deposit.d.ts +10 -0
  32. package/dist/commands/deposit.js +222 -0
  33. package/dist/commands/doctor.d.ts +6 -0
  34. package/dist/commands/doctor.js +87 -0
  35. package/dist/commands/funding-browser.d.ts +12 -0
  36. package/dist/commands/funding-browser.js +33 -0
  37. package/dist/commands/guide.d.ts +6 -0
  38. package/dist/commands/guide.js +15 -0
  39. package/dist/commands/home.d.ts +23 -0
  40. package/dist/commands/home.js +210 -0
  41. package/dist/commands/init.d.ts +7 -0
  42. package/dist/commands/init.js +122 -0
  43. package/dist/commands/install.d.ts +9 -0
  44. package/dist/commands/install.js +89 -0
  45. package/dist/commands/interactive.d.ts +17 -0
  46. package/dist/commands/interactive.js +179 -0
  47. package/dist/commands/lessons.d.ts +12 -0
  48. package/dist/commands/lessons.js +33 -0
  49. package/dist/commands/montecarlo.d.ts +19 -0
  50. package/dist/commands/montecarlo.js +168 -0
  51. package/dist/commands/more.d.ts +11 -0
  52. package/dist/commands/more.js +227 -0
  53. package/dist/commands/new.d.ts +14 -0
  54. package/dist/commands/new.js +306 -0
  55. package/dist/commands/pairs.d.ts +22 -0
  56. package/dist/commands/pairs.js +147 -0
  57. package/dist/commands/perp/close.d.ts +12 -0
  58. package/dist/commands/perp/close.js +57 -0
  59. package/dist/commands/perp/long.d.ts +14 -0
  60. package/dist/commands/perp/long.js +38 -0
  61. package/dist/commands/perp/short.d.ts +14 -0
  62. package/dist/commands/perp/short.js +27 -0
  63. package/dist/commands/perp/status.d.ts +9 -0
  64. package/dist/commands/perp/status.js +26 -0
  65. package/dist/commands/portfolio/alerts.d.ts +6 -0
  66. package/dist/commands/portfolio/alerts.js +47 -0
  67. package/dist/commands/portfolio/backtest.d.ts +12 -0
  68. package/dist/commands/portfolio/backtest.js +178 -0
  69. package/dist/commands/portfolio/create.d.ts +7 -0
  70. package/dist/commands/portfolio/create.js +195 -0
  71. package/dist/commands/portfolio/start.d.ts +9 -0
  72. package/dist/commands/portfolio/start.js +64 -0
  73. package/dist/commands/portfolio/status.d.ts +6 -0
  74. package/dist/commands/portfolio/status.js +128 -0
  75. package/dist/commands/portfolio/stop.d.ts +6 -0
  76. package/dist/commands/portfolio/stop.js +81 -0
  77. package/dist/commands/portfolio-backtest.d.ts +13 -0
  78. package/dist/commands/portfolio-backtest.js +37 -0
  79. package/dist/commands/portfolio-matrix.d.ts +12 -0
  80. package/dist/commands/portfolio-matrix.js +30 -0
  81. package/dist/commands/quick-test.d.ts +17 -0
  82. package/dist/commands/quick-test.js +45 -0
  83. package/dist/commands/research.d.ts +57 -0
  84. package/dist/commands/research.js +1976 -0
  85. package/dist/commands/scout.d.ts +14 -0
  86. package/dist/commands/scout.js +184 -0
  87. package/dist/commands/serve.d.ts +9 -0
  88. package/dist/commands/serve.js +1176 -0
  89. package/dist/commands/setup/proxy.d.ts +10 -0
  90. package/dist/commands/setup/proxy.js +267 -0
  91. package/dist/commands/spot/buy.d.ts +14 -0
  92. package/dist/commands/spot/buy.js +38 -0
  93. package/dist/commands/spot/sell.d.ts +14 -0
  94. package/dist/commands/spot/sell.js +39 -0
  95. package/dist/commands/strategies/list.d.ts +6 -0
  96. package/dist/commands/strategies/list.js +34 -0
  97. package/dist/commands/sweep.d.ts +19 -0
  98. package/dist/commands/sweep.js +137 -0
  99. package/dist/commands/sync.d.ts +17 -0
  100. package/dist/commands/sync.js +54 -0
  101. package/dist/commands/test-trade.d.ts +6 -0
  102. package/dist/commands/test-trade.js +97 -0
  103. package/dist/commands/trade.d.ts +26 -0
  104. package/dist/commands/trade.js +274 -0
  105. package/dist/commands/transfer.d.ts +13 -0
  106. package/dist/commands/transfer.js +65 -0
  107. package/dist/commands/verify.d.ts +16 -0
  108. package/dist/commands/verify.js +38 -0
  109. package/dist/commands/walkforward.d.ts +20 -0
  110. package/dist/commands/walkforward.js +191 -0
  111. package/dist/commands/withdraw.d.ts +12 -0
  112. package/dist/commands/withdraw.js +55 -0
  113. package/dist/commands/workbench-create.d.ts +13 -0
  114. package/dist/commands/workbench-create.js +39 -0
  115. package/dist/lib/account-mode.d.ts +44 -0
  116. package/dist/lib/account-mode.js +96 -0
  117. package/dist/lib/analyzer.d.ts +4 -0
  118. package/dist/lib/analyzer.js +62 -0
  119. package/dist/lib/base-command.d.ts +35 -0
  120. package/dist/lib/base-command.js +49 -0
  121. package/dist/lib/credentials.d.ts +46 -0
  122. package/dist/lib/credentials.js +137 -0
  123. package/dist/lib/engine-passthrough.d.ts +28 -0
  124. package/dist/lib/engine-passthrough.js +60 -0
  125. package/dist/lib/fees.d.ts +52 -0
  126. package/dist/lib/fees.js +97 -0
  127. package/dist/lib/python-bridge.d.ts +24 -0
  128. package/dist/lib/python-bridge.js +182 -0
  129. package/dist/lib/setup-status.d.ts +32 -0
  130. package/dist/lib/setup-status.js +121 -0
  131. package/dist/lib/status-footer.d.ts +35 -0
  132. package/dist/lib/status-footer.js +101 -0
  133. package/dist/lib/tui.d.ts +130 -0
  134. package/dist/lib/tui.js +300 -0
  135. package/dist/lib/walletconnect.d.ts +70 -0
  136. package/dist/lib/walletconnect.js +407 -0
  137. 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
+ }