@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,283 @@
1
+ import { Flags, Args } from '@oclif/core';
2
+ import { GatedCommand } from '../lib/base-command.js';
3
+ import * as fs from 'node:fs';
4
+ import { runEngine } from '../lib/python-bridge.js';
5
+ import { analyzeBacktest } from '../lib/analyzer.js';
6
+ // ANSI color helpers
7
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
8
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
9
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
10
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
11
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
12
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
13
+ function colorNum(val, suffix = '') {
14
+ const str = `${val}${suffix}`;
15
+ if (val > 0)
16
+ return green(str);
17
+ if (val < 0)
18
+ return red(str);
19
+ return yellow(str);
20
+ }
21
+ export default class Backtest extends GatedCommand {
22
+ static description = 'Run a backtest on cached candle data';
23
+ static examples = [
24
+ '$ rift backtest trend_follow --pair BTC --tf 4h',
25
+ '$ rift backtest my_strategy --pair BTC --tf 1h --equity 50000',
26
+ ];
27
+ static args = {
28
+ strategy: Args.string({ description: 'Strategy name', required: true }),
29
+ };
30
+ static flags = {
31
+ pair: Flags.string({ description: 'Trading pair', default: 'BTC-PERP' }),
32
+ tf: Flags.string({ description: 'Timeframe (auto-detected from strategy if omitted)' }),
33
+ equity: Flags.integer({ description: 'Starting equity in USDC', default: 10000 }),
34
+ leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
35
+ export: Flags.string({ description: 'Export results to file (csv or json)', options: ['csv', 'json'] }),
36
+ analyze: Flags.boolean({ description: 'AI-powered analysis of backtest results', default: false }),
37
+ 'all-pairs': Flags.boolean({ description: 'Run across top pairs and rank results', default: false }),
38
+ top: Flags.integer({ description: 'Number of top pairs for --all-pairs', default: 10 }),
39
+ };
40
+ async run() {
41
+ const { args, flags } = await this.parse(Backtest);
42
+ const tfDisplay = flags.tf || 'auto';
43
+ const mode = flags['all-pairs'] ? `top ${flags.top} pairs` : flags.pair;
44
+ this.log('');
45
+ this.log(dim(` Backtesting ${bold(args.strategy)} on ${mode} ${tfDisplay}...`));
46
+ this.log('');
47
+ const engineArgs = [
48
+ args.strategy,
49
+ '--pair', flags.pair,
50
+ '--equity', String(flags.equity),
51
+ '--leverage', String(flags.leverage),
52
+ ];
53
+ if (flags.tf)
54
+ engineArgs.push('--tf', flags.tf);
55
+ if (flags['all-pairs']) {
56
+ engineArgs.push('--all-pairs');
57
+ engineArgs.push('--top', String(flags.top));
58
+ }
59
+ let resultMsg = null;
60
+ await runEngine('backtest', engineArgs, (msg) => {
61
+ if (msg.type === 'progress') {
62
+ if (msg.equity != null) {
63
+ const bar = this.progressBar(msg.pct);
64
+ process.stdout.write(`\r ${bar} ${String(msg.pct).padStart(3)}% equity: $${msg.equity}`);
65
+ }
66
+ else {
67
+ process.stdout.write(`\r ${dim(String(msg.msg || ''))}${''.padEnd(20)}`);
68
+ }
69
+ }
70
+ else if (msg.type === 'result') {
71
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
72
+ if (msg.command === 'backtest-all-pairs') {
73
+ this.renderAllPairs(msg);
74
+ }
75
+ else {
76
+ resultMsg = msg;
77
+ this.renderResult(msg);
78
+ }
79
+ }
80
+ else if (msg.type === 'error') {
81
+ this.error(msg.msg);
82
+ }
83
+ });
84
+ // Handle export
85
+ if (flags.export && resultMsg) {
86
+ this.exportResults(resultMsg, flags.export, args.strategy, flags.pair, flags.tf);
87
+ }
88
+ // Handle AI analysis
89
+ if (flags.analyze && resultMsg) {
90
+ await this.runAnalysis(resultMsg);
91
+ }
92
+ }
93
+ async runAnalysis(msg) {
94
+ this.log('');
95
+ this.log(dim(' ── AI Analysis ──'));
96
+ this.log('');
97
+ try {
98
+ this.log(dim(' Thinking...'));
99
+ const analysis = await analyzeBacktest(msg);
100
+ // Move cursor up to overwrite "Thinking..."
101
+ process.stdout.write('\x1b[1A\x1b[2K');
102
+ // Print analysis with nice formatting
103
+ const lines = analysis.split('\n');
104
+ for (const line of lines) {
105
+ this.log(` ${line}`);
106
+ }
107
+ this.log('');
108
+ }
109
+ catch (error) {
110
+ process.stdout.write('\x1b[1A\x1b[2K');
111
+ const errMsg = error.message || String(error);
112
+ if (errMsg.includes('api_key') || errMsg.includes('API key') || errMsg.includes('authentication')) {
113
+ this.log(` ${red('✘')} No API key configured.`);
114
+ this.log('');
115
+ this.log(` ${dim('Set your Anthropic API key:')}`);
116
+ this.log(` ${cyan('rift config set ai.api_key sk-ant-...')}`);
117
+ this.log(` ${dim('Or set environment variable:')}`);
118
+ this.log(` ${cyan('export RIFT_AI_API_KEY=sk-ant-...')}`);
119
+ }
120
+ else {
121
+ this.log(` ${red('✘')} Analysis failed: ${errMsg.split('\n')[0]}`);
122
+ }
123
+ this.log('');
124
+ }
125
+ }
126
+ exportResults(msg, format, strategy, pair, tf) {
127
+ const exportData = msg.export;
128
+ if (!exportData) {
129
+ this.log(dim(' No export data available.'));
130
+ return;
131
+ }
132
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
133
+ const baseName = `rift-${strategy}-${pair}-${tf}-${timestamp}`;
134
+ if (format === 'json') {
135
+ const filePath = `${baseName}.json`;
136
+ fs.writeFileSync(filePath, JSON.stringify(exportData, null, 2));
137
+ this.log(` ${green('✔')} Exported to ${bold(filePath)}`);
138
+ }
139
+ else if (format === 'csv') {
140
+ // Metrics CSV
141
+ const metricsPath = `${baseName}-metrics.csv`;
142
+ const { trades, equity_curve, ...metrics } = exportData;
143
+ const metricsHeaders = Object.keys(metrics).join(',');
144
+ const metricsValues = Object.values(metrics).join(',');
145
+ fs.writeFileSync(metricsPath, `${metricsHeaders}\n${metricsValues}\n`);
146
+ // Trades CSV
147
+ const tradesPath = `${baseName}-trades.csv`;
148
+ if (trades && trades.length > 0) {
149
+ const tradeHeaders = Object.keys(trades[0]).join(',');
150
+ const tradeRows = trades.map((t) => Object.values(t).join(',')).join('\n');
151
+ fs.writeFileSync(tradesPath, `${tradeHeaders}\n${tradeRows}\n`);
152
+ }
153
+ else {
154
+ fs.writeFileSync(tradesPath, 'No trades\n');
155
+ }
156
+ this.log(` ${green('✔')} Exported to:`);
157
+ this.log(` ${bold(metricsPath)}`);
158
+ this.log(` ${bold(tradesPath)}`);
159
+ }
160
+ this.log('');
161
+ }
162
+ renderResult(msg) {
163
+ const totalReturn = msg.total_return_pct;
164
+ const maxDD = msg.max_drawdown_pct;
165
+ const sharpe = msg.sharpe_ratio;
166
+ const pf = msg.profit_factor;
167
+ const winRate = msg.win_rate;
168
+ // Equity curve chart
169
+ const chart = msg.chart;
170
+ if (chart && chart.length > 0) {
171
+ this.log(dim(' ── Equity Curve ──'));
172
+ this.log('');
173
+ for (const line of chart) {
174
+ // Color the bars based on position relative to initial equity
175
+ this.log(` ${cyan(line)}`);
176
+ }
177
+ this.log('');
178
+ }
179
+ // Results box
180
+ const w = 50;
181
+ const hr = '─'.repeat(w - 2);
182
+ this.log(` ${dim('┌' + hr + '┐')}`);
183
+ this.log(` ${dim('│')} ${bold('BACKTEST RESULTS')}${' '.repeat(w - 19)}${dim('│')}`);
184
+ this.log(` ${dim('├' + hr + '┤')}`);
185
+ this.log(this.row('Strategy', String(msg.strategy), w));
186
+ this.log(this.row('Pair', String(msg.pair), w));
187
+ this.log(this.row('Interval', String(msg.interval), w));
188
+ this.log(` ${dim('├' + hr + '┤')}`);
189
+ this.log(this.row('Initial', `$${Number(msg.initial_equity).toLocaleString()}`, w));
190
+ this.log(this.row('Final', `$${Number(msg.final_equity).toLocaleString()}`, w));
191
+ this.log(this.rowColored('Return', totalReturn, '%', w));
192
+ this.log(` ${dim('├' + hr + '┤')}`);
193
+ this.log(this.row('Trades', String(msg.num_trades), w));
194
+ this.log(this.row('Win Rate', `${winRate}%`, w, winRate >= 50 ? 'green' : winRate >= 40 ? 'yellow' : 'red'));
195
+ this.log(this.row('Wins / Losses', `${msg.wins} / ${msg.losses}`, w));
196
+ this.log(this.rowColored('Avg Win', msg.avg_win_pct, '%', w));
197
+ this.log(this.rowColored('Avg Loss', msg.avg_loss_pct, '%', w));
198
+ this.log(this.rowColored('Best Trade', msg.best_trade_pct, '%', w));
199
+ this.log(this.rowColored('Worst Trade', msg.worst_trade_pct, '%', w));
200
+ this.log(` ${dim('├' + hr + '┤')}`);
201
+ this.log(this.rowColored('Max Drawdown', maxDD, '%', w));
202
+ this.log(this.row('Sharpe Ratio', String(sharpe), w, sharpe > 1 ? 'green' : sharpe > 0 ? 'yellow' : 'red'));
203
+ this.log(this.row('Profit Factor', String(pf), w, pf > 1.5 ? 'green' : pf > 1 ? 'yellow' : 'red'));
204
+ this.log(this.row('Max Consec Wins', String(msg.max_consec_wins), w));
205
+ this.log(this.row('Max Consec Losses', String(msg.max_consec_losses), w));
206
+ // Funding info
207
+ const totalFunding = msg.total_funding;
208
+ if (totalFunding !== 0 && totalFunding != null) {
209
+ this.log(` ${dim('├' + hr + '┤')}`);
210
+ this.log(this.rowColored('Funding P&L', totalFunding, '', w));
211
+ }
212
+ this.log(` ${dim('└' + hr + '┘')}`);
213
+ this.log('');
214
+ }
215
+ row(label, value, width, color) {
216
+ const labelStr = ` ${label}:`;
217
+ // Strip ANSI for length calc
218
+ const cleanVal = value.replace(/\x1b\[[0-9;]*m/g, '');
219
+ const padding = width - labelStr.length - cleanVal.length - 3;
220
+ let coloredVal = value;
221
+ if (color === 'green')
222
+ coloredVal = green(value);
223
+ else if (color === 'red')
224
+ coloredVal = red(value);
225
+ else if (color === 'yellow')
226
+ coloredVal = yellow(value);
227
+ return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredVal} ${dim('│')}`;
228
+ }
229
+ rowColored(label, value, suffix, width) {
230
+ const coloredStr = colorNum(value, suffix);
231
+ const cleanStr = `${value}${suffix}`;
232
+ const labelStr = ` ${label}:`;
233
+ const padding = width - labelStr.length - cleanStr.length - 3;
234
+ return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredStr} ${dim('│')}`;
235
+ }
236
+ renderAllPairs(msg) {
237
+ const results = msg.results;
238
+ const strategy = msg.strategy;
239
+ const interval = msg.interval;
240
+ this.log(bold(` ${strategy} across ${results.length} pairs (${interval})`));
241
+ this.log('');
242
+ if (!results || results.length === 0) {
243
+ this.log(dim(' No results.'));
244
+ return;
245
+ }
246
+ // Table header
247
+ const hdr = ` ${dim('│')} ${'#'.padEnd(3)} ${'Pair'.padEnd(10)} ${'Return'.padEnd(10)} ${'Sharpe'.padEnd(10)} ${'PF'.padEnd(8)} ${'MaxDD'.padEnd(10)} ${'Win%'.padEnd(8)} ${'Trades'.padEnd(8)} ${'Funding'.padEnd(10)} ${dim('│')}`;
248
+ const hrLen = hdr.replace(/\x1b\[[0-9;]*m/g, '').length;
249
+ const hr = ` ${dim('─'.repeat(hrLen - 2))}`;
250
+ this.log(hr);
251
+ this.log(hdr);
252
+ this.log(hr);
253
+ for (let i = 0; i < results.length; i++) {
254
+ const r = results[i];
255
+ const retStr = `${r.return_pct}%`;
256
+ const ddStr = `${r.max_drawdown_pct}%`;
257
+ let row = ` ${dim('│')} ${bold(String(i + 1).padEnd(3))} ${r.pair.padEnd(10)} `;
258
+ row += `${colorNum(r.return_pct, '%')}${' '.repeat(Math.max(1, 10 - retStr.length))} `;
259
+ row += `${colorNum(r.sharpe)}${' '.repeat(Math.max(1, 10 - String(r.sharpe).length))} `;
260
+ row += `${String(r.profit_factor).padEnd(8)} `;
261
+ row += `${colorNum(r.max_drawdown_pct, '%')}${' '.repeat(Math.max(1, 10 - ddStr.length))} `;
262
+ row += `${String(r.win_rate).padEnd(8)} `;
263
+ row += `${String(r.num_trades).padEnd(8)} `;
264
+ row += `${colorNum(r.total_funding)}${' '.repeat(Math.max(1, 10 - String(r.total_funding).length))} `;
265
+ row += dim('│');
266
+ this.log(row);
267
+ }
268
+ this.log(hr);
269
+ this.log('');
270
+ // Best pair
271
+ if (results.length > 1) {
272
+ const best = results[0];
273
+ this.log(` ${green('★')} Best by Sharpe: ${bold(best.pair)} (${colorNum(best.return_pct, '% return')}, Sharpe ${colorNum(best.sharpe)})`);
274
+ this.log('');
275
+ }
276
+ }
277
+ progressBar(pct) {
278
+ const width = 25;
279
+ const filled = Math.round(width * pct / 100);
280
+ const empty = width - filled;
281
+ return dim('[') + green('█'.repeat(filled)) + dim('░'.repeat(empty)) + dim(']');
282
+ }
283
+ }
@@ -0,0 +1,11 @@
1
+ import { GatedCommand } from '../../lib/base-command.js';
2
+ export default class CollectStart extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ symbols: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'ob-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,78 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { GatedCommand } from '../../lib/base-command.js';
3
+ import { runEngine } from '../../lib/python-bridge.js';
4
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
5
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
6
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
7
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
8
+ export default class CollectStart extends GatedCommand {
9
+ static description = 'Start the persistent data collector — builds historical data 24/7';
10
+ static examples = [
11
+ '$ rift collect start',
12
+ '$ rift collect start --symbols BTC,ETH,SOL,HYPE,DOGE --tf 1h,15m',
13
+ '$ rift collect start --ob-interval 30',
14
+ ];
15
+ static flags = {
16
+ symbols: Flags.string({ description: 'Comma-separated symbols to collect', default: 'BTC,ETH,SOL,HYPE' }),
17
+ tf: Flags.string({ description: 'Comma-separated timeframes for candles', default: '1h' }),
18
+ 'ob-interval': Flags.integer({ description: 'Seconds between order book snapshots', default: 60 }),
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(CollectStart);
22
+ const symbols = flags.symbols.split(',').map(s => s.trim());
23
+ const timeframes = flags.tf.split(',').map(t => t.trim());
24
+ this.log('');
25
+ this.log(` ${bold('RIFT Data Collector')}`);
26
+ this.log(` ${dim('─'.repeat(55))}`);
27
+ this.log(` Symbols: ${symbols.map(s => bold(s)).join(', ')}`);
28
+ this.log(` Timeframes: ${timeframes.join(', ')}`);
29
+ this.log(` Order book: every ${flags['ob-interval']}s`);
30
+ this.log(` ${dim('─'.repeat(55))}`);
31
+ this.log('');
32
+ this.log(` ${dim('Collecting:')}`);
33
+ this.log(` ${green('•')} Candle data (OHLCV) for ${symbols.length} pairs`);
34
+ this.log(` ${green('•')} Funding rates (hourly)`);
35
+ this.log(` ${green('•')} L2 order book snapshots (${symbols.length} × ${Math.round(86400 / flags['ob-interval'])}/day)`);
36
+ this.log(` ${green('•')} Market data (mid prices, funding)`);
37
+ this.log('');
38
+ this.log(` ${dim('Data stored at: ~/.rift/data/ and ~/.rift/collector.db')}`);
39
+ this.log(` ${dim('The longer this runs, the more valuable your data becomes.')}`);
40
+ this.log(` ${dim('Press Ctrl+C to stop')}`);
41
+ this.log('');
42
+ const engineArgs = [
43
+ '--symbols', flags.symbols,
44
+ '--tf', flags.tf,
45
+ '--ob-interval', String(flags['ob-interval']),
46
+ ];
47
+ try {
48
+ await runEngine('collect', engineArgs, (msg) => {
49
+ if (msg.type === 'status') {
50
+ this.log(` ${green('✔')} ${msg.msg}`);
51
+ }
52
+ else if (msg.type === 'heartbeat') {
53
+ const stats = msg.stats;
54
+ const dbSize = msg.db_size_mb;
55
+ process.stdout.write(`\r ${dim(`Uptime: ${msg.uptime_minutes}min | OB: ${stats?.orderbook || 0} | Market: ${stats?.market || 0} | Candles: ${stats?.candles || 0} | DB: ${dbSize}MB`)}${''.padEnd(10)}`);
56
+ }
57
+ else if (msg.type === 'shutdown') {
58
+ process.stdout.write('\r' + ' '.repeat(100) + '\r');
59
+ this.log('');
60
+ this.log(` ${dim('Collector stopped.')}`);
61
+ const stats = msg.stats;
62
+ if (stats) {
63
+ this.log(` ${dim(`Total collected: ${stats.orderbook} orderbook, ${stats.market} market, ${stats.candles} candles, ${stats.funding} funding`)}`);
64
+ }
65
+ this.log('');
66
+ }
67
+ else if (msg.type === 'error') {
68
+ this.log(` ${dim(`Error: ${msg.msg}`)}`);
69
+ }
70
+ });
71
+ }
72
+ catch (error) {
73
+ if (!error.message.includes('null') && !error.message.includes('SIGTERM')) {
74
+ this.log(` Error: ${error.message.split('\n')[0]}`);
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,6 @@
1
+ import { GatedCommand } from '../../lib/base-command.js';
2
+ export default class CollectStatus extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,60 @@
1
+ import { GatedCommand } from '../../lib/base-command.js';
2
+ import { runEngine } from '../../lib/python-bridge.js';
3
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
4
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
5
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
6
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
7
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
8
+ export default class CollectStatus extends GatedCommand {
9
+ static description = 'Check data collector status and statistics';
10
+ static examples = [
11
+ '$ rift collect status',
12
+ ];
13
+ async run() {
14
+ this.log('');
15
+ this.log(` ${bold('Data Collector Status')}`);
16
+ this.log(` ${dim('─'.repeat(45))}`);
17
+ this.log('');
18
+ await runEngine('collect-status', [], (msg) => {
19
+ if (msg.type === 'result') {
20
+ const running = msg.running;
21
+ const dbExists = msg.db_exists;
22
+ const dbSize = msg.db_size_mb;
23
+ const obCount = msg.orderbook_count;
24
+ const marketCount = msg.market_count;
25
+ const oldest = msg.oldest_data;
26
+ const newest = msg.newest_data;
27
+ const symbols = msg.symbols;
28
+ if (running) {
29
+ this.log(` ${green('●')} Collector is ${green('running')} (PID ${msg.pid})`);
30
+ }
31
+ else {
32
+ this.log(` ${red('●')} Collector is ${red('stopped')}`);
33
+ this.log(` ${dim('Start with: rift collect start')}`);
34
+ }
35
+ this.log('');
36
+ if (dbExists) {
37
+ this.log(` ${bold('Database:')} ${dim(`~/.rift/collector.db (${dbSize} MB)`)}`);
38
+ this.log(` ${bold('Order book snapshots:')} ${obCount.toLocaleString()}`);
39
+ this.log(` ${bold('Market snapshots:')} ${marketCount.toLocaleString()}`);
40
+ if (symbols && symbols.length > 0) {
41
+ this.log(` ${bold('Symbols:')} ${symbols.join(', ')}`);
42
+ }
43
+ if (oldest && newest) {
44
+ this.log(` ${bold('Data range:')} ${oldest} → ${newest}`);
45
+ // Calculate days of data
46
+ const oldDate = new Date(oldest);
47
+ const newDate = new Date(newest);
48
+ const days = Math.round((newDate.getTime() - oldDate.getTime()) / (1000 * 60 * 60 * 24));
49
+ this.log(` ${bold('Days collected:')} ${days}`);
50
+ }
51
+ }
52
+ else {
53
+ this.log(` ${yellow('!')} No collector database found`);
54
+ this.log(` ${dim('Start collecting: rift collect start')}`);
55
+ }
56
+ this.log('');
57
+ }
58
+ });
59
+ }
60
+ }
@@ -0,0 +1,16 @@
1
+ import { GatedCommand } from '../lib/base-command.js';
2
+ export default class Compare extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ strategies: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ pair: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ equity: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ leverage: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ private renderComparison;
16
+ }
@@ -0,0 +1,130 @@
1
+ import { Flags, Args } from '@oclif/core';
2
+ import { GatedCommand } from '../lib/base-command.js';
3
+ import { runEngine } from '../lib/python-bridge.js';
4
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
5
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
6
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
7
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
8
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
9
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
10
+ function colorNum(val, suffix = '') {
11
+ const str = `${val}${suffix}`;
12
+ if (val > 0)
13
+ return green(str);
14
+ if (val < 0)
15
+ return red(str);
16
+ return yellow(str);
17
+ }
18
+ function bestOf(results, key, higher = true) {
19
+ const values = results.map(r => r[key]);
20
+ return higher ? Math.max(...values) : Math.min(...values);
21
+ }
22
+ export default class Compare extends GatedCommand {
23
+ static description = 'Compare multiple strategies head-to-head';
24
+ static examples = [
25
+ '$ rift compare trend_follow,my_strategy --pair BTC --tf 4h',
26
+ ];
27
+ static args = {
28
+ strategies: Args.string({ description: 'Comma-separated strategy names', required: true }),
29
+ };
30
+ static flags = {
31
+ pair: Flags.string({ description: 'Trading pair', default: 'BTC-PERP' }),
32
+ tf: Flags.string({ description: 'Timeframe', default: '15m' }),
33
+ equity: Flags.integer({ description: 'Starting equity in USDC', default: 10000 }),
34
+ leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
35
+ };
36
+ async run() {
37
+ const { args, flags } = await this.parse(Compare);
38
+ this.log('');
39
+ this.log(dim(` Comparing strategies on ${flags.pair} ${flags.tf}...`));
40
+ this.log('');
41
+ const engineArgs = [
42
+ args.strategies,
43
+ '--pair', flags.pair,
44
+ '--tf', flags.tf,
45
+ '--equity', String(flags.equity),
46
+ '--leverage', String(flags.leverage),
47
+ ];
48
+ await runEngine('compare', engineArgs, (msg) => {
49
+ if (msg.type === 'progress' && msg.msg) {
50
+ process.stdout.write(`\r ${dim(String(msg.msg))}${''.padEnd(40)}`);
51
+ }
52
+ else if (msg.type === 'result') {
53
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
54
+ this.renderComparison(msg.results);
55
+ }
56
+ else if (msg.type === 'error') {
57
+ this.error(msg.msg);
58
+ }
59
+ });
60
+ }
61
+ renderComparison(results) {
62
+ if (!results || results.length === 0) {
63
+ this.log(' No results.');
64
+ return;
65
+ }
66
+ // Column widths
67
+ const labelW = 18;
68
+ const colW = 18;
69
+ // Header
70
+ const hr = '─'.repeat(labelW + colW * results.length + 3);
71
+ this.log(` ${dim(hr)}`);
72
+ let header = ` ${dim('│')} ${'Metric'.padEnd(labelW)}`;
73
+ for (const r of results) {
74
+ header += `${dim('│')} ${bold(cyan(String(r.strategy).padEnd(colW - 2)))} `;
75
+ }
76
+ header += dim('│');
77
+ this.log(header);
78
+ this.log(` ${dim(hr)}`);
79
+ // Metrics rows
80
+ const metrics = [
81
+ { label: 'Return', key: 'total_return_pct', suffix: '%', higher: true },
82
+ { label: 'Final Equity', key: 'final_equity', suffix: '', higher: true, format: v => `$${Number(v).toLocaleString()}` },
83
+ { label: 'Trades', key: 'num_trades', suffix: '', higher: false },
84
+ { label: 'Win Rate', key: 'win_rate', suffix: '%', higher: true },
85
+ { label: 'Avg Win', key: 'avg_win_pct', suffix: '%', higher: true },
86
+ { label: 'Avg Loss', key: 'avg_loss_pct', suffix: '%', higher: false },
87
+ { label: 'Max Drawdown', key: 'max_drawdown_pct', suffix: '%', higher: false },
88
+ { label: 'Sharpe Ratio', key: 'sharpe_ratio', suffix: '', higher: true },
89
+ { label: 'Profit Factor', key: 'profit_factor', suffix: '', higher: true },
90
+ ];
91
+ for (const m of metrics) {
92
+ const best = bestOf(results, m.key, m.higher);
93
+ let row = ` ${dim('│')} ${m.label.padEnd(labelW)}`;
94
+ for (const r of results) {
95
+ const val = r[m.key];
96
+ let display;
97
+ if (m.format) {
98
+ display = m.format(val);
99
+ }
100
+ else {
101
+ display = `${val}${m.suffix}`;
102
+ }
103
+ // Highlight best
104
+ const isBest = val === best && results.length > 1;
105
+ const cleanLen = display.replace(/\x1b\[[0-9;]*m/g, '').length;
106
+ if (m.key === 'total_return_pct' || m.key === 'max_drawdown_pct' || m.key === 'avg_win_pct' || m.key === 'avg_loss_pct' || m.key === 'sharpe_ratio') {
107
+ display = colorNum(val, m.suffix);
108
+ }
109
+ if (isBest) {
110
+ display = bold(display) + ' ★';
111
+ row += `${dim('│')} ${display}${' '.repeat(Math.max(0, colW - cleanLen - 4))} `;
112
+ }
113
+ else {
114
+ row += `${dim('│')} ${display}${' '.repeat(Math.max(0, colW - cleanLen - 2))} `;
115
+ }
116
+ }
117
+ row += dim('│');
118
+ this.log(row);
119
+ }
120
+ this.log(` ${dim(hr)}`);
121
+ // Winner
122
+ const bestReturn = bestOf(results, 'total_return_pct', true);
123
+ const winner = results.find(r => r.total_return_pct === bestReturn);
124
+ if (winner && results.length > 1) {
125
+ this.log('');
126
+ this.log(` ${green('★')} Winner by return: ${bold(cyan(winner.strategy))} ${green(`(${winner.total_return_pct}%)`)}`);
127
+ }
128
+ this.log('');
129
+ }
130
+ }
@@ -0,0 +1,16 @@
1
+ import { GatedCommand } from '../lib/base-command.js';
2
+ export default class Config extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static strict: boolean;
6
+ static args: {
7
+ action: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
9
+ value: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
10
+ };
11
+ run(): Promise<void>;
12
+ private listConfig;
13
+ private printObject;
14
+ private getConfig;
15
+ private setConfig;
16
+ }