@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,1976 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { runEngine } from '../lib/python-bridge.js';
|
|
4
|
+
import { green, red, yellow, cyan, bold, dim, colorNum, gradeColor, boxRow, boxTop, boxBottom, boxDivider, boldBoxRow, boldBoxTop, boldBoxBottom, boldBoxDivider, resultRow, padEndVis, ask, } from '../lib/tui.js';
|
|
5
|
+
// Market type color coding
|
|
6
|
+
const marketColor = (m) => {
|
|
7
|
+
if (m === 'All Conditions')
|
|
8
|
+
return cyan(m);
|
|
9
|
+
if (m === 'Adaptive')
|
|
10
|
+
return cyan(m);
|
|
11
|
+
if (m.startsWith('Sideways'))
|
|
12
|
+
return dim(m);
|
|
13
|
+
if (m.includes('Trend'))
|
|
14
|
+
return yellow(m);
|
|
15
|
+
if (m === 'Mean-Reverting')
|
|
16
|
+
return yellow(m);
|
|
17
|
+
if (m === 'Breakout')
|
|
18
|
+
return yellow(m);
|
|
19
|
+
return dim(m);
|
|
20
|
+
};
|
|
21
|
+
// Strategy catalog with descriptions
|
|
22
|
+
// Strategies discovered dynamically from engine — users create their own
|
|
23
|
+
const STRATEGIES = [];
|
|
24
|
+
const TEMPLATES = [
|
|
25
|
+
{ key: '1', label: 'Funding template', desc: 'funding rate capture with EMA trend filter', template: 'funding' },
|
|
26
|
+
{ key: '2', label: 'VWAP reversion', desc: 'VWAP deviation mean reversion', template: 'vwap_reversion' },
|
|
27
|
+
{ key: '3', label: 'Trend following', desc: 'EMA crossover with ADX filter', template: 'trend_follow' },
|
|
28
|
+
{ key: '4', label: 'Blank', desc: 'empty strategy, build from scratch', template: 'blank' },
|
|
29
|
+
];
|
|
30
|
+
export default class Research extends GatedCommand {
|
|
31
|
+
static description = 'Research Lab — discover, test, build, optimize, and compare strategies';
|
|
32
|
+
static examples = [
|
|
33
|
+
'$ rift research',
|
|
34
|
+
'$ rift research my_strategy --pair SUI',
|
|
35
|
+
];
|
|
36
|
+
static args = {
|
|
37
|
+
strategy: Args.string({ description: 'Strategy name (interactive if omitted)', required: false }),
|
|
38
|
+
};
|
|
39
|
+
static flags = {
|
|
40
|
+
pair: Flags.string({ description: 'Ticker symbol (e.g. BTC, ETH, SOL)', default: 'BTC' }),
|
|
41
|
+
tf: Flags.string({ description: 'Timeframe (auto-detected if omitted)' }),
|
|
42
|
+
equity: Flags.integer({ description: 'Starting equity', default: 10000 }),
|
|
43
|
+
};
|
|
44
|
+
async run() {
|
|
45
|
+
const { args, flags } = await this.parse(Research);
|
|
46
|
+
if (args.strategy) {
|
|
47
|
+
return this.runPipeline(args.strategy, flags.pair, flags.tf, flags.equity);
|
|
48
|
+
}
|
|
49
|
+
return this.mainMenu();
|
|
50
|
+
}
|
|
51
|
+
// ═══════════════════════════════════════════
|
|
52
|
+
// MAIN MENU
|
|
53
|
+
// ═══════════════════════════════════════════
|
|
54
|
+
async mainMenu() {
|
|
55
|
+
// Gather live stats for the dashboard header
|
|
56
|
+
let customCount = 0;
|
|
57
|
+
let testCount = 0;
|
|
58
|
+
try {
|
|
59
|
+
await runEngine('workbench-list', [], (msg) => {
|
|
60
|
+
if (msg.type === 'result') {
|
|
61
|
+
customCount = (msg.strategies || []).length;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch { /* empty */ }
|
|
66
|
+
// Find best grade and return from validated strategies (if any).
|
|
67
|
+
// STRATEGIES is currently empty by design (users discover strategies
|
|
68
|
+
// dynamically); guard against an empty-array reduce TypeError.
|
|
69
|
+
const bestStrategy = STRATEGIES.length > 0
|
|
70
|
+
? STRATEGIES.reduce((a, b) => a.ret > b.ret ? a : b)
|
|
71
|
+
: null;
|
|
72
|
+
const iw = 51; // inner width
|
|
73
|
+
this.log('');
|
|
74
|
+
this.log(boldBoxTop(iw));
|
|
75
|
+
this.log(boldBoxRow(iw)(` ${bold('RESEARCH LAB')}`));
|
|
76
|
+
this.log(boldBoxDivider(iw));
|
|
77
|
+
// Stat boxes — 3 columns
|
|
78
|
+
const col = 16; // column width
|
|
79
|
+
const bestVal = bestStrategy
|
|
80
|
+
? `${gradeColor(bestStrategy.grade)} ${green('+' + bestStrategy.ret + '%')}`
|
|
81
|
+
: dim('—');
|
|
82
|
+
const valCount = bold(String(STRATEGIES.length));
|
|
83
|
+
const custCount = bold(String(customCount));
|
|
84
|
+
// Labels
|
|
85
|
+
this.log(boldBoxRow(iw)(` ${padEndVis(dim('BEST'), col)}${padEndVis(dim('VALIDATED'), col)}${dim('CUSTOM')}`));
|
|
86
|
+
// Values
|
|
87
|
+
this.log(boldBoxRow(iw)(` ${padEndVis(bestVal, col)}${padEndVis(valCount, col)}${custCount}`));
|
|
88
|
+
this.log(boldBoxDivider(iw));
|
|
89
|
+
// Menu options — use boldBoxRow for proper alignment
|
|
90
|
+
const brow = boldBoxRow(iw);
|
|
91
|
+
this.log(brow(''));
|
|
92
|
+
this.log(brow(` ${cyan('1')} Test ${dim('validate a strategy')}`));
|
|
93
|
+
this.log(brow(` ${cyan('2')} Explore ${dim('browse what works')}`));
|
|
94
|
+
this.log(brow(` ${cyan('3')} Build ${dim('create in the workbench')}`));
|
|
95
|
+
this.log(brow(` ${cyan('4')} Optimize ${dim('find best parameters')}`));
|
|
96
|
+
this.log(brow(` ${cyan('5')} Compare ${dim('head-to-head showdown')}`));
|
|
97
|
+
this.log(brow(''));
|
|
98
|
+
this.log(brow(` ${cyan('0')} ${dim('Exit')}`));
|
|
99
|
+
this.log(brow(''));
|
|
100
|
+
this.log(boldBoxBottom(iw));
|
|
101
|
+
this.log('');
|
|
102
|
+
this.log(` ${dim('Tip:')} ${cyan('rift more')} ${dim('shows every engine command (102 total)')}`);
|
|
103
|
+
this.log('');
|
|
104
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
105
|
+
switch (choice) {
|
|
106
|
+
case '0':
|
|
107
|
+
case 'q':
|
|
108
|
+
case 'b':
|
|
109
|
+
case 'B':
|
|
110
|
+
return;
|
|
111
|
+
case '1': return this.testMenu();
|
|
112
|
+
case '2': return this.exploreMenu();
|
|
113
|
+
case '3': return this.buildMenu();
|
|
114
|
+
case '4': return this.optimizeMenu();
|
|
115
|
+
case '5': return this.compareMenu();
|
|
116
|
+
default:
|
|
117
|
+
if (choice)
|
|
118
|
+
this.log(dim(' Invalid selection.'));
|
|
119
|
+
return this.mainMenu();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ═══════════════════════════════════════════
|
|
123
|
+
// SHARED STRATEGY PICKER
|
|
124
|
+
// ═══════════════════════════════════════════
|
|
125
|
+
/**
|
|
126
|
+
* Show a numbered list of all strategies (validated + custom).
|
|
127
|
+
* Returns the selected strategy name, or null if cancelled.
|
|
128
|
+
* If multi=true, allows comma-separated selection and returns names joined by comma.
|
|
129
|
+
*/
|
|
130
|
+
async pickStrategy(prompt = 'Select a strategy', multi = false) {
|
|
131
|
+
// Fetch custom workbench strategies
|
|
132
|
+
let customStrategies = [];
|
|
133
|
+
try {
|
|
134
|
+
await runEngine('workbench-list', [], (msg) => {
|
|
135
|
+
if (msg.type === 'result') {
|
|
136
|
+
customStrategies = (msg.strategies || []);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch { /* empty */ }
|
|
141
|
+
this.log('');
|
|
142
|
+
this.log(` ${bold(prompt + ':')}`);
|
|
143
|
+
this.log('');
|
|
144
|
+
// Validated strategies
|
|
145
|
+
const allStrategies = [];
|
|
146
|
+
for (const s of STRATEGIES) {
|
|
147
|
+
allStrategies.push({
|
|
148
|
+
name: s.name,
|
|
149
|
+
desc: s.desc,
|
|
150
|
+
tag: `${gradeColor(s.grade)} ${padEndVis(colorNum(s.ret, '%'), 10)}`,
|
|
151
|
+
market: s.market,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Custom strategies
|
|
155
|
+
for (const s of customStrategies) {
|
|
156
|
+
allStrategies.push({
|
|
157
|
+
name: String(s.name),
|
|
158
|
+
desc: String(s.description || '').slice(0, 40),
|
|
159
|
+
tag: dim(`v${s.version} custom`),
|
|
160
|
+
market: '',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Display
|
|
164
|
+
for (let i = 0; i < allStrategies.length; i++) {
|
|
165
|
+
const s = allStrategies[i];
|
|
166
|
+
const mkt = s.market ? `${padEndVis(marketColor(s.market), 24)} ` : '';
|
|
167
|
+
this.log(` ${cyan(String(i + 1))} ${bold(s.name.padEnd(22))} ${s.tag} ${mkt} ${dim(s.desc)}`);
|
|
168
|
+
}
|
|
169
|
+
this.log(` ${cyan(String(allStrategies.length + 1))} ${dim('Enter custom name')}`);
|
|
170
|
+
this.log('');
|
|
171
|
+
if (multi) {
|
|
172
|
+
this.log(dim(' Enter numbers separated by commas (e.g., 1,2,4)'));
|
|
173
|
+
}
|
|
174
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
175
|
+
if (multi) {
|
|
176
|
+
// Parse comma-separated numbers
|
|
177
|
+
const parts = choice.split(',').map(s => s.trim());
|
|
178
|
+
const names = [];
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
const idx = parseInt(part) - 1;
|
|
181
|
+
if (idx >= 0 && idx < allStrategies.length) {
|
|
182
|
+
names.push(allStrategies[idx].name);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (names.length > 0)
|
|
186
|
+
return names.join(',');
|
|
187
|
+
// Fallback to raw input (they may have typed names)
|
|
188
|
+
return choice || null;
|
|
189
|
+
}
|
|
190
|
+
const idx = parseInt(choice) - 1;
|
|
191
|
+
if (idx >= 0 && idx < allStrategies.length) {
|
|
192
|
+
return allStrategies[idx].name;
|
|
193
|
+
}
|
|
194
|
+
if (parseInt(choice) === allStrategies.length + 1) {
|
|
195
|
+
return await ask(` ${cyan('Strategy name')}: `) || null;
|
|
196
|
+
}
|
|
197
|
+
// If they typed a name directly, use it
|
|
198
|
+
if (choice && isNaN(parseInt(choice)))
|
|
199
|
+
return choice;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// ═══════════════════════════════════════════
|
|
203
|
+
// 1. TEST — Full validation pipeline
|
|
204
|
+
// ═══════════════════════════════════════════
|
|
205
|
+
async testMenu() {
|
|
206
|
+
const strategy = await this.pickStrategy('Test a strategy');
|
|
207
|
+
if (!strategy)
|
|
208
|
+
return this.mainMenu();
|
|
209
|
+
const pair = (await ask(` ${cyan('Ticker')} ${dim('(BTC)')}: `) || 'BTC').replace('-PERP', '').replace('-perp', '').toUpperCase();
|
|
210
|
+
this.log('');
|
|
211
|
+
await this.runPipeline(strategy, pair);
|
|
212
|
+
return this.mainMenu();
|
|
213
|
+
}
|
|
214
|
+
// ═══════════════════════════════════════════
|
|
215
|
+
// 2. EXPLORE — Discovery hub
|
|
216
|
+
// ═══════════════════════════════════════════
|
|
217
|
+
async exploreMenu() {
|
|
218
|
+
const iw = 60;
|
|
219
|
+
const brow = boldBoxRow(iw);
|
|
220
|
+
this.log('');
|
|
221
|
+
this.log(boldBoxTop(iw));
|
|
222
|
+
this.log(brow(` ${bold('EXPLORE')} ${dim('— discover what exists before building')}`));
|
|
223
|
+
this.log(boldBoxDivider(iw));
|
|
224
|
+
this.log(brow(''));
|
|
225
|
+
this.log(brow(` ${cyan('1')} Indicator catalog ${dim('50+ indicators, filterable')}`));
|
|
226
|
+
this.log(brow(` ${cyan('2')} Strategy showcase ${dim('validated + custom')}`));
|
|
227
|
+
this.log(brow(` ${cyan('3')} Market scanner ${dim('rift scout — live opportunities')}`));
|
|
228
|
+
this.log(brow(` ${cyan('4')} Signal forensics ${dim('stats / decay / backfill')}`));
|
|
229
|
+
this.log(brow(` ${cyan('5')} Funding rate browser ${dim('current + 7d + extremes')}`));
|
|
230
|
+
this.log(brow(` ${cyan('6')} Order flow browser ${dim('taker ratio / imbalance / flow')}`));
|
|
231
|
+
this.log(brow(` ${cyan('7')} Cross-asset matrix ${dim('correlation / lead-lag / beta')}`));
|
|
232
|
+
this.log(brow(` ${cyan('8')} Regime browser ${dim('vol + trend regime now & history')}`));
|
|
233
|
+
this.log(brow(''));
|
|
234
|
+
this.log(brow(` ${cyan('b')} Back to Research Lab`));
|
|
235
|
+
this.log(boldBoxBottom(iw));
|
|
236
|
+
this.log('');
|
|
237
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
238
|
+
switch (choice) {
|
|
239
|
+
case '1': return this.indicatorCatalogMenu();
|
|
240
|
+
case '2': return this.strategyShowcaseMenu();
|
|
241
|
+
case '3':
|
|
242
|
+
await this.config.runCommand('scout');
|
|
243
|
+
return this.exploreMenu();
|
|
244
|
+
case '4': return this.signalForensicsMenu();
|
|
245
|
+
case '5': return this.fundingBrowserMenu();
|
|
246
|
+
case '6': return this.orderFlowBrowserMenu();
|
|
247
|
+
case '7': return this.crossAssetMenu();
|
|
248
|
+
case '8': return this.regimeBrowserMenu();
|
|
249
|
+
case 'b':
|
|
250
|
+
case 'B': return this.mainMenu();
|
|
251
|
+
default:
|
|
252
|
+
this.log(dim(' Invalid selection.'));
|
|
253
|
+
return this.exploreMenu();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ─── Strategy showcase (formerly exploreMenu) ─────────────
|
|
257
|
+
async strategyShowcaseMenu() {
|
|
258
|
+
const iw = 56; // inner width for strategy cards
|
|
259
|
+
const row = boxRow(iw);
|
|
260
|
+
// Dynamic discovery — pull whatever's actually registered (shipped OSS
|
|
261
|
+
// + private + workbench customs). No hardcoded list of RIFT-team-only
|
|
262
|
+
// strategies that an OSS user wouldn't have on disk.
|
|
263
|
+
let registered = [];
|
|
264
|
+
try {
|
|
265
|
+
await runEngine('strategies', [], (msg) => {
|
|
266
|
+
if (msg.type === 'result') {
|
|
267
|
+
registered = (msg.strategies || []).map(s => ({
|
|
268
|
+
name: String(s.name),
|
|
269
|
+
doc: s.doc ? String(s.doc) : undefined,
|
|
270
|
+
class: s.class ? String(s.class) : undefined,
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch { /* empty registry handled below */ }
|
|
276
|
+
let customs = [];
|
|
277
|
+
try {
|
|
278
|
+
await runEngine('workbench-list', [], (msg) => {
|
|
279
|
+
if (msg.type === 'result') {
|
|
280
|
+
customs = msg.strategies || [];
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch { /* empty */ }
|
|
285
|
+
this.log('');
|
|
286
|
+
this.log(boldBoxTop(iw + 2));
|
|
287
|
+
this.log(` ${bold('║')} ${'Strategy Explorer'.padStart(Math.floor((iw) / 2) + 9).padEnd(iw + 1)}${bold('║')}`);
|
|
288
|
+
this.log(boldBoxBottom(iw + 2));
|
|
289
|
+
this.log('');
|
|
290
|
+
if (registered.length === 0 && customs.length === 0) {
|
|
291
|
+
this.log(` ${yellow('!')} No strategies registered yet.`);
|
|
292
|
+
this.log('');
|
|
293
|
+
this.log(` ${dim('Get started:')}`);
|
|
294
|
+
this.log(` ${cyan('rift new my-strategy')} ${dim('— scaffold from template')}`);
|
|
295
|
+
this.log(` ${cyan('rift backtest trend_follow --pair BTC --tf 4h')} ${dim('— try the shipped example')}`);
|
|
296
|
+
this.log('');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
if (registered.length > 0) {
|
|
300
|
+
this.log(dim(' Registered (shipped + custom):'));
|
|
301
|
+
this.log('');
|
|
302
|
+
for (const s of registered) {
|
|
303
|
+
const doc = (s.doc || s.class || '').toString().split('\n')[0].slice(0, iw - 4);
|
|
304
|
+
this.log(boxTop(iw));
|
|
305
|
+
this.log(row(`${green('★')} ${bold(s.name)}`));
|
|
306
|
+
if (doc)
|
|
307
|
+
this.log(row(` ${dim(doc)}`));
|
|
308
|
+
this.log(boxBottom(iw));
|
|
309
|
+
}
|
|
310
|
+
this.log('');
|
|
311
|
+
}
|
|
312
|
+
if (customs.length > 0) {
|
|
313
|
+
this.log(dim(' Your workbench strategies:'));
|
|
314
|
+
this.log('');
|
|
315
|
+
for (const s of customs) {
|
|
316
|
+
this.log(` ${cyan(String(s.name).padEnd(22))} v${s.version} ${dim(String(s.description || '').slice(0, 40))}`);
|
|
317
|
+
}
|
|
318
|
+
this.log('');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.log(` ${bold('What next?')}`);
|
|
322
|
+
this.log(` ${cyan('1')} Test a strategy on a specific pair`);
|
|
323
|
+
this.log(` ${cyan('2')} See which pairs work best for a strategy`);
|
|
324
|
+
this.log(` ${cyan('3')} Back to Explore`);
|
|
325
|
+
this.log('');
|
|
326
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
327
|
+
switch (choice) {
|
|
328
|
+
case '1': return this.testMenu();
|
|
329
|
+
case '2': {
|
|
330
|
+
const strat = await this.pickStrategy('Which strategy to test across pairs');
|
|
331
|
+
if (!strat)
|
|
332
|
+
return this.exploreMenu();
|
|
333
|
+
this.log('');
|
|
334
|
+
await this.config.runCommand('backtest', [strat, '--all-pairs', '--top', '10']);
|
|
335
|
+
return this.exploreMenu();
|
|
336
|
+
}
|
|
337
|
+
case '3': return this.exploreMenu();
|
|
338
|
+
default: return this.exploreMenu();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ═══════════════════════════════════════════
|
|
342
|
+
// 3. BUILD — Strategy Workbench
|
|
343
|
+
// ═══════════════════════════════════════════
|
|
344
|
+
async buildMenu() {
|
|
345
|
+
this.log('');
|
|
346
|
+
this.log(` ${bold('╔═══════════════════════════════════════════╗')}`);
|
|
347
|
+
this.log(` ${bold('║ Strategy Workbench ║')}`);
|
|
348
|
+
this.log(` ${bold('╚═══════════════════════════════════════════╝')}`);
|
|
349
|
+
this.log('');
|
|
350
|
+
// Check for existing custom strategies
|
|
351
|
+
let customStrategies = [];
|
|
352
|
+
try {
|
|
353
|
+
await runEngine('workbench-list', [], (msg) => {
|
|
354
|
+
if (msg.type === 'result') {
|
|
355
|
+
customStrategies = msg.strategies || [];
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch { /* empty */ }
|
|
360
|
+
if (customStrategies.length > 0) {
|
|
361
|
+
this.log(dim(' Your strategies:'));
|
|
362
|
+
this.log('');
|
|
363
|
+
for (let i = 0; i < customStrategies.length; i++) {
|
|
364
|
+
const s = customStrategies[i];
|
|
365
|
+
const filters = (s.filters || []).join(', ');
|
|
366
|
+
this.log(` ${cyan(String(i + 1))} ${bold(String(s.name).padEnd(22))} v${s.version} ${dim(String(s.description || '').slice(0, 35))}`);
|
|
367
|
+
if (filters)
|
|
368
|
+
this.log(` ${dim('filters: ' + filters)}`);
|
|
369
|
+
}
|
|
370
|
+
this.log('');
|
|
371
|
+
this.log(` ${cyan(String(customStrategies.length + 1))} ${green('+')} Create new strategy`);
|
|
372
|
+
this.log('');
|
|
373
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
374
|
+
const idx = parseInt(choice) - 1;
|
|
375
|
+
if (idx >= 0 && idx < customStrategies.length) {
|
|
376
|
+
return this.workbench(String(customStrategies[idx].name));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Create new strategy flow
|
|
380
|
+
this.log(dim(' Start from a template:'));
|
|
381
|
+
this.log('');
|
|
382
|
+
for (const t of TEMPLATES) {
|
|
383
|
+
this.log(` ${cyan(t.key)} ${bold(t.label.padEnd(22))} ${dim('— ' + t.desc)}`);
|
|
384
|
+
}
|
|
385
|
+
this.log('');
|
|
386
|
+
const templateChoice = await ask(` ${cyan('>')} `);
|
|
387
|
+
const template = TEMPLATES.find(t => t.key === templateChoice);
|
|
388
|
+
if (!template)
|
|
389
|
+
return this.mainMenu();
|
|
390
|
+
const name = await ask(`\n ${cyan('Strategy name')} ${dim('(snake_case)')}: `);
|
|
391
|
+
if (!name)
|
|
392
|
+
return this.mainMenu();
|
|
393
|
+
// Create via engine
|
|
394
|
+
let created = false;
|
|
395
|
+
try {
|
|
396
|
+
await runEngine('workbench-create', [name, '--template', template.template], (msg) => {
|
|
397
|
+
if (msg.type === 'result') {
|
|
398
|
+
created = true;
|
|
399
|
+
this.log('');
|
|
400
|
+
this.log(` ${green('✔')} Created ${bold(name)} from ${dim(template.label)} template`);
|
|
401
|
+
}
|
|
402
|
+
else if (msg.type === 'error') {
|
|
403
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
409
|
+
return this.mainMenu();
|
|
410
|
+
}
|
|
411
|
+
if (created) {
|
|
412
|
+
this.log('');
|
|
413
|
+
return this.workbench(name);
|
|
414
|
+
}
|
|
415
|
+
return this.mainMenu();
|
|
416
|
+
}
|
|
417
|
+
// ═══════════════════════════════════════════
|
|
418
|
+
// WORKBENCH — The persistent editing view
|
|
419
|
+
// ═══════════════════════════════════════════
|
|
420
|
+
async workbench(strategyName, pair = 'BTC') {
|
|
421
|
+
// Load config
|
|
422
|
+
let config = {};
|
|
423
|
+
try {
|
|
424
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
425
|
+
if (msg.type === 'result') {
|
|
426
|
+
config = msg.config;
|
|
427
|
+
}
|
|
428
|
+
else if (msg.type === 'error') {
|
|
429
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!config.name)
|
|
438
|
+
return;
|
|
439
|
+
const entry = config.entry || {};
|
|
440
|
+
const exit_ = config.exit || {};
|
|
441
|
+
const risk = config.risk || {};
|
|
442
|
+
const filters = config.filters || {};
|
|
443
|
+
const entryConds = entry.conditions || [];
|
|
444
|
+
const exitConds = exit_.conditions || [];
|
|
445
|
+
// Get last test result
|
|
446
|
+
const lastData = { result: null };
|
|
447
|
+
try {
|
|
448
|
+
await runEngine('experiments', [strategyName, '--limit', '1'], (msg) => {
|
|
449
|
+
if (msg.type === 'result') {
|
|
450
|
+
const exps = msg.experiments || [];
|
|
451
|
+
if (exps.length > 0)
|
|
452
|
+
lastData.result = exps[0];
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch { /* empty */ }
|
|
457
|
+
const lastResult = lastData.result;
|
|
458
|
+
// Render workbench
|
|
459
|
+
const iw = 54;
|
|
460
|
+
const wr = boldBoxRow(iw);
|
|
461
|
+
const stratMeta = STRATEGIES.find(s => s.name === strategyName);
|
|
462
|
+
const mktLabel = stratMeta ? ` · ${marketColor(stratMeta.market)}` : '';
|
|
463
|
+
this.log('');
|
|
464
|
+
this.log(boldBoxTop(iw));
|
|
465
|
+
this.log(wr(` WORKBENCH: ${bold(strategyName)} on ${pair} PERP ${config.timeframe || '1h'}${mktLabel}`));
|
|
466
|
+
this.log(boldBoxDivider(iw));
|
|
467
|
+
if (lastResult) {
|
|
468
|
+
const ret = Number(lastResult.return_pct ?? 0).toFixed(2);
|
|
469
|
+
const sharpe = Number(lastResult.sharpe ?? 0).toFixed(2);
|
|
470
|
+
const trades = lastResult.num_trades ?? 0;
|
|
471
|
+
const win = Number(lastResult.win_rate ?? 0).toFixed(0);
|
|
472
|
+
this.log(wr(` LAST TEST: ${colorNum(Number(ret), '%')} | Sharpe ${colorNum(Number(sharpe))} | ${trades} trades | ${win}% win`));
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
this.log(wr(` ${dim('No tests yet — press [t] to quick test')}`));
|
|
476
|
+
}
|
|
477
|
+
this.log(boldBoxDivider(iw));
|
|
478
|
+
// Entry zone
|
|
479
|
+
this.log(wr(''));
|
|
480
|
+
this.log(wr(` ${cyan('[1]')} Entry ${this.formatConditions(entryConds, 'entry')}`));
|
|
481
|
+
// Exit zone
|
|
482
|
+
this.log(wr(''));
|
|
483
|
+
const exitDesc = this.formatConditions(exitConds, 'exit');
|
|
484
|
+
const maxHold = exit_.max_hold || 48;
|
|
485
|
+
this.log(wr(` ${cyan('[2]')} Exit ${exitDesc}`));
|
|
486
|
+
this.log(wr(` ${dim(`max hold: ${maxHold} candles`)}`));
|
|
487
|
+
// Risk zone
|
|
488
|
+
this.log(wr(''));
|
|
489
|
+
const sl = risk.stop_loss ? `${(risk.stop_loss * 100).toFixed(1)}%` : '2.0%';
|
|
490
|
+
const lev = risk.leverage || 2.0;
|
|
491
|
+
const rpt = risk.risk_per_trade ? `${(risk.risk_per_trade * 100).toFixed(1)}%` : '2.0%';
|
|
492
|
+
this.log(wr(` ${cyan('[3]')} Risk SL: ${sl} Size: ${rpt} Lev: ${lev}x`));
|
|
493
|
+
// Filters zone
|
|
494
|
+
this.log(wr(''));
|
|
495
|
+
const filterList = Object.entries(filters)
|
|
496
|
+
.map(([k, v]) => `${v ? green('☑') : dim('☐')} ${k.replace(/_/g, ' ')}`)
|
|
497
|
+
.join(' ');
|
|
498
|
+
this.log(wr(` ${cyan('[4]')} Filters ${filterList || dim('none')}`));
|
|
499
|
+
this.log(wr(''));
|
|
500
|
+
this.log(boldBoxDivider(iw));
|
|
501
|
+
this.log(wr(` ${cyan('[t]')} Quick test ${cyan('[T]')} Full validate ${cyan('[h]')} History`));
|
|
502
|
+
this.log(wr(` ${cyan('[p]')} Change pair ${cyan('[m]')} Mixer ${cyan('[q]')} Back`));
|
|
503
|
+
this.log(boldBoxBottom(iw));
|
|
504
|
+
this.log('');
|
|
505
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
506
|
+
switch (choice) {
|
|
507
|
+
case '1': return this.editEntry(strategyName, pair);
|
|
508
|
+
case '2': return this.editExit(strategyName, pair);
|
|
509
|
+
case '3': return this.editRisk(strategyName, pair);
|
|
510
|
+
case '4': return this.editFilters(strategyName, pair);
|
|
511
|
+
case 't': return this.runQuickTest(strategyName, pair);
|
|
512
|
+
case 'T': return this.runFullValidate(strategyName, pair);
|
|
513
|
+
case 'h': return this.showHistory(strategyName, pair);
|
|
514
|
+
case 'p':
|
|
515
|
+
const newPair = (await ask(` ${cyan('Ticker')}: `) || pair).replace('-PERP', '').replace('-perp', '').toUpperCase();
|
|
516
|
+
return this.workbench(strategyName, newPair);
|
|
517
|
+
case 'm': return this.mixerMenu(strategyName, pair);
|
|
518
|
+
case 'q': return this.buildMenu();
|
|
519
|
+
default: return this.workbench(strategyName, pair);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
formatConditions(conds, _type) {
|
|
523
|
+
const validConds = conds.filter(c => !c.indicator?.startsWith('_'));
|
|
524
|
+
if (validConds.length === 0)
|
|
525
|
+
return dim('(none configured)');
|
|
526
|
+
// Show first condition inline, rest are visible in the edit menu
|
|
527
|
+
const c = validConds[0];
|
|
528
|
+
const ind = c.indicator;
|
|
529
|
+
const op = c.op;
|
|
530
|
+
const val = c.value !== undefined ? c.value : c.ref;
|
|
531
|
+
const side = c.side ? dim(` [${c.side}]`) : '';
|
|
532
|
+
let result = `${ind} ${op} ${val}${side}`;
|
|
533
|
+
if (validConds.length > 1)
|
|
534
|
+
result += dim(` +${validConds.length - 1} more`);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
// ═══════════════════════════════════════════
|
|
538
|
+
// WORKBENCH — Zone editors
|
|
539
|
+
// ═══════════════════════════════════════════
|
|
540
|
+
async editEntry(strategyName, pair) {
|
|
541
|
+
let config = {};
|
|
542
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
543
|
+
if (msg.type === 'result')
|
|
544
|
+
config = msg.config;
|
|
545
|
+
});
|
|
546
|
+
const entry = config.entry || {};
|
|
547
|
+
const conds = entry.conditions || [];
|
|
548
|
+
this.log('');
|
|
549
|
+
this.log(` ${bold('ENTRY CONDITIONS')} for ${bold(strategyName)}`);
|
|
550
|
+
this.log('');
|
|
551
|
+
this.log(` Current:`);
|
|
552
|
+
if (conds.length === 0) {
|
|
553
|
+
this.log(` ${dim('(no conditions set)')}`);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
for (let i = 0; i < conds.length; i++) {
|
|
557
|
+
const c = conds[i];
|
|
558
|
+
this.log(` ${dim(String(i + 1) + '.')} ${c.indicator} ${c.op} ${c.value ?? c.ref}${c.side ? dim(` [${c.side}]`) : ''}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
this.log('');
|
|
562
|
+
this.log(` ${cyan('a')} Add a condition`);
|
|
563
|
+
this.log(` ${cyan('r')} Remove a condition`);
|
|
564
|
+
this.log(` ${cyan('d')} Change direction ${dim(`(current: ${entry.direction || 'both'})`)}`);
|
|
565
|
+
this.log(` ${cyan('b')} Back to workbench`);
|
|
566
|
+
this.log('');
|
|
567
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
568
|
+
if (choice === 'a') {
|
|
569
|
+
const newCond = await this.guidedConditionPicker(pair);
|
|
570
|
+
if (newCond) {
|
|
571
|
+
conds.push(newCond);
|
|
572
|
+
entry.conditions = conds;
|
|
573
|
+
config.entry = entry;
|
|
574
|
+
const desc = `added entry: ${newCond.indicator} ${newCond.op} ${newCond.value ?? newCond.ref}`;
|
|
575
|
+
await this.saveAndRegenerate(strategyName, config, pair, desc);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else if (choice === 'r' && conds.length > 0) {
|
|
579
|
+
const idx = parseInt(await ask(` ${cyan('Remove #')}: `)) - 1;
|
|
580
|
+
if (idx >= 0 && idx < conds.length) {
|
|
581
|
+
const removed = conds.splice(idx, 1)[0];
|
|
582
|
+
entry.conditions = conds;
|
|
583
|
+
config.entry = entry;
|
|
584
|
+
await this.saveAndRegenerate(strategyName, config, pair, `removed entry: ${removed.indicator}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (choice === 'd') {
|
|
588
|
+
const dir = await ask(` ${cyan('Direction')} ${dim('(both/long_only/short_only)')}: `);
|
|
589
|
+
if (dir) {
|
|
590
|
+
entry.direction = dir;
|
|
591
|
+
config.entry = entry;
|
|
592
|
+
await this.saveAndRegenerate(strategyName, config, pair, `direction → ${dir}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return this.workbench(strategyName, pair);
|
|
596
|
+
}
|
|
597
|
+
async editExit(strategyName, pair) {
|
|
598
|
+
let config = {};
|
|
599
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
600
|
+
if (msg.type === 'result')
|
|
601
|
+
config = msg.config;
|
|
602
|
+
});
|
|
603
|
+
const exit_ = config.exit || {};
|
|
604
|
+
const conds = exit_.conditions || [];
|
|
605
|
+
this.log('');
|
|
606
|
+
this.log(` ${bold('EXIT CONDITIONS')} for ${bold(strategyName)}`);
|
|
607
|
+
this.log('');
|
|
608
|
+
this.log(` Current:`);
|
|
609
|
+
this.log(` ${dim('•')} Max hold: ${exit_.max_hold || 48} candles`);
|
|
610
|
+
for (let i = 0; i < conds.length; i++) {
|
|
611
|
+
const c = conds[i];
|
|
612
|
+
this.log(` ${dim(String(i + 1) + '.')} ${c.indicator} ${c.op} ${c.value ?? c.ref}${c.side ? dim(` [${c.side}]`) : ''}`);
|
|
613
|
+
}
|
|
614
|
+
this.log('');
|
|
615
|
+
this.log(` ${cyan('a')} Add exit condition`);
|
|
616
|
+
this.log(` ${cyan('r')} Remove exit condition`);
|
|
617
|
+
this.log(` ${cyan('h')} Change max hold`);
|
|
618
|
+
this.log(` ${cyan('b')} Back`);
|
|
619
|
+
this.log('');
|
|
620
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
621
|
+
if (choice === 'a') {
|
|
622
|
+
const newCond = await this.guidedConditionPicker(pair, true);
|
|
623
|
+
if (newCond) {
|
|
624
|
+
if (newCond.indicator === '_max_hold') {
|
|
625
|
+
// Special case: max hold is a config property, not a condition
|
|
626
|
+
exit_.max_hold = newCond.value;
|
|
627
|
+
config.exit = exit_;
|
|
628
|
+
await this.saveAndRegenerate(strategyName, config, pair, `max hold → ${newCond.value}`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
conds.push(newCond);
|
|
632
|
+
exit_.conditions = conds;
|
|
633
|
+
config.exit = exit_;
|
|
634
|
+
await this.saveAndRegenerate(strategyName, config, pair, `added exit: ${newCond.indicator} ${newCond.op} ${newCond.value}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else if (choice === 'r' && conds.length > 0) {
|
|
639
|
+
const idx = parseInt(await ask(` ${cyan('Remove #')}: `)) - 1;
|
|
640
|
+
if (idx >= 0 && idx < conds.length) {
|
|
641
|
+
conds.splice(idx, 1);
|
|
642
|
+
exit_.conditions = conds;
|
|
643
|
+
config.exit = exit_;
|
|
644
|
+
await this.saveAndRegenerate(strategyName, config, pair, `removed exit condition`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else if (choice === 'h') {
|
|
648
|
+
const hold = await ask(` ${cyan('Max hold (candles)')}: `);
|
|
649
|
+
if (hold) {
|
|
650
|
+
exit_.max_hold = parseInt(hold);
|
|
651
|
+
config.exit = exit_;
|
|
652
|
+
await this.saveAndRegenerate(strategyName, config, pair, `max hold → ${hold}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return this.workbench(strategyName, pair);
|
|
656
|
+
}
|
|
657
|
+
async editRisk(strategyName, pair) {
|
|
658
|
+
let config = {};
|
|
659
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
660
|
+
if (msg.type === 'result')
|
|
661
|
+
config = msg.config;
|
|
662
|
+
});
|
|
663
|
+
const risk = config.risk || {};
|
|
664
|
+
this.log('');
|
|
665
|
+
this.log(` ${bold('RISK SETTINGS')} for ${bold(strategyName)}`);
|
|
666
|
+
this.log('');
|
|
667
|
+
this.log(` ${cyan('1')} Stop loss: ${bold(String((risk.stop_loss * 100).toFixed(1) + '%'))}`);
|
|
668
|
+
this.log(` ${cyan('2')} Risk per trade: ${bold(String((risk.risk_per_trade * 100).toFixed(1) + '%'))}`);
|
|
669
|
+
this.log(` ${cyan('3')} Leverage: ${bold(String(risk.leverage + 'x'))}`);
|
|
670
|
+
this.log(` ${cyan('b')} Back`);
|
|
671
|
+
this.log('');
|
|
672
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
673
|
+
if (choice === '1') {
|
|
674
|
+
const val = await ask(` ${cyan('Stop loss %')}: `);
|
|
675
|
+
if (val) {
|
|
676
|
+
const oldSl = (risk.stop_loss * 100).toFixed(1);
|
|
677
|
+
risk.stop_loss = parseFloat(val) / 100;
|
|
678
|
+
config.risk = risk;
|
|
679
|
+
await this.saveAndRegenerate(strategyName, config, pair, `stop loss ${oldSl}% → ${val}%`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else if (choice === '2') {
|
|
683
|
+
const val = await ask(` ${cyan('Risk per trade %')}: `);
|
|
684
|
+
if (val) {
|
|
685
|
+
risk.risk_per_trade = parseFloat(val) / 100;
|
|
686
|
+
config.risk = risk;
|
|
687
|
+
await this.saveAndRegenerate(strategyName, config, pair, `risk per trade → ${val}%`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else if (choice === '3') {
|
|
691
|
+
const val = await ask(` ${cyan('Leverage')}: `);
|
|
692
|
+
if (val) {
|
|
693
|
+
risk.leverage = parseFloat(val);
|
|
694
|
+
config.risk = risk;
|
|
695
|
+
await this.saveAndRegenerate(strategyName, config, pair, `leverage → ${val}x`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return this.workbench(strategyName, pair);
|
|
699
|
+
}
|
|
700
|
+
async editFilters(strategyName, pair) {
|
|
701
|
+
let config = {};
|
|
702
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
703
|
+
if (msg.type === 'result')
|
|
704
|
+
config = msg.config;
|
|
705
|
+
});
|
|
706
|
+
const filters = config.filters || {};
|
|
707
|
+
const available = [
|
|
708
|
+
{ key: 'hmm_filter', desc: 'HMM regime filter — skip crisis markets (self-contained, no dependency)' },
|
|
709
|
+
{ key: 'rsi_confirmation', desc: 'RSI confirmation — oversold/overbought gates' },
|
|
710
|
+
{ key: 'volume_filter', desc: 'Volume filter — require 1.5x avg volume' },
|
|
711
|
+
{ key: 'adx_trend', desc: 'ADX trend filter — only trade when trending (>25)' },
|
|
712
|
+
];
|
|
713
|
+
this.log('');
|
|
714
|
+
this.log(` ${bold('FILTERS')} for ${bold(strategyName)}`);
|
|
715
|
+
this.log('');
|
|
716
|
+
for (let i = 0; i < available.length; i++) {
|
|
717
|
+
const f = available[i];
|
|
718
|
+
const on = filters[f.key] === true;
|
|
719
|
+
this.log(` ${cyan(String(i + 1))} ${on ? green('☑') : dim('☐')} ${bold(f.key.padEnd(22))} ${dim(f.desc)}`);
|
|
720
|
+
}
|
|
721
|
+
this.log(` ${cyan('b')} Back`);
|
|
722
|
+
this.log('');
|
|
723
|
+
const choice = await ask(` ${cyan('Toggle #')} `);
|
|
724
|
+
const idx = parseInt(choice) - 1;
|
|
725
|
+
if (idx >= 0 && idx < available.length) {
|
|
726
|
+
const key = available[idx].key;
|
|
727
|
+
filters[key] = !filters[key];
|
|
728
|
+
config.filters = filters;
|
|
729
|
+
const action = filters[key] ? 'enabled' : 'disabled';
|
|
730
|
+
await this.saveAndRegenerate(strategyName, config, pair, `${action} ${key}`);
|
|
731
|
+
}
|
|
732
|
+
return this.workbench(strategyName, pair);
|
|
733
|
+
}
|
|
734
|
+
// ═══════════════════════════════════════════
|
|
735
|
+
// GUIDED CONDITION PICKER — with live stats
|
|
736
|
+
// ═══════════════════════════════════════════
|
|
737
|
+
async guidedConditionPicker(pair, isExit = false) {
|
|
738
|
+
// Step 1: Category
|
|
739
|
+
this.log('');
|
|
740
|
+
this.log(` ${bold(isExit ? 'ADD EXIT CONDITION' : 'ADD ENTRY CONDITION')}`);
|
|
741
|
+
this.log('');
|
|
742
|
+
this.log(` ${dim('Pick a category:')}`);
|
|
743
|
+
this.log('');
|
|
744
|
+
this.log(` ${cyan('1')} ${bold('Funding')} ${dim('funding rate thresholds')}`);
|
|
745
|
+
this.log(` ${cyan('2')} ${bold('Price')} ${dim('vs EMA, vs VWAP')}`);
|
|
746
|
+
this.log(` ${cyan('3')} ${bold('Momentum')} ${dim('RSI, ADX')}`);
|
|
747
|
+
this.log(` ${cyan('4')} ${bold('Volume')} ${dim('volume spike detection')}`);
|
|
748
|
+
if (isExit) {
|
|
749
|
+
this.log(` ${cyan('5')} ${bold('Time')} ${dim('max hold candles')}`);
|
|
750
|
+
}
|
|
751
|
+
this.log('');
|
|
752
|
+
const cat = await ask(` ${cyan('>')} `);
|
|
753
|
+
// Fetch live stats for this pair
|
|
754
|
+
let stats = {};
|
|
755
|
+
this.log(dim(' Loading market stats...'));
|
|
756
|
+
try {
|
|
757
|
+
await runEngine('indicator-stats', ['--pair', pair], (msg) => {
|
|
758
|
+
if (msg.type === 'result')
|
|
759
|
+
stats = msg;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
catch { /* stats are optional enhancement */ }
|
|
763
|
+
switch (cat) {
|
|
764
|
+
case '1': return this.pickFundingCondition(stats, isExit);
|
|
765
|
+
case '2': return this.pickPriceCondition(stats, isExit);
|
|
766
|
+
case '3': return this.pickMomentumCondition(stats, isExit);
|
|
767
|
+
case '4': return this.pickVolumeCondition(stats);
|
|
768
|
+
case '5': if (isExit)
|
|
769
|
+
return this.pickTimeCondition();
|
|
770
|
+
default: return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async pickFundingCondition(stats, isExit) {
|
|
774
|
+
const fs = stats.funding_rate;
|
|
775
|
+
this.log('');
|
|
776
|
+
if (isExit) {
|
|
777
|
+
this.log(` ${bold('FUNDING EXIT')}`);
|
|
778
|
+
this.log('');
|
|
779
|
+
this.log(` ${cyan('1')} Funding normalizes ${dim('exit when rate drops below threshold')}`);
|
|
780
|
+
this.log('');
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
this.log(` ${bold('FUNDING ENTRY')}`);
|
|
784
|
+
this.log('');
|
|
785
|
+
this.log(` ${cyan('1')} Funding rate extreme ${dim('enter when |rate| exceeds threshold')}`);
|
|
786
|
+
this.log(` ${cyan('2')} Funding rate positive ${dim('enter when shorts earn (rate > 0)')}`);
|
|
787
|
+
this.log(` ${cyan('3')} Funding rate negative ${dim('enter when longs earn (rate < 0)')}`);
|
|
788
|
+
this.log('');
|
|
789
|
+
}
|
|
790
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
791
|
+
// Show live stats
|
|
792
|
+
if (fs) {
|
|
793
|
+
this.log('');
|
|
794
|
+
this.log(` ${dim(`${stats.pair} funding — last ${stats.candles} candles:`)}`);
|
|
795
|
+
// Visual gauge
|
|
796
|
+
const min = fs.min;
|
|
797
|
+
const max = fs.max;
|
|
798
|
+
const p75 = fs.p75;
|
|
799
|
+
const p90 = fs.p90;
|
|
800
|
+
const rec = fs.recommended;
|
|
801
|
+
this.log(` ${dim('Min')} ${dim(String(min))}`);
|
|
802
|
+
this.log(` ${dim('Median')} ${dim(String(fs.median))}`);
|
|
803
|
+
this.log(` ${dim('75th')} ${cyan(String(p75))}`);
|
|
804
|
+
this.log(` ${dim('90th')} ${yellow(String(p90))}`);
|
|
805
|
+
this.log(` ${dim('Max')} ${dim(String(max))}`);
|
|
806
|
+
this.log(` ${dim(`${fs.positive_pct}% of the time, longs pay shorts`)}`);
|
|
807
|
+
this.log('');
|
|
808
|
+
this.log(` ${green('★')} Recommended: ${bold(String(rec))} ${dim('(75th percentile of |rate|)')}`);
|
|
809
|
+
}
|
|
810
|
+
if (isExit) {
|
|
811
|
+
const defaultVal = fs ? fs.median : 0.000003;
|
|
812
|
+
const val = await ask(`\n ${cyan('Exit below')} ${dim(`(${defaultVal})`)}: `);
|
|
813
|
+
const threshold = val ? parseFloat(val) : defaultVal;
|
|
814
|
+
return { indicator: 'funding_rate', op: 'abs_below', value: threshold };
|
|
815
|
+
}
|
|
816
|
+
if (choice === '1') {
|
|
817
|
+
const defaultVal = fs ? fs.recommended : 0.000015;
|
|
818
|
+
const val = await ask(`\n ${cyan('Threshold')} ${dim(`(${defaultVal})`)}: `);
|
|
819
|
+
const threshold = val ? parseFloat(val) : defaultVal;
|
|
820
|
+
this.log('');
|
|
821
|
+
this.log(` ${dim('When funding exceeds this threshold:')}`);
|
|
822
|
+
this.log(` ${cyan('1')} ${bold('SHORT')} when positive ${dim('— shorts earn (recommended)')}`);
|
|
823
|
+
this.log(` ${cyan('2')} ${bold('LONG')} when negative ${dim('— longs earn (recommended)')}`);
|
|
824
|
+
this.log(` ${cyan('3')} ${bold('Both')} directions`);
|
|
825
|
+
this.log('');
|
|
826
|
+
const sideChoice = await ask(` ${cyan('>')} `);
|
|
827
|
+
if (sideChoice === '1') {
|
|
828
|
+
return { indicator: 'funding_rate', op: '>', value: threshold, side: 'short' };
|
|
829
|
+
}
|
|
830
|
+
else if (sideChoice === '2') {
|
|
831
|
+
return { indicator: 'funding_rate', op: '<', value: -threshold, side: 'long' };
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// Both — add as two conditions? Just return the positive side, user can add more
|
|
835
|
+
return { indicator: 'funding_rate', op: '>', value: threshold, side: 'short' };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else if (choice === '2') {
|
|
839
|
+
const defaultVal = fs ? fs.recommended : 0.000015;
|
|
840
|
+
const val = await ask(`\n ${cyan('Min positive rate')} ${dim(`(${defaultVal})`)}: `);
|
|
841
|
+
return { indicator: 'funding_rate', op: '>', value: val ? parseFloat(val) : defaultVal, side: 'short' };
|
|
842
|
+
}
|
|
843
|
+
else if (choice === '3') {
|
|
844
|
+
const defaultVal = fs ? -fs.recommended : -0.000015;
|
|
845
|
+
const val = await ask(`\n ${cyan('Max negative rate')} ${dim(`(${defaultVal})`)}: `);
|
|
846
|
+
return { indicator: 'funding_rate', op: '<', value: val ? parseFloat(val) : defaultVal, side: 'long' };
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
async pickPriceCondition(stats, isExit) {
|
|
851
|
+
this.log('');
|
|
852
|
+
this.log(` ${bold('PRICE CONDITIONS')}`);
|
|
853
|
+
this.log('');
|
|
854
|
+
this.log(` ${cyan('1')} Price above EMA ${dim('trend filter — only trade with trend')}`);
|
|
855
|
+
this.log(` ${cyan('2')} Price below EMA ${dim('mean reversion — buy the dip')}`);
|
|
856
|
+
this.log(` ${cyan('3')} VWAP deviation ${dim('extreme deviation from fair value')}`);
|
|
857
|
+
this.log('');
|
|
858
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
859
|
+
if (choice === '1' || choice === '2') {
|
|
860
|
+
// EMA period selection
|
|
861
|
+
const emaStats = stats.ema_100;
|
|
862
|
+
this.log('');
|
|
863
|
+
this.log(` ${dim('EMA period:')}`);
|
|
864
|
+
this.log(` ${cyan('1')} EMA 50 ${dim('— faster, more responsive')}`);
|
|
865
|
+
this.log(` ${cyan('2')} EMA 100 ${dim('— balanced (default)')}`);
|
|
866
|
+
this.log(` ${cyan('3')} EMA 200 ${dim('— slower, stronger filter')}`);
|
|
867
|
+
this.log('');
|
|
868
|
+
if (emaStats) {
|
|
869
|
+
this.log(` ${dim(`${stats.pair}: price above EMA 100 ${emaStats.pct_above}% of the time`)}`);
|
|
870
|
+
this.log('');
|
|
871
|
+
}
|
|
872
|
+
const periodChoice = await ask(` ${cyan('>')} ${dim('(2)')}: `) || '2';
|
|
873
|
+
const period = periodChoice === '1' ? 50 : periodChoice === '3' ? 200 : 100;
|
|
874
|
+
// Price above EMA = bullish = use as filter for SHORT funding trades (price overextended)
|
|
875
|
+
// Price below EMA = bearish = use as filter for LONG funding trades (price oversold)
|
|
876
|
+
const op = choice === '1' ? '>' : '<';
|
|
877
|
+
const side = choice === '1' ? 'short' : 'long';
|
|
878
|
+
return { indicator: 'price', op, ref: `ema_${period}`, side };
|
|
879
|
+
}
|
|
880
|
+
else if (choice === '3') {
|
|
881
|
+
const vs = stats.vwap_zscore;
|
|
882
|
+
if (vs) {
|
|
883
|
+
this.log('');
|
|
884
|
+
this.log(` ${dim(`${stats.pair} VWAP z-score distribution:`)}`);
|
|
885
|
+
this.log(` ${dim('5th')} ${dim(String(vs.p5))}σ`);
|
|
886
|
+
this.log(` ${dim('95th')} ${dim(String(vs.p95))}σ`);
|
|
887
|
+
this.log(` ${dim(`Beyond ±2σ: ${vs.pct_beyond_2}% of candles`)}`);
|
|
888
|
+
this.log(` ${dim(`Beyond ±3σ: ${vs.pct_beyond_3}% of candles`)}`);
|
|
889
|
+
this.log('');
|
|
890
|
+
this.log(` ${green('★')} Recommended entry: ${bold(`±${vs.recommended_entry}σ`)} ${dim('(95th percentile)')}`);
|
|
891
|
+
}
|
|
892
|
+
const defaultDev = vs ? vs.recommended_entry : 2.5;
|
|
893
|
+
const val = await ask(`\n ${cyan('Entry deviation (σ)')} ${dim(`(${defaultDev})`)}: `);
|
|
894
|
+
const dev = val ? parseFloat(val) : defaultDev;
|
|
895
|
+
return { indicator: 'vwap_zscore', op: isExit ? 'abs_below' : '<', value: isExit ? dev : -dev, side: 'long' };
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
async pickMomentumCondition(stats, isExit) {
|
|
900
|
+
this.log('');
|
|
901
|
+
this.log(` ${bold('MOMENTUM CONDITIONS')}`);
|
|
902
|
+
this.log('');
|
|
903
|
+
this.log(` ${cyan('1')} RSI oversold/overbought ${dim('momentum extremes')}`);
|
|
904
|
+
this.log(` ${cyan('2')} ADX trend strength ${dim('only trade when trending')}`);
|
|
905
|
+
this.log('');
|
|
906
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
907
|
+
if (choice === '1') {
|
|
908
|
+
const rs = stats.rsi;
|
|
909
|
+
if (rs) {
|
|
910
|
+
this.log('');
|
|
911
|
+
this.log(` ${dim(`${stats.pair} RSI (14) distribution:`)}`);
|
|
912
|
+
this.log(` ${dim('10th')} ${dim(String(rs.p10))}`);
|
|
913
|
+
this.log(` ${dim('25th')} ${dim(String(rs.p25))}`);
|
|
914
|
+
this.log(` ${dim('Mean')} ${dim(String(rs.mean))}`);
|
|
915
|
+
this.log(` ${dim('75th')} ${dim(String(rs.p75))}`);
|
|
916
|
+
this.log(` ${dim('90th')} ${dim(String(rs.p90))}`);
|
|
917
|
+
this.log(` ${dim(`Below 30: ${rs.pct_below_30}% · Above 70: ${rs.pct_above_70}%`)}`);
|
|
918
|
+
this.log('');
|
|
919
|
+
this.log(` ${green('★')} Recommended: oversold < ${bold(String(rs.recommended_oversold))} · overbought > ${bold(String(rs.recommended_overbought))}`);
|
|
920
|
+
}
|
|
921
|
+
this.log('');
|
|
922
|
+
this.log(` ${cyan('1')} RSI oversold ${dim('— enter LONG when RSI < threshold')}`);
|
|
923
|
+
this.log(` ${cyan('2')} RSI overbought ${dim('— enter SHORT when RSI > threshold')}`);
|
|
924
|
+
this.log('');
|
|
925
|
+
const rsiChoice = await ask(` ${cyan('>')} `);
|
|
926
|
+
if (rsiChoice === '1') {
|
|
927
|
+
const def = rs ? rs.recommended_oversold : 40;
|
|
928
|
+
const val = await ask(` ${cyan('RSI below')} ${dim(`(${def})`)}: `);
|
|
929
|
+
return { indicator: 'rsi', op: '<', value: val ? parseFloat(val) : def, side: 'long' };
|
|
930
|
+
}
|
|
931
|
+
else if (rsiChoice === '2') {
|
|
932
|
+
const def = rs ? rs.recommended_overbought : 60;
|
|
933
|
+
const val = await ask(` ${cyan('RSI above')} ${dim(`(${def})`)}: `);
|
|
934
|
+
return { indicator: 'rsi', op: '>', value: val ? parseFloat(val) : def, side: 'short' };
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
else if (choice === '2') {
|
|
938
|
+
const as = stats.adx;
|
|
939
|
+
if (as) {
|
|
940
|
+
this.log('');
|
|
941
|
+
this.log(` ${dim(`${stats.pair} ADX (14):`)}`);
|
|
942
|
+
this.log(` ${dim(`Mean: ${as.mean} · Median: ${as.median}`)}`);
|
|
943
|
+
this.log(` ${dim(`Above 25 (trending): ${as.pct_above_25}%`)}`);
|
|
944
|
+
this.log(` ${dim(`Above 40 (strong trend): ${as.pct_above_40}%`)}`);
|
|
945
|
+
this.log('');
|
|
946
|
+
this.log(` ${green('★')} Recommended: ${bold('> 25')} ${dim('(standard trend threshold)')}`);
|
|
947
|
+
}
|
|
948
|
+
const def = 25;
|
|
949
|
+
const val = await ask(`\n ${cyan('ADX above')} ${dim(`(${def})`)}: `);
|
|
950
|
+
return { indicator: 'adx', op: '>', value: val ? parseFloat(val) : def };
|
|
951
|
+
}
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
async pickVolumeCondition(stats) {
|
|
955
|
+
const vs = stats.volume;
|
|
956
|
+
this.log('');
|
|
957
|
+
this.log(` ${bold('VOLUME CONDITIONS')}`);
|
|
958
|
+
if (vs) {
|
|
959
|
+
this.log('');
|
|
960
|
+
this.log(` ${dim(`${stats.pair} volume ratio (vs 20-period avg):`)}`);
|
|
961
|
+
this.log(` ${dim(`Median: ${vs.median}x · 75th: ${vs.p75}x · 90th: ${vs.p90}x · 95th: ${vs.p95}x`)}`);
|
|
962
|
+
this.log('');
|
|
963
|
+
this.log(` ${green('★')} Recommended: ${bold(`> ${vs.recommended}x`)} ${dim('(above average volume)')}`);
|
|
964
|
+
}
|
|
965
|
+
const def = vs ? vs.recommended : 1.5;
|
|
966
|
+
const val = await ask(`\n ${cyan('Volume above')} ${dim(`(${def}x avg)`)}: `);
|
|
967
|
+
return { indicator: 'vol_ratio', op: '>', value: val ? parseFloat(val) : def };
|
|
968
|
+
}
|
|
969
|
+
async pickTimeCondition() {
|
|
970
|
+
this.log('');
|
|
971
|
+
this.log(` ${bold('TIME EXIT')}`);
|
|
972
|
+
this.log(` ${dim('Close position after N candles regardless of conditions.')}`);
|
|
973
|
+
this.log('');
|
|
974
|
+
this.log(` ${dim('Common values: 6 (quick), 24 (1 day), 48 (2 days), 72 (3 days)')}`);
|
|
975
|
+
const val = await ask(`\n ${cyan('Max hold candles')} ${dim('(48)')}: `);
|
|
976
|
+
// This is handled via max_hold in exit config, not as a condition
|
|
977
|
+
// Return a special marker that editExit handles
|
|
978
|
+
return { indicator: '_max_hold', op: '=', value: val ? parseInt(val, 10) : 48 };
|
|
979
|
+
}
|
|
980
|
+
async saveAndRegenerate(strategyName, config, pair, changeDesc) {
|
|
981
|
+
try {
|
|
982
|
+
await runEngine('workbench-update', [strategyName, JSON.stringify(config)], (msg) => {
|
|
983
|
+
if (msg.type === 'result') {
|
|
984
|
+
this.log(` ${green('✔')} Updated — ${dim(changeDesc)}`);
|
|
985
|
+
}
|
|
986
|
+
else if (msg.type === 'error') {
|
|
987
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
catch (e) {
|
|
992
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
993
|
+
}
|
|
994
|
+
// Auto quick-test after every change
|
|
995
|
+
const doTest = await ask(` ${dim('Quick test? (y/n)')}: `);
|
|
996
|
+
if (doTest.toLowerCase() === 'y' || doTest === '') {
|
|
997
|
+
await this.runQuickTest(strategyName, pair, changeDesc);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// ═══════════════════════════════════════════
|
|
1001
|
+
// WORKBENCH — Quick test & history
|
|
1002
|
+
// ═══════════════════════════════════════════
|
|
1003
|
+
async runQuickTest(strategyName, pair, changeDesc = '') {
|
|
1004
|
+
this.log('');
|
|
1005
|
+
this.log(dim(` Testing ${strategyName} on ${pair}...`));
|
|
1006
|
+
const args = [strategyName, '--pair', pair];
|
|
1007
|
+
if (changeDesc)
|
|
1008
|
+
args.push('--change', changeDesc);
|
|
1009
|
+
try {
|
|
1010
|
+
await runEngine('quick-test', args, (msg) => {
|
|
1011
|
+
if (msg.type === 'progress') {
|
|
1012
|
+
this.log(` ${dim(String(msg.msg))}`);
|
|
1013
|
+
}
|
|
1014
|
+
else if (msg.type === 'result') {
|
|
1015
|
+
const ret = msg.return_pct;
|
|
1016
|
+
const sharpe = msg.sharpe;
|
|
1017
|
+
const trades = msg.num_trades;
|
|
1018
|
+
const win = msg.win_rate;
|
|
1019
|
+
const dd = msg.max_drawdown;
|
|
1020
|
+
const pf = msg.profit_factor;
|
|
1021
|
+
this.log('');
|
|
1022
|
+
this.log(` ${bold('Quick test:')} ${colorNum(ret, '%')}, ${trades} trades, ${win}% win, ${colorNum(dd, '%')} DD, Sharpe ${colorNum(sharpe)}`);
|
|
1023
|
+
// Show delta if available
|
|
1024
|
+
if (msg.has_previous) {
|
|
1025
|
+
const delta = msg.delta;
|
|
1026
|
+
const parts = [];
|
|
1027
|
+
if (delta.return_pct)
|
|
1028
|
+
parts.push(`return ${delta.return_pct > 0 ? '↑' : '↓'}${Math.abs(delta.return_pct).toFixed(1)}%`);
|
|
1029
|
+
if (delta.sharpe)
|
|
1030
|
+
parts.push(`Sharpe ${delta.sharpe > 0 ? '↑' : '↓'}${Math.abs(delta.sharpe).toFixed(2)}`);
|
|
1031
|
+
if (delta.num_trades)
|
|
1032
|
+
parts.push(`trades ${delta.num_trades > 0 ? '↑' : '↓'}${Math.abs(delta.num_trades)}`);
|
|
1033
|
+
if (delta.max_drawdown)
|
|
1034
|
+
parts.push(`DD ${delta.max_drawdown > 0 ? '↑' : '↓'}${Math.abs(delta.max_drawdown).toFixed(1)}%`);
|
|
1035
|
+
if (parts.length > 0) {
|
|
1036
|
+
this.log(` ${dim('vs last:')} ${parts.join(', ')}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
this.log('');
|
|
1040
|
+
}
|
|
1041
|
+
else if (msg.type === 'error') {
|
|
1042
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
catch (e) {
|
|
1047
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1048
|
+
}
|
|
1049
|
+
return this.workbench(strategyName, pair);
|
|
1050
|
+
}
|
|
1051
|
+
async runFullValidate(strategyName, pair) {
|
|
1052
|
+
this.log('');
|
|
1053
|
+
await this.runPipeline(strategyName, pair);
|
|
1054
|
+
return this.workbench(strategyName, pair);
|
|
1055
|
+
}
|
|
1056
|
+
async showHistory(strategyName, pair) {
|
|
1057
|
+
this.log('');
|
|
1058
|
+
this.log(` ${bold('EXPERIMENT LOG:')} ${strategyName}`);
|
|
1059
|
+
this.log('');
|
|
1060
|
+
try {
|
|
1061
|
+
await runEngine('experiments', [strategyName, '--limit', '15'], (msg) => {
|
|
1062
|
+
if (msg.type === 'result') {
|
|
1063
|
+
const exps = msg.experiments || [];
|
|
1064
|
+
if (exps.length === 0) {
|
|
1065
|
+
this.log(dim(' No experiments yet. Press [t] in the workbench to start.'));
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
this.log(` ${dim('#'.padEnd(4))} ${dim('v'.padEnd(4))} ${dim('Pair'.padEnd(6))} ${dim('Return'.padEnd(10))} ${dim('Sharpe'.padEnd(9))} ${dim('Trades'.padEnd(8))} ${dim('Change')}`);
|
|
1069
|
+
this.log(` ${dim('─'.repeat(65))}`);
|
|
1070
|
+
for (const e of exps) {
|
|
1071
|
+
const ret = colorNum(e.return_pct, '%');
|
|
1072
|
+
const cleanRet = String(e.return_pct).slice(0, 6);
|
|
1073
|
+
this.log(` ${String(e.id).padEnd(4)} v${String(e.version).padEnd(3)} ${String(e.pair).padEnd(6)} ${ret.padEnd(18)} ${String(e.sharpe?.toFixed(2) || '—').padEnd(9)} ${String(e.num_trades).padEnd(8)} ${dim(String(e.change_description || ''))}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1081
|
+
}
|
|
1082
|
+
this.log('');
|
|
1083
|
+
this.log(` ${cyan('r')} Revert to a version`);
|
|
1084
|
+
this.log(` ${cyan('b')} Back to workbench`);
|
|
1085
|
+
this.log('');
|
|
1086
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
1087
|
+
if (choice === 'r') {
|
|
1088
|
+
const expId = await ask(` ${cyan('Experiment #')}: `);
|
|
1089
|
+
if (expId) {
|
|
1090
|
+
try {
|
|
1091
|
+
await runEngine('experiment-revert', [expId], (msg) => {
|
|
1092
|
+
if (msg.type === 'result') {
|
|
1093
|
+
this.log(` ${green('✔')} Reverted to experiment #${expId} (now v${msg.version})`);
|
|
1094
|
+
}
|
|
1095
|
+
else if (msg.type === 'error') {
|
|
1096
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
catch (e) {
|
|
1101
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return this.workbench(strategyName, pair);
|
|
1106
|
+
}
|
|
1107
|
+
// ═══════════════════════════════════════════
|
|
1108
|
+
// WORKBENCH — Strategy Mixer
|
|
1109
|
+
// ═══════════════════════════════════════════
|
|
1110
|
+
async mixerMenu(strategyName, pair) {
|
|
1111
|
+
this.log('');
|
|
1112
|
+
this.log(` ${bold('STRATEGY MIXER')} — combine components from validated strategies`);
|
|
1113
|
+
this.log('');
|
|
1114
|
+
// Get current config
|
|
1115
|
+
let config = {};
|
|
1116
|
+
try {
|
|
1117
|
+
await runEngine('workbench-show', [strategyName], (msg) => {
|
|
1118
|
+
if (msg.type === 'result')
|
|
1119
|
+
config = msg.config;
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* empty */ }
|
|
1123
|
+
const filters = config.filters || {};
|
|
1124
|
+
this.log(` Your base: ${bold(strategyName)}`);
|
|
1125
|
+
this.log('');
|
|
1126
|
+
this.log(` ${dim('FILTERS (toggle to add/remove):')}`);
|
|
1127
|
+
this.log('');
|
|
1128
|
+
const filterOptions = [
|
|
1129
|
+
{ key: 'hmm_filter', label: 'HMM crisis filter', source: 'workbench-builtin', desc: 'skip trades during crisis regimes (HMM, self-contained)' },
|
|
1130
|
+
{ key: 'rsi_confirmation', label: 'RSI confirmation', source: 'workbench-builtin', desc: 'require RSI < 40 for longs, > 60 for shorts' },
|
|
1131
|
+
{ key: 'volume_filter', label: 'Volume spike', source: 'custom', desc: 'require 1.5x average volume' },
|
|
1132
|
+
{ key: 'adx_trend', label: 'ADX trend', source: 'custom', desc: 'only trade when ADX > 25' },
|
|
1133
|
+
];
|
|
1134
|
+
for (let i = 0; i < filterOptions.length; i++) {
|
|
1135
|
+
const f = filterOptions[i];
|
|
1136
|
+
const on = filters[f.key] === true;
|
|
1137
|
+
this.log(` ${cyan(String(i + 1))} ${on ? green('☑') : dim('☐')} From ${bold(f.source)}: ${f.desc}`);
|
|
1138
|
+
}
|
|
1139
|
+
this.log('');
|
|
1140
|
+
this.log(` ${dim('Benchmarks — your strategy vs validated:')}`);
|
|
1141
|
+
this.log('');
|
|
1142
|
+
for (const s of STRATEGIES) {
|
|
1143
|
+
this.log(` ${green('★')} ${s.name.padEnd(22)} ${colorNum(s.ret, '%').padEnd(18)} Sharpe ${s.sharpe}`);
|
|
1144
|
+
}
|
|
1145
|
+
this.log('');
|
|
1146
|
+
this.log(` ${cyan('#')} Toggle a filter`);
|
|
1147
|
+
this.log(` ${cyan('t')} Quick test this combination`);
|
|
1148
|
+
this.log(` ${cyan('b')} Back to workbench`);
|
|
1149
|
+
this.log('');
|
|
1150
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
1151
|
+
const idx = parseInt(choice) - 1;
|
|
1152
|
+
if (idx >= 0 && idx < filterOptions.length) {
|
|
1153
|
+
const key = filterOptions[idx].key;
|
|
1154
|
+
filters[key] = !filters[key];
|
|
1155
|
+
config.filters = filters;
|
|
1156
|
+
const action = filters[key] ? 'enabled' : 'disabled';
|
|
1157
|
+
await this.saveAndRegenerate(strategyName, config, pair, `mixer: ${action} ${key}`);
|
|
1158
|
+
}
|
|
1159
|
+
else if (choice === 't') {
|
|
1160
|
+
await this.runQuickTest(strategyName, pair, 'mixer test');
|
|
1161
|
+
}
|
|
1162
|
+
if (choice === 'b') {
|
|
1163
|
+
return this.workbench(strategyName, pair);
|
|
1164
|
+
}
|
|
1165
|
+
return this.mixerMenu(strategyName, pair);
|
|
1166
|
+
}
|
|
1167
|
+
// ═══════════════════════════════════════════
|
|
1168
|
+
// 4. OPTIMIZE — Parameter sweep + apply
|
|
1169
|
+
// ═══════════════════════════════════════════
|
|
1170
|
+
async optimizeMenu() {
|
|
1171
|
+
this.log('');
|
|
1172
|
+
this.log(` ${bold('╔═══════════════════════════════════════════╗')}`);
|
|
1173
|
+
this.log(` ${bold('║ Strategy Optimizer ║')}`);
|
|
1174
|
+
this.log(` ${bold('╚═══════════════════════════════════════════╝')}`);
|
|
1175
|
+
const strategy = await this.pickStrategy('Strategy to optimize');
|
|
1176
|
+
if (!strategy)
|
|
1177
|
+
return this.mainMenu();
|
|
1178
|
+
const pair = (await ask(` ${cyan('Ticker')} ${dim('(BTC)')}: `) || 'BTC').replace('-PERP', '').replace('-perp', '').toUpperCase();
|
|
1179
|
+
this.log('');
|
|
1180
|
+
this.log(dim(` Running parameter sweep on ${strategy}...`));
|
|
1181
|
+
this.log('');
|
|
1182
|
+
// Run sweep via engine directly so we can capture the best params
|
|
1183
|
+
let bestParams = null;
|
|
1184
|
+
let bestMetrics = null;
|
|
1185
|
+
const isValidated = STRATEGIES.some(s => s.name === strategy);
|
|
1186
|
+
try {
|
|
1187
|
+
await runEngine('sweep', [strategy, '--pair', pair, '--top', '5'], (msg) => {
|
|
1188
|
+
if (msg.type === 'progress' && msg.msg) {
|
|
1189
|
+
// Compact progress: just combo count + ETA
|
|
1190
|
+
const msgStr = String(msg.msg);
|
|
1191
|
+
const match = msgStr.match(/Combo (\d+)\/(\d+)(.*?ETA \S+)?/);
|
|
1192
|
+
if (match) {
|
|
1193
|
+
const eta = match[3] ? match[3].trim() : '';
|
|
1194
|
+
process.stdout.write(`\x1b[2K\r ${dim(`Combo ${match[1]}/${match[2]}${eta ? ' — ' + eta : ''}`)}`);
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
process.stdout.write(`\x1b[2K\r ${dim(msgStr)}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
else if (msg.type === 'result') {
|
|
1201
|
+
process.stdout.write('\x1b[2K\r');
|
|
1202
|
+
const topEntries = msg.top;
|
|
1203
|
+
const total = msg.total_combos;
|
|
1204
|
+
const completed = msg.completed;
|
|
1205
|
+
this.log(dim(` Tested ${completed}/${total} combinations`));
|
|
1206
|
+
this.log('');
|
|
1207
|
+
if (topEntries && topEntries.length > 0) {
|
|
1208
|
+
bestParams = topEntries[0].params;
|
|
1209
|
+
bestMetrics = topEntries[0].metrics;
|
|
1210
|
+
this.log(dim(' ── Top Results ──'));
|
|
1211
|
+
this.log('');
|
|
1212
|
+
for (let i = 0; i < Math.min(topEntries.length, 5); i++) {
|
|
1213
|
+
const e = topEntries[i];
|
|
1214
|
+
const m = e.metrics;
|
|
1215
|
+
const ret = m.total_return_pct;
|
|
1216
|
+
const sharpe = m.sharpe_ratio;
|
|
1217
|
+
const marker = i === 0 ? green('★') : dim(String(i + 1));
|
|
1218
|
+
this.log(` ${marker} ${colorNum(ret, '%').padEnd(22)} Sharpe ${colorNum(sharpe)}`);
|
|
1219
|
+
const paramStr = Object.entries(e.params).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
1220
|
+
this.log(` ${dim(paramStr)}`);
|
|
1221
|
+
this.log('');
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else if (msg.type === 'error') {
|
|
1226
|
+
process.stdout.write('\x1b[2K\r');
|
|
1227
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1233
|
+
return this.mainMenu();
|
|
1234
|
+
}
|
|
1235
|
+
if (!bestParams) {
|
|
1236
|
+
this.log(dim(' No results from sweep.'));
|
|
1237
|
+
return this.mainMenu();
|
|
1238
|
+
}
|
|
1239
|
+
this.log(` ${bold('What next?')}`);
|
|
1240
|
+
this.log(` ${cyan('1')} Validate the top config with full research pipeline`);
|
|
1241
|
+
this.log(` ${cyan('2')} Run another sweep with different settings`);
|
|
1242
|
+
this.log(` ${cyan('3')} Back to main menu`);
|
|
1243
|
+
this.log('');
|
|
1244
|
+
const next = await ask(` ${cyan('>')} `);
|
|
1245
|
+
if (next === '1') {
|
|
1246
|
+
// Run the research pipeline with config overrides — no new strategy file needed
|
|
1247
|
+
this.log('');
|
|
1248
|
+
this.log(dim(` Validating ${strategy} with optimized parameters...`));
|
|
1249
|
+
this.log('');
|
|
1250
|
+
const overridesJson = JSON.stringify(bestParams);
|
|
1251
|
+
let finalGrade = '';
|
|
1252
|
+
const engineArgs = [strategy, '--pair', pair, '--config-overrides', overridesJson];
|
|
1253
|
+
await runEngine('research', engineArgs, (msg) => {
|
|
1254
|
+
if (msg.type === 'step') {
|
|
1255
|
+
this.log(` ${dim(`Step ${msg.step}/5`)} ${msg.msg}`);
|
|
1256
|
+
}
|
|
1257
|
+
else if (msg.type === 'step_done') {
|
|
1258
|
+
this.log(` ${green('✔')} ${msg.msg}`);
|
|
1259
|
+
}
|
|
1260
|
+
else if (msg.type === 'progress' && msg.msg) {
|
|
1261
|
+
this.log(` ${dim(String(msg.msg))}`);
|
|
1262
|
+
}
|
|
1263
|
+
else if (msg.type === 'result') {
|
|
1264
|
+
finalGrade = msg.grade;
|
|
1265
|
+
this.renderGradedResult(msg);
|
|
1266
|
+
}
|
|
1267
|
+
else if (msg.type === 'error') {
|
|
1268
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
// After validation, offer to save
|
|
1272
|
+
if (finalGrade === 'A' || finalGrade === 'B') {
|
|
1273
|
+
this.log(` ${green('★')} Optimization validated! Grade ${gradeColor(finalGrade)}`);
|
|
1274
|
+
this.log('');
|
|
1275
|
+
this.log(` ${cyan('1')} Save as a new strategy`);
|
|
1276
|
+
this.log(` ${cyan('2')} Back to main menu`);
|
|
1277
|
+
this.log('');
|
|
1278
|
+
const saveChoice = await ask(` ${cyan('>')} `);
|
|
1279
|
+
if (saveChoice === '1') {
|
|
1280
|
+
const saveName = await ask(` ${cyan('Strategy name')} ${dim('(snake_case)')}: `);
|
|
1281
|
+
if (saveName) {
|
|
1282
|
+
await this.saveOptimizedStrategy(strategy, saveName, bestParams);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
this.log(` ${dim('Optimization did not pass validation. Try different parameters.')}`);
|
|
1288
|
+
}
|
|
1289
|
+
return this.mainMenu();
|
|
1290
|
+
}
|
|
1291
|
+
else if (next === '2') {
|
|
1292
|
+
return this.optimizeMenu();
|
|
1293
|
+
}
|
|
1294
|
+
return this.mainMenu();
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Save an optimized strategy by copying the original .py file and replacing config defaults.
|
|
1298
|
+
*/
|
|
1299
|
+
async saveOptimizedStrategy(baseStrategy, newName, params) {
|
|
1300
|
+
// Use the Python engine to create the strategy file
|
|
1301
|
+
const paramsJson = JSON.stringify(params);
|
|
1302
|
+
try {
|
|
1303
|
+
await runEngine('save-optimized', [baseStrategy, newName, paramsJson], (msg) => {
|
|
1304
|
+
if (msg.type === 'result') {
|
|
1305
|
+
this.log(` ${green('✔')} Saved ${bold(newName)} with optimized parameters`);
|
|
1306
|
+
this.log(` ${dim(`File: ${msg.path}`)}`);
|
|
1307
|
+
}
|
|
1308
|
+
else if (msg.type === 'error') {
|
|
1309
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
catch (e) {
|
|
1314
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// ═══════════════════════════════════════════
|
|
1318
|
+
// 5. COMPARE — Head-to-head
|
|
1319
|
+
// ═══════════════════════════════════════════
|
|
1320
|
+
async compareMenu() {
|
|
1321
|
+
this.log('');
|
|
1322
|
+
this.log(` ${bold('╔═══════════════════════════════════════════╗')}`);
|
|
1323
|
+
this.log(` ${bold('║ Strategy Comparison ║')}`);
|
|
1324
|
+
this.log(` ${bold('╚═══════════════════════════════════════════╝')}`);
|
|
1325
|
+
this.log('');
|
|
1326
|
+
// Picker — dynamic from registry + workbench
|
|
1327
|
+
this.log(` ${cyan('1')} Compare all registered strategies`);
|
|
1328
|
+
this.log(` ${cyan('2')} Pick specific strategies`);
|
|
1329
|
+
this.log('');
|
|
1330
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
1331
|
+
let strategies;
|
|
1332
|
+
if (choice === '1') {
|
|
1333
|
+
// Fetch full registry, comma-join
|
|
1334
|
+
let allNames = [];
|
|
1335
|
+
try {
|
|
1336
|
+
await runEngine('strategies', [], (msg) => {
|
|
1337
|
+
if (msg.type === 'result') {
|
|
1338
|
+
const strats = msg.strategies || [];
|
|
1339
|
+
allNames = strats.map((s) => String(s.name));
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
catch { /* empty */ }
|
|
1344
|
+
if (allNames.length < 2) {
|
|
1345
|
+
this.log(` ${dim('Need at least 2 registered strategies. Create one: rift new my-strategy')}`);
|
|
1346
|
+
return this.mainMenu();
|
|
1347
|
+
}
|
|
1348
|
+
strategies = allNames.join(',');
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
const picked = await this.pickStrategy('Pick strategies to compare', true);
|
|
1352
|
+
strategies = picked || '';
|
|
1353
|
+
}
|
|
1354
|
+
if (!strategies)
|
|
1355
|
+
return this.mainMenu();
|
|
1356
|
+
const pair = (await ask(` ${cyan('Ticker')} ${dim('(BTC)')}: `) || 'BTC').replace('-PERP', '').replace('-perp', '').toUpperCase();
|
|
1357
|
+
this.log('');
|
|
1358
|
+
await this.config.runCommand('compare', [strategies, '--pair', pair]);
|
|
1359
|
+
// Visual summary
|
|
1360
|
+
this.log(` ${bold('Recommendation:')}`);
|
|
1361
|
+
this.log(` Run ${cyan('rift research <strategy> --pair <COIN>')} to validate any strategy`);
|
|
1362
|
+
this.log(` Run ${cyan('rift scan --pair <COIN>')} to discover predictive features`);
|
|
1363
|
+
this.log('');
|
|
1364
|
+
this.log(` ${bold('What next?')}`);
|
|
1365
|
+
this.log(` ${cyan('1')} Run full research on one of these`);
|
|
1366
|
+
this.log(` ${cyan('2')} Build a portfolio with these strategies`);
|
|
1367
|
+
this.log(` ${cyan('3')} Back to main menu`);
|
|
1368
|
+
this.log('');
|
|
1369
|
+
const next = await ask(` ${cyan('>')} `);
|
|
1370
|
+
if (next === '1')
|
|
1371
|
+
return this.testMenu();
|
|
1372
|
+
if (next === '2') {
|
|
1373
|
+
this.log(`\n ${dim('Run:')} ${cyan('rift portfolio backtest strategies/configs/portfolio_btc.yaml')}\n`);
|
|
1374
|
+
}
|
|
1375
|
+
return this.mainMenu();
|
|
1376
|
+
}
|
|
1377
|
+
// ═══════════════════════════════════════════
|
|
1378
|
+
// PIPELINE — Full validation
|
|
1379
|
+
// ═══════════════════════════════════════════
|
|
1380
|
+
async runPipeline(strategy, pair, tf, equity = 10000) {
|
|
1381
|
+
this.log(` ${bold('RIFT Research Pipeline')}`);
|
|
1382
|
+
this.log(` ${dim('─'.repeat(50))}`);
|
|
1383
|
+
this.log(` Strategy: ${bold(strategy)}`);
|
|
1384
|
+
this.log(` Pair: ${pair}`);
|
|
1385
|
+
if (tf)
|
|
1386
|
+
this.log(` Timeframe: ${tf}`);
|
|
1387
|
+
this.log(` ${dim('─'.repeat(50))}`);
|
|
1388
|
+
this.log('');
|
|
1389
|
+
const engineArgs = [strategy, '--pair', pair, '--equity', String(equity)];
|
|
1390
|
+
if (tf)
|
|
1391
|
+
engineArgs.push('--tf', tf);
|
|
1392
|
+
let finalGrade = '';
|
|
1393
|
+
await runEngine('research', engineArgs, (msg) => {
|
|
1394
|
+
if (msg.type === 'step') {
|
|
1395
|
+
this.log(` ${dim(`Step ${msg.step}/5`)} ${msg.msg}`);
|
|
1396
|
+
}
|
|
1397
|
+
else if (msg.type === 'step_done') {
|
|
1398
|
+
this.log(` ${green('✔')} ${msg.msg}`);
|
|
1399
|
+
}
|
|
1400
|
+
else if (msg.type === 'progress' && msg.msg) {
|
|
1401
|
+
this.log(` ${dim(String(msg.msg))}`);
|
|
1402
|
+
}
|
|
1403
|
+
else if (msg.type === 'result') {
|
|
1404
|
+
finalGrade = msg.grade;
|
|
1405
|
+
this.renderGradedResult(msg);
|
|
1406
|
+
}
|
|
1407
|
+
else if (msg.type === 'error') {
|
|
1408
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
// Post-pipeline guidance. When stdin isn't a TTY (README quickstart,
|
|
1412
|
+
// piped/scripted use, CI), printing an interactive menu and awaiting
|
|
1413
|
+
// input either hangs forever or — with </dev/null — drops the user
|
|
1414
|
+
// at an unanswerable prompt. Detect and print actionable copyable
|
|
1415
|
+
// commands instead, then exit cleanly.
|
|
1416
|
+
if (!process.stdin.isTTY) {
|
|
1417
|
+
this.log(` ${bold('Next steps:')}`);
|
|
1418
|
+
if (finalGrade === 'A' || finalGrade === 'B') {
|
|
1419
|
+
this.log(` ${cyan(`rift algo ${strategy} --pair ${pair}`)} ${dim('— go live with this strategy')}`);
|
|
1420
|
+
this.log(` ${cyan(`rift sweep ${strategy} --pair ${pair}`)} ${dim('— optimize parameters')}`);
|
|
1421
|
+
this.log(` ${cyan(`rift backtest ${strategy} --all-pairs --top 10`)} ${dim('— test other pairs')}`);
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
this.log(` ${cyan(`rift sweep ${strategy} --pair ${pair}`)} ${dim('— optimize parameters (may improve the grade)')}`);
|
|
1425
|
+
this.log(` ${cyan('rift strategies list')} ${dim('— see all available strategies')}`);
|
|
1426
|
+
this.log(` ${cyan(`rift research ${strategy} --pair <OTHER-COIN>`)} ${dim('— try a different pair')}`);
|
|
1427
|
+
}
|
|
1428
|
+
this.log('');
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
this.log(` ${bold('What next?')}`);
|
|
1432
|
+
if (finalGrade === 'A' || finalGrade === 'B') {
|
|
1433
|
+
this.log(` ${cyan('1')} Go live ${dim(`→ rift algo ${strategy} --pair ${pair}`)}`);
|
|
1434
|
+
this.log(` ${cyan('2')} Optimize parameters`);
|
|
1435
|
+
this.log(` ${cyan('3')} Test on different pairs`);
|
|
1436
|
+
this.log(` ${cyan('4')} Back to Research Lab`);
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
this.log(` ${cyan('1')} Optimize parameters ${dim('— might improve the grade')}`);
|
|
1440
|
+
this.log(` ${cyan('2')} Try a different strategy`);
|
|
1441
|
+
this.log(` ${cyan('3')} Try a different pair`);
|
|
1442
|
+
this.log(` ${cyan('4')} Back to Research Lab`);
|
|
1443
|
+
}
|
|
1444
|
+
this.log('');
|
|
1445
|
+
const next = await ask(` ${cyan('>')} `);
|
|
1446
|
+
if (finalGrade === 'A' || finalGrade === 'B') {
|
|
1447
|
+
if (next === '1') {
|
|
1448
|
+
this.log(`\n ${dim('Starting algo trading...')}\n`);
|
|
1449
|
+
await this.config.runCommand('algo', [strategy, '--pair', pair]);
|
|
1450
|
+
return this.mainMenu();
|
|
1451
|
+
}
|
|
1452
|
+
else if (next === '2')
|
|
1453
|
+
return this.optimizeMenu();
|
|
1454
|
+
else if (next === '3') {
|
|
1455
|
+
this.log('');
|
|
1456
|
+
await this.config.runCommand('backtest', [strategy, '--all-pairs', '--top', '10']);
|
|
1457
|
+
return this.mainMenu();
|
|
1458
|
+
}
|
|
1459
|
+
else if (next === '4')
|
|
1460
|
+
return this.mainMenu();
|
|
1461
|
+
else
|
|
1462
|
+
return this.mainMenu();
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
if (next === '1')
|
|
1466
|
+
return this.optimizeMenu();
|
|
1467
|
+
else if (next === '2')
|
|
1468
|
+
return this.testMenu();
|
|
1469
|
+
else if (next === '3') {
|
|
1470
|
+
const newPair = await ask(` ${cyan('New ticker')}: `);
|
|
1471
|
+
if (newPair) {
|
|
1472
|
+
return this.runPipeline(strategy, newPair.replace('-PERP', '').replace('-perp', '').toUpperCase(), tf, equity);
|
|
1473
|
+
}
|
|
1474
|
+
return this.mainMenu();
|
|
1475
|
+
}
|
|
1476
|
+
else if (next === '4')
|
|
1477
|
+
return this.mainMenu();
|
|
1478
|
+
else
|
|
1479
|
+
return this.mainMenu();
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
// ═══════════════════════════════════════════
|
|
1483
|
+
// GRADE RENDER
|
|
1484
|
+
// ═══════════════════════════════════════════
|
|
1485
|
+
renderGradedResult(msg) {
|
|
1486
|
+
const grade = msg.grade;
|
|
1487
|
+
const verdict = msg.verdict;
|
|
1488
|
+
const bt = msg.backtest;
|
|
1489
|
+
const wf = msg.walkforward;
|
|
1490
|
+
const mc = msg.montecarlo;
|
|
1491
|
+
const multi = msg.multi_pair;
|
|
1492
|
+
const iw = 53;
|
|
1493
|
+
const row = boxRow(iw);
|
|
1494
|
+
const rr = resultRow(iw);
|
|
1495
|
+
this.log('');
|
|
1496
|
+
// Grade banner
|
|
1497
|
+
const gradeText = `GRADE: ${gradeColor(grade)}`;
|
|
1498
|
+
this.log(boldBoxTop(iw + 2));
|
|
1499
|
+
this.log(` ${bold('║')}${padEndVis(' '.repeat(Math.floor((iw - 7) / 2)) + gradeText, iw + 1)}${bold('║')}`);
|
|
1500
|
+
this.log(boldBoxBottom(iw + 2));
|
|
1501
|
+
this.log('');
|
|
1502
|
+
if (grade === 'A')
|
|
1503
|
+
this.log(` ${green(verdict)}`);
|
|
1504
|
+
else if (grade === 'B')
|
|
1505
|
+
this.log(` ${cyan(verdict)}`);
|
|
1506
|
+
else if (grade === 'C')
|
|
1507
|
+
this.log(` ${yellow(verdict)}`);
|
|
1508
|
+
else
|
|
1509
|
+
this.log(` ${red(verdict)}`);
|
|
1510
|
+
this.log('');
|
|
1511
|
+
this.log(boxTop(iw));
|
|
1512
|
+
// Backtest
|
|
1513
|
+
if (bt) {
|
|
1514
|
+
this.log(row(`${bold('BACKTEST')}`));
|
|
1515
|
+
this.log(boxDivider(iw));
|
|
1516
|
+
this.log(rr('Return', colorNum(bt.return_pct, '%')));
|
|
1517
|
+
this.log(rr('Sharpe', colorNum(bt.sharpe)));
|
|
1518
|
+
this.log(rr('Profit Factor', String(bt.profit_factor)));
|
|
1519
|
+
this.log(rr('Max Drawdown', colorNum(bt.max_drawdown_pct, '%')));
|
|
1520
|
+
this.log(rr('Win Rate', `${bt.win_rate}%`));
|
|
1521
|
+
this.log(rr('Trades', String(bt.num_trades)));
|
|
1522
|
+
}
|
|
1523
|
+
// Walk-Forward
|
|
1524
|
+
if (wf && !wf.error) {
|
|
1525
|
+
this.log(boxDivider(iw));
|
|
1526
|
+
this.log(row(`${bold('WALK-FORWARD')}`));
|
|
1527
|
+
this.log(boxDivider(iw));
|
|
1528
|
+
const deg = wf.degradation_ratio;
|
|
1529
|
+
const degLabel = deg >= 0.7 ? green('ROBUST') : deg >= 0.4 ? yellow('MODERATE') : deg > 0 ? red('WEAK') : red('OVERFIT');
|
|
1530
|
+
this.log(rr('Degradation', `${deg} — ${degLabel}`));
|
|
1531
|
+
this.log(rr('Profitable Windows', `${wf.profitable_windows}%`));
|
|
1532
|
+
this.log(rr('Combined OOS Return', colorNum(wf.combined_oos_return, '%')));
|
|
1533
|
+
}
|
|
1534
|
+
// Monte Carlo
|
|
1535
|
+
if (mc && !mc.error) {
|
|
1536
|
+
this.log(boxDivider(iw));
|
|
1537
|
+
this.log(row(`${bold('MONTE CARLO')}`));
|
|
1538
|
+
this.log(boxDivider(iw));
|
|
1539
|
+
const probColor = mc.prob_profit >= 85 ? green : mc.prob_profit >= 60 ? yellow : red;
|
|
1540
|
+
this.log(rr('Profit Probability', probColor(`${mc.prob_profit}%`)));
|
|
1541
|
+
this.log(rr('Ruin Probability', mc.prob_ruin === 0 ? green(`${mc.prob_ruin}%`) : red(`${mc.prob_ruin}%`)));
|
|
1542
|
+
this.log(rr('Worst Case (5th)', colorNum(mc.p5, '%')));
|
|
1543
|
+
this.log(rr('Median', colorNum(mc.p50, '%')));
|
|
1544
|
+
}
|
|
1545
|
+
// Multi-pair
|
|
1546
|
+
if (multi && multi.length > 0) {
|
|
1547
|
+
this.log(boxDivider(iw));
|
|
1548
|
+
this.log(row(`${bold('MULTI-PAIR TEST')}`));
|
|
1549
|
+
this.log(boxDivider(iw));
|
|
1550
|
+
for (const r of multi) {
|
|
1551
|
+
const marker = r.return_pct > 0 ? green('✔') : red('✘');
|
|
1552
|
+
this.log(row(` ${marker} ${r.pair.padEnd(10)} ${padEndVis(colorNum(r.return_pct, '%'), 14)} Sharpe ${colorNum(r.sharpe)}`));
|
|
1553
|
+
}
|
|
1554
|
+
const profitable = multi.filter(r => r.return_pct > 0).length;
|
|
1555
|
+
this.log(row(` ${dim(`Profitable on ${profitable}/${multi.length} additional pairs`)}`));
|
|
1556
|
+
}
|
|
1557
|
+
this.log(boxBottom(iw));
|
|
1558
|
+
this.log('');
|
|
1559
|
+
}
|
|
1560
|
+
// ═══════════════════════════════════════════
|
|
1561
|
+
// EXPLORE SUBMENUS
|
|
1562
|
+
// ═══════════════════════════════════════════
|
|
1563
|
+
// ─── 1. Indicator catalog ────────────────────
|
|
1564
|
+
async indicatorCatalogMenu(category = '') {
|
|
1565
|
+
let data = null;
|
|
1566
|
+
try {
|
|
1567
|
+
const args = ['indicators'];
|
|
1568
|
+
if (category)
|
|
1569
|
+
args.push('--category', category);
|
|
1570
|
+
await runEngine(args[0], args.slice(1), (msg) => {
|
|
1571
|
+
if (msg.type === 'result')
|
|
1572
|
+
data = msg;
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
catch (e) {
|
|
1576
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1577
|
+
return this.exploreMenu();
|
|
1578
|
+
}
|
|
1579
|
+
if (!data)
|
|
1580
|
+
return this.exploreMenu();
|
|
1581
|
+
const d = data;
|
|
1582
|
+
this.log('');
|
|
1583
|
+
const title = category ? `INDICATOR CATALOG — ${category}` : 'INDICATOR CATALOG';
|
|
1584
|
+
this.log(` ${bold(title)} ${dim(`(${d.total} indicators)`)}`);
|
|
1585
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
1586
|
+
const renderItems = (items) => {
|
|
1587
|
+
for (const it of items) {
|
|
1588
|
+
const params = it.params.map(p => p.default !== null && p.default !== undefined ? `${p.name}=${p.default}` : p.name).join(', ');
|
|
1589
|
+
const paramStr = params ? `(${params})` : '';
|
|
1590
|
+
this.log(` ${cyan(it.name.padEnd(22))} ${dim(paramStr.padEnd(30))} ${dim(it.description)}`);
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
for (const [cat, items] of Object.entries(d.categories)) {
|
|
1594
|
+
this.log('');
|
|
1595
|
+
this.log(` ${bold(cat.toUpperCase())} ${dim(`(${items.length})`)}`);
|
|
1596
|
+
renderItems(items);
|
|
1597
|
+
}
|
|
1598
|
+
if (d.uncategorized.length > 0) {
|
|
1599
|
+
this.log('');
|
|
1600
|
+
this.log(` ${yellow('UNCATEGORIZED')} ${dim(`(${d.uncategorized.length} — update _INDICATOR_CATEGORIES)`)}`);
|
|
1601
|
+
renderItems(d.uncategorized);
|
|
1602
|
+
}
|
|
1603
|
+
this.log('');
|
|
1604
|
+
if (!category) {
|
|
1605
|
+
this.log(` ${dim('Filter:')} ${cyan('1')} trend ${cyan('2')} momentum ${cyan('3')} volatility ${cyan('4')} volume`);
|
|
1606
|
+
this.log(` ${dim(' ')} ${cyan('5')} structure ${cyan('6')} adaptive ${cyan('7')} cross_asset ${cyan('8')} order_flow`);
|
|
1607
|
+
this.log(` ${dim(' ')} ${cyan('s')} search by name/description ${cyan('b')} back`);
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
this.log(` ${cyan('a')} show all categories ${cyan('b')} back`);
|
|
1611
|
+
}
|
|
1612
|
+
this.log('');
|
|
1613
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
1614
|
+
const catMap = {
|
|
1615
|
+
'1': 'trend', '2': 'momentum', '3': 'volatility', '4': 'volume',
|
|
1616
|
+
'5': 'structure', '6': 'adaptive', '7': 'cross_asset', '8': 'order_flow',
|
|
1617
|
+
};
|
|
1618
|
+
if (catMap[choice])
|
|
1619
|
+
return this.indicatorCatalogMenu(catMap[choice]);
|
|
1620
|
+
if (choice === 'a' || choice === 'A')
|
|
1621
|
+
return this.indicatorCatalogMenu('');
|
|
1622
|
+
if (choice === 's' || choice === 'S')
|
|
1623
|
+
return this.indicatorSearchMenu();
|
|
1624
|
+
return this.exploreMenu();
|
|
1625
|
+
}
|
|
1626
|
+
async indicatorSearchMenu() {
|
|
1627
|
+
const q = await ask(` ${cyan('Search term')}: `);
|
|
1628
|
+
if (!q)
|
|
1629
|
+
return this.exploreMenu();
|
|
1630
|
+
try {
|
|
1631
|
+
await runEngine('indicators', ['--search', q], (msg) => {
|
|
1632
|
+
if (msg.type !== 'result')
|
|
1633
|
+
return;
|
|
1634
|
+
const d = msg;
|
|
1635
|
+
this.log('');
|
|
1636
|
+
this.log(` ${bold('SEARCH:')} "${q}" ${dim(`(${d.total} matches)`)}`);
|
|
1637
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
1638
|
+
for (const [cat, items] of Object.entries(d.categories)) {
|
|
1639
|
+
for (const it of items) {
|
|
1640
|
+
const params = it.params.map((p) => p.default !== null && p.default !== undefined ? `${p.name}=${p.default}` : p.name).join(', ');
|
|
1641
|
+
const paramStr = params ? `(${params})` : '';
|
|
1642
|
+
this.log(` ${cyan(it.name.padEnd(22))} ${dim(cat.padEnd(14))} ${dim(paramStr.padEnd(28))} ${dim(it.description)}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
this.log('');
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
catch (e) {
|
|
1649
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1650
|
+
}
|
|
1651
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1652
|
+
return this.exploreMenu();
|
|
1653
|
+
}
|
|
1654
|
+
// ─── 4. Signal forensics ─────────────────────
|
|
1655
|
+
async signalForensicsMenu() {
|
|
1656
|
+
this.log('');
|
|
1657
|
+
this.log(` ${bold('SIGNAL FORENSICS')}`);
|
|
1658
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
1659
|
+
this.log('');
|
|
1660
|
+
this.log(` ${cyan('1')} Signal stats ${dim('hit rate + edge per signal on a coin')}`);
|
|
1661
|
+
this.log(` ${cyan('2')} Signal decay ${dim('does the signal lose edge over time?')}`);
|
|
1662
|
+
this.log(` ${cyan('3')} Signal backfill ${dim('compute missing signal series from cache')}`);
|
|
1663
|
+
this.log(` ${cyan('b')} Back`);
|
|
1664
|
+
this.log('');
|
|
1665
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
1666
|
+
if (choice === 'b' || choice === 'B')
|
|
1667
|
+
return this.exploreMenu();
|
|
1668
|
+
const coin = (await ask(` ${cyan('Coin')} ${dim('(BTC)')}: `) || 'BTC').toUpperCase();
|
|
1669
|
+
const tf = await ask(` ${cyan('Timeframe')} ${dim('(1h)')}: `) || '1h';
|
|
1670
|
+
let cmd = '';
|
|
1671
|
+
let args = [];
|
|
1672
|
+
if (choice === '1') {
|
|
1673
|
+
cmd = 'signal-stats';
|
|
1674
|
+
args = ['--pair', coin, '--tf', tf];
|
|
1675
|
+
}
|
|
1676
|
+
else if (choice === '2') {
|
|
1677
|
+
cmd = 'signal-decay';
|
|
1678
|
+
args = ['--pair', coin, '--tf', tf];
|
|
1679
|
+
}
|
|
1680
|
+
else if (choice === '3') {
|
|
1681
|
+
cmd = 'signal-backfill';
|
|
1682
|
+
args = ['--pair', coin, '--tf', tf];
|
|
1683
|
+
}
|
|
1684
|
+
else
|
|
1685
|
+
return this.signalForensicsMenu();
|
|
1686
|
+
this.log('');
|
|
1687
|
+
this.log(dim(` Running ${cmd}...`));
|
|
1688
|
+
try {
|
|
1689
|
+
await runEngine(cmd, args, (msg) => {
|
|
1690
|
+
if (msg.type === 'result') {
|
|
1691
|
+
// Print structured JSON for now; per-command rendering can come later.
|
|
1692
|
+
this.log(JSON.stringify(msg, null, 2));
|
|
1693
|
+
}
|
|
1694
|
+
else if (msg.type === 'error') {
|
|
1695
|
+
this.log(` ${red('✘')} ${msg.msg}`);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
catch (e) {
|
|
1700
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1701
|
+
}
|
|
1702
|
+
this.log('');
|
|
1703
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1704
|
+
return this.signalForensicsMenu();
|
|
1705
|
+
}
|
|
1706
|
+
// ─── 5. Funding rate browser ─────────────────
|
|
1707
|
+
async fundingBrowserMenu() {
|
|
1708
|
+
let data = null;
|
|
1709
|
+
try {
|
|
1710
|
+
await runEngine('funding-browser', ['--top', '20'], (msg) => {
|
|
1711
|
+
if (msg.type === 'result')
|
|
1712
|
+
data = msg;
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
catch (e) {
|
|
1716
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1717
|
+
return this.exploreMenu();
|
|
1718
|
+
}
|
|
1719
|
+
if (!data)
|
|
1720
|
+
return this.exploreMenu();
|
|
1721
|
+
const rows = (data.coins || []);
|
|
1722
|
+
this.log('');
|
|
1723
|
+
this.log(` ${bold('FUNDING RATE BROWSER')} ${dim(`(${data.lookback_days}d stats, sorted by |current rate|)`)}`);
|
|
1724
|
+
this.log(` ${dim('─'.repeat(76))}`);
|
|
1725
|
+
if (rows.length === 0) {
|
|
1726
|
+
this.log('');
|
|
1727
|
+
this.log(` ${yellow('!')} No funding data cached yet.`);
|
|
1728
|
+
this.log('');
|
|
1729
|
+
this.log(` ${dim('To populate:')}`);
|
|
1730
|
+
this.log(` ${cyan('rift fetch BTC --tf 1h')} ${dim('— fetches candles + funding (free, HL info)')}`);
|
|
1731
|
+
this.log(` ${cyan('rift sync --include-funding')} ${dim('— full historical (requires AWS for HL S3)')}`);
|
|
1732
|
+
this.log('');
|
|
1733
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1734
|
+
return this.exploreMenu();
|
|
1735
|
+
}
|
|
1736
|
+
this.log(` ${dim('coin'.padEnd(10))} ${dim('current/hr'.padStart(12))} ${dim('mean/hr'.padStart(12))} ${dim('min'.padStart(12))} ${dim('max'.padStart(12))} ${dim('zscore'.padStart(8))}`);
|
|
1737
|
+
for (const r of rows) {
|
|
1738
|
+
const cur = (r.current_pct_per_hour * 1).toFixed(4) + '%';
|
|
1739
|
+
const mean = (r.mean_rate * 100).toFixed(4) + '%';
|
|
1740
|
+
const min = (r.min_rate * 100).toFixed(4) + '%';
|
|
1741
|
+
const max = (r.max_rate * 100).toFixed(4) + '%';
|
|
1742
|
+
const z = r.zscore.toFixed(2);
|
|
1743
|
+
const curColor = r.current_rate > 0 ? green : r.current_rate < 0 ? red : dim;
|
|
1744
|
+
const zColor = Math.abs(r.zscore) >= 2 ? yellow : dim;
|
|
1745
|
+
this.log(` ${cyan(r.coin.padEnd(10))} ${curColor(cur.padStart(12))} ${dim(mean.padStart(12))} ${dim(min.padStart(12))} ${dim(max.padStart(12))} ${zColor(z.padStart(8))}`);
|
|
1746
|
+
}
|
|
1747
|
+
this.log('');
|
|
1748
|
+
this.log(dim(' positive = longs pay shorts (longs overcrowded)'));
|
|
1749
|
+
this.log(dim(' |z|≥2 = currently extreme vs the trailing window'));
|
|
1750
|
+
this.log('');
|
|
1751
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1752
|
+
return this.exploreMenu();
|
|
1753
|
+
}
|
|
1754
|
+
// ─── 6. Order flow browser ───────────────────
|
|
1755
|
+
async orderFlowBrowserMenu() {
|
|
1756
|
+
let data = null;
|
|
1757
|
+
try {
|
|
1758
|
+
await runEngine('order-flow', ['--top', '20'], (msg) => {
|
|
1759
|
+
if (msg.type === 'result')
|
|
1760
|
+
data = msg;
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
catch (e) {
|
|
1764
|
+
this.log(` ${red('✘')} ${e.message}`);
|
|
1765
|
+
return this.exploreMenu();
|
|
1766
|
+
}
|
|
1767
|
+
if (!data)
|
|
1768
|
+
return this.exploreMenu();
|
|
1769
|
+
const rows = (data.coins || []);
|
|
1770
|
+
this.log('');
|
|
1771
|
+
this.log(` ${bold('ORDER FLOW BROWSER')} ${dim(`(${data.lookback_hours}h, sorted by |imbalance|)`)}`);
|
|
1772
|
+
this.log(` ${dim('─'.repeat(80))}`);
|
|
1773
|
+
if (rows.length === 0) {
|
|
1774
|
+
this.log('');
|
|
1775
|
+
this.log(` ${yellow('!')} No fill data cached yet.`);
|
|
1776
|
+
this.log('');
|
|
1777
|
+
this.log(` ${dim('Fill data only comes from HL\'s S3 archive (requester-pays).')}`);
|
|
1778
|
+
this.log(` ${dim('To populate:')}`);
|
|
1779
|
+
this.log(` ${cyan('rift sync --coins BTC --include-fills')} ${dim('— requires AWS credentials')}`);
|
|
1780
|
+
this.log('');
|
|
1781
|
+
this.log(` ${dim('No free path exists for order-flow data (HL info endpoint')}`);
|
|
1782
|
+
this.log(` ${dim('doesn\'t expose per-fill data, only aggregated candles).')}`);
|
|
1783
|
+
this.log('');
|
|
1784
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1785
|
+
return this.exploreMenu();
|
|
1786
|
+
}
|
|
1787
|
+
this.log(` ${dim('coin'.padEnd(8))} ${dim('fills'.padStart(8))} ${dim('imbalance'.padStart(12))} ${dim('taker'.padStart(8))} ${dim('opens'.padStart(12))} ${dim('closes'.padStart(12))} ${dim('net flow'.padStart(12))}`);
|
|
1788
|
+
for (const r of rows) {
|
|
1789
|
+
const imb = (r.imbalance_pct).toFixed(2) + '%';
|
|
1790
|
+
const taker = isNaN(r.taker_ratio) ? '—' : (r.taker_ratio * 100).toFixed(1) + '%';
|
|
1791
|
+
const opens = isNaN(r.opens) ? '—' : r.opens.toFixed(0);
|
|
1792
|
+
const closes = isNaN(r.closes) ? '—' : r.closes.toFixed(0);
|
|
1793
|
+
const netflow = isNaN(r.net_flow) ? '—' : r.net_flow.toFixed(0);
|
|
1794
|
+
const imbColor = r.imbalance > 0.02 ? green : r.imbalance < -0.02 ? red : dim;
|
|
1795
|
+
this.log(` ${cyan(r.coin.padEnd(8))} ${dim(String(r.fills).padStart(8))} ${imbColor(imb.padStart(12))} ${dim(taker.padStart(8))} ${dim(opens.padStart(12))} ${dim(closes.padStart(12))} ${dim(netflow.padStart(12))}`);
|
|
1796
|
+
}
|
|
1797
|
+
this.log('');
|
|
1798
|
+
this.log(dim(' imbalance > 0 = more buy-aggressor volume; < 0 = more sell-aggressor'));
|
|
1799
|
+
this.log(dim(' taker = % of fills that crossed the spread (aggressive)'));
|
|
1800
|
+
this.log(dim(' net flow > 0 = positions being opened; < 0 = being closed'));
|
|
1801
|
+
this.log('');
|
|
1802
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1803
|
+
return this.exploreMenu();
|
|
1804
|
+
}
|
|
1805
|
+
// ─── 7. Cross-asset relationships ────────────
|
|
1806
|
+
async crossAssetMenu() {
|
|
1807
|
+
// Default coin list = whatever's actually cached at 1h (no hardcoded
|
|
1808
|
+
// RIFT-team assumptions). OSS users with only BTC cached will see
|
|
1809
|
+
// BTC and a helpful hint to fetch more.
|
|
1810
|
+
let defaultCoins = 'BTC';
|
|
1811
|
+
try {
|
|
1812
|
+
const fs = await import('node:fs');
|
|
1813
|
+
const path = await import('node:path');
|
|
1814
|
+
const dataDir = path.join(process.env.HOME || '~', '.rift', 'data');
|
|
1815
|
+
if (fs.existsSync(dataDir)) {
|
|
1816
|
+
const coins = [];
|
|
1817
|
+
for (const entry of fs.readdirSync(dataDir)) {
|
|
1818
|
+
if (entry.startsWith('_'))
|
|
1819
|
+
continue;
|
|
1820
|
+
const candleFile = path.join(dataDir, entry, '1h', 'candles.parquet');
|
|
1821
|
+
if (fs.existsSync(candleFile))
|
|
1822
|
+
coins.push(entry);
|
|
1823
|
+
}
|
|
1824
|
+
if (coins.length > 0)
|
|
1825
|
+
defaultCoins = coins.slice(0, 8).join(',');
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch { /* fall back to BTC */ }
|
|
1829
|
+
if (defaultCoins === 'BTC') {
|
|
1830
|
+
this.log('');
|
|
1831
|
+
this.log(` ${dim('Only BTC cached at 1h. For a meaningful cross-asset matrix, fetch a few coins first:')}`);
|
|
1832
|
+
this.log(` ${cyan('rift fetch ETH --tf 1h')}`);
|
|
1833
|
+
this.log(` ${cyan('rift fetch SOL --tf 1h')}`);
|
|
1834
|
+
this.log(` ${cyan('rift fetch HYPE --tf 1h')}`);
|
|
1835
|
+
}
|
|
1836
|
+
const coinsInput = await ask(` ${cyan('Coins')} ${dim(`(${defaultCoins})`)}: `);
|
|
1837
|
+
const coins = coinsInput || defaultCoins;
|
|
1838
|
+
const lookbackInput = await ask(` ${cyan('Lookback candles')} ${dim('(720 = 30d of 1h)')}: `);
|
|
1839
|
+
const lookback = lookbackInput || '720';
|
|
1840
|
+
this.log('');
|
|
1841
|
+
this.log(dim(' Computing correlation + lead-lag + beta...'));
|
|
1842
|
+
let data = null;
|
|
1843
|
+
let errMsg = '';
|
|
1844
|
+
try {
|
|
1845
|
+
await runEngine('cross-asset', ['--coins', coins, '--lookback', lookback], (msg) => {
|
|
1846
|
+
if (msg.type === 'result')
|
|
1847
|
+
data = msg;
|
|
1848
|
+
else if (msg.type === 'error')
|
|
1849
|
+
errMsg = String(msg.msg);
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
catch (e) {
|
|
1853
|
+
errMsg = e.message;
|
|
1854
|
+
}
|
|
1855
|
+
if (!data) {
|
|
1856
|
+
this.log('');
|
|
1857
|
+
this.log(` ${yellow('!')} ${errMsg || 'No data returned.'}`);
|
|
1858
|
+
this.log('');
|
|
1859
|
+
this.log(` ${dim('Fetch candle data first:')}`);
|
|
1860
|
+
this.log(` ${cyan('rift fetch <COIN> --tf 1h')} ${dim('— for each coin you want in the matrix')}`);
|
|
1861
|
+
this.log('');
|
|
1862
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1863
|
+
return this.exploreMenu();
|
|
1864
|
+
}
|
|
1865
|
+
this.log('');
|
|
1866
|
+
this.log(` ${bold('CROSS-ASSET MATRIX')} ${dim(`(${data.lookback_candles} ${data.tf} candles, benchmark=${data.benchmark})`)}`);
|
|
1867
|
+
this.log(` ${dim('─'.repeat(70))}`);
|
|
1868
|
+
const available = data.available_coins;
|
|
1869
|
+
const skipped = data.skipped;
|
|
1870
|
+
// Correlation matrix
|
|
1871
|
+
this.log('');
|
|
1872
|
+
this.log(` ${bold('Correlation matrix')} ${dim('(log returns)')}`);
|
|
1873
|
+
const header = ' ' + available.map(c => c.padStart(8)).join('');
|
|
1874
|
+
this.log(` ${dim(header)}`);
|
|
1875
|
+
for (const ci of available) {
|
|
1876
|
+
const row = available.map(cj => {
|
|
1877
|
+
const v = data.corr[ci][cj];
|
|
1878
|
+
const s = v.toFixed(2).padStart(8);
|
|
1879
|
+
if (ci === cj)
|
|
1880
|
+
return dim(s);
|
|
1881
|
+
if (Math.abs(v) >= 0.7)
|
|
1882
|
+
return green(s);
|
|
1883
|
+
if (Math.abs(v) >= 0.4)
|
|
1884
|
+
return cyan(s);
|
|
1885
|
+
if (Math.abs(v) >= 0.2)
|
|
1886
|
+
return dim(s);
|
|
1887
|
+
return dim(s);
|
|
1888
|
+
}).join('');
|
|
1889
|
+
this.log(` ${cyan(ci.padEnd(7))}${row}`);
|
|
1890
|
+
}
|
|
1891
|
+
// Lead-lag
|
|
1892
|
+
this.log('');
|
|
1893
|
+
this.log(` ${bold('Lead-lag vs')} ${cyan(data.benchmark)} ${dim('(positive lag = benchmark leads)')}`);
|
|
1894
|
+
for (const ll of data.lead_lag) {
|
|
1895
|
+
const lagStr = ll.best_lag > 0 ? `+${ll.best_lag}` : String(ll.best_lag);
|
|
1896
|
+
const corrColor = Math.abs(ll.best_corr) >= 0.5 ? green : Math.abs(ll.best_corr) >= 0.3 ? cyan : dim;
|
|
1897
|
+
this.log(` ${cyan(ll.coin.padEnd(8))} best lag=${lagStr.padStart(3)} corr=${corrColor(ll.best_corr.toFixed(3))}`);
|
|
1898
|
+
}
|
|
1899
|
+
// Beta
|
|
1900
|
+
this.log('');
|
|
1901
|
+
this.log(` ${bold('Beta vs')} ${cyan(data.benchmark)} ${dim('(>1 = more volatile, <1 = less)')}`);
|
|
1902
|
+
for (const b of data.beta) {
|
|
1903
|
+
const betaColor = b.beta > 1.5 ? red : b.beta > 1 ? yellow : b.beta < 0.5 ? cyan : dim;
|
|
1904
|
+
this.log(` ${cyan(b.coin.padEnd(8))} β = ${betaColor(b.beta.toFixed(3))}`);
|
|
1905
|
+
}
|
|
1906
|
+
if (skipped.length > 0) {
|
|
1907
|
+
this.log('');
|
|
1908
|
+
this.log(dim(' Skipped:'));
|
|
1909
|
+
for (const s of skipped) {
|
|
1910
|
+
this.log(` ${dim(s.coin)}: ${dim(s.reason)}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
this.log('');
|
|
1914
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1915
|
+
return this.exploreMenu();
|
|
1916
|
+
}
|
|
1917
|
+
// ─── 8. Regime browser ───────────────────────
|
|
1918
|
+
async regimeBrowserMenu() {
|
|
1919
|
+
const coin = (await ask(` ${cyan('Coin')} ${dim('(BTC)')}: `) || 'BTC').toUpperCase();
|
|
1920
|
+
const tf = await ask(` ${cyan('Timeframe')} ${dim('(1h)')}: `) || '1h';
|
|
1921
|
+
this.log('');
|
|
1922
|
+
this.log(dim(` Classifying regime for ${coin} ${tf}...`));
|
|
1923
|
+
let data = null;
|
|
1924
|
+
let errMsg = '';
|
|
1925
|
+
try {
|
|
1926
|
+
await runEngine('regime', ['--coin', coin, '--tf', tf], (msg) => {
|
|
1927
|
+
if (msg.type === 'result')
|
|
1928
|
+
data = msg;
|
|
1929
|
+
else if (msg.type === 'error')
|
|
1930
|
+
errMsg = String(msg.msg);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
catch (e) {
|
|
1934
|
+
errMsg = e.message;
|
|
1935
|
+
}
|
|
1936
|
+
if (!data) {
|
|
1937
|
+
this.log('');
|
|
1938
|
+
this.log(` ${yellow('!')} ${errMsg || `No ${coin} ${tf} data cached.`}`);
|
|
1939
|
+
this.log('');
|
|
1940
|
+
this.log(` ${dim('Fetch it:')} ${cyan(`rift fetch ${coin} --tf ${tf}`)}`);
|
|
1941
|
+
this.log('');
|
|
1942
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1943
|
+
return this.exploreMenu();
|
|
1944
|
+
}
|
|
1945
|
+
const cur = data.current;
|
|
1946
|
+
const volColor = cur.vol_regime === 'high' ? red : cur.vol_regime === 'low' ? cyan : dim;
|
|
1947
|
+
const trendColor = cur.trend_regime === 'bull' ? green : cur.trend_regime === 'bear' ? red : dim;
|
|
1948
|
+
this.log('');
|
|
1949
|
+
this.log(` ${bold('REGIME BROWSER')} ${dim(`(${data.coin} ${data.tf}, ${data.candles_analyzed} candles)`)}`);
|
|
1950
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
1951
|
+
this.log('');
|
|
1952
|
+
this.log(` ${bold('Right now:')}`);
|
|
1953
|
+
this.log(` Vol regime: ${volColor(cur.vol_regime.toUpperCase())} ${dim(`ATR ${cur.atr.toFixed(2)}`)}`);
|
|
1954
|
+
this.log(` Trend regime: ${trendColor(cur.trend_regime.toUpperCase())} ${dim(`ADX ${cur.adx.toFixed(1)} +DI ${cur.plus_di.toFixed(1)} -DI ${cur.minus_di.toFixed(1)}`)}`);
|
|
1955
|
+
this.log(` Last close: ${bold('$' + cur.close.toLocaleString())}`);
|
|
1956
|
+
this.log('');
|
|
1957
|
+
this.log(` ${bold('Historical breakdown')} ${dim(`(% of analyzed candles)`)}`);
|
|
1958
|
+
this.log(` Vol:`);
|
|
1959
|
+
const vb = data.vol_breakdown_pct;
|
|
1960
|
+
for (const [k, v] of Object.entries(vb)) {
|
|
1961
|
+
const c = k === 'high' ? red : k === 'low' ? cyan : dim;
|
|
1962
|
+
const barLen = Math.round(v / 2);
|
|
1963
|
+
this.log(` ${c(k.padEnd(8))} ${c('█'.repeat(barLen))} ${dim(v.toFixed(1) + '%')}`);
|
|
1964
|
+
}
|
|
1965
|
+
this.log(` Trend:`);
|
|
1966
|
+
const tb = data.trend_breakdown_pct;
|
|
1967
|
+
for (const [k, v] of Object.entries(tb)) {
|
|
1968
|
+
const c = k === 'bull' ? green : k === 'bear' ? red : dim;
|
|
1969
|
+
const barLen = Math.round(v / 2);
|
|
1970
|
+
this.log(` ${c(k.padEnd(8))} ${c('█'.repeat(barLen))} ${dim(v.toFixed(1) + '%')}`);
|
|
1971
|
+
}
|
|
1972
|
+
this.log('');
|
|
1973
|
+
await ask(` ${dim('Press Enter to continue')} `);
|
|
1974
|
+
return this.exploreMenu();
|
|
1975
|
+
}
|
|
1976
|
+
}
|