@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,195 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { getDataDir } from '../../lib/python-bridge.js';
|
|
6
|
+
import { green, cyan, bold, dim, } from '../../lib/tui.js';
|
|
7
|
+
function ask(question) {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export default class PortfolioCreate extends GatedCommand {
|
|
14
|
+
static description = 'Interactive wizard to build a portfolio configuration';
|
|
15
|
+
static examples = [
|
|
16
|
+
'$ rift portfolio create',
|
|
17
|
+
];
|
|
18
|
+
// Strategies discovered dynamically — users create their own. Default
|
|
19
|
+
// shows only OSS-shipped strategies; user's workbench customs are added
|
|
20
|
+
// at runtime by the picker.
|
|
21
|
+
availableStrategies = [
|
|
22
|
+
{ name: 'trend_follow', grade: 'C', desc: 'Bidirectional EMA-crossover trend follower (OSS demo strategy)' },
|
|
23
|
+
];
|
|
24
|
+
async run() {
|
|
25
|
+
this.log('');
|
|
26
|
+
this.log(` ${bold('Portfolio Builder')}`);
|
|
27
|
+
this.log(` ${dim('─'.repeat(50))}`);
|
|
28
|
+
this.log('');
|
|
29
|
+
const portfolioName = await ask(` ${cyan('Portfolio name')} ${dim('(e.g. nexstone_alpha)')}: `) || 'my_portfolio';
|
|
30
|
+
const strategies = [];
|
|
31
|
+
let totalAlloc = 0;
|
|
32
|
+
// Add strategies
|
|
33
|
+
let addMore = true;
|
|
34
|
+
while (addMore) {
|
|
35
|
+
this.log('');
|
|
36
|
+
this.log(` ${bold('Add a strategy')} ${dim(`(${(100 - totalAlloc * 100).toFixed(0)}% remaining)`)}`);
|
|
37
|
+
this.log('');
|
|
38
|
+
for (let i = 0; i < this.availableStrategies.length; i++) {
|
|
39
|
+
const s = this.availableStrategies[i];
|
|
40
|
+
const gc = s.grade === 'A' ? green : cyan;
|
|
41
|
+
this.log(` ${cyan(String(i + 1))} ${s.name.padEnd(22)} ${gc(s.grade)} ${dim(s.desc)}`);
|
|
42
|
+
}
|
|
43
|
+
this.log(` ${cyan(String(this.availableStrategies.length + 1))} ${dim('Enter custom name')}`);
|
|
44
|
+
this.log('');
|
|
45
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
46
|
+
const idx = parseInt(choice) - 1;
|
|
47
|
+
let stratName;
|
|
48
|
+
if (idx >= 0 && idx < this.availableStrategies.length) {
|
|
49
|
+
stratName = this.availableStrategies[idx].name;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
stratName = await ask(` ${cyan('Strategy name')}: `);
|
|
53
|
+
if (!stratName)
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const pair = (await ask(` ${cyan('Pair')} ${dim('(BTC)')}: `)) || 'BTC';
|
|
57
|
+
const remainPct = Math.round((1 - totalAlloc) * 100);
|
|
58
|
+
const allocStr = await ask(` ${cyan('Max allocation %')} ${dim(`(${Math.min(40, remainPct)}%)`)}: `);
|
|
59
|
+
const allocPct = parseInt(allocStr) || Math.min(40, remainPct);
|
|
60
|
+
const alloc = allocPct / 100;
|
|
61
|
+
const schedStr = await ask(` ${cyan('Schedule')} ${dim('(always / HH:MM-HH:MM UTC)')}: `) || 'always';
|
|
62
|
+
let schedule = 'always';
|
|
63
|
+
if (schedStr !== 'always' && schedStr.includes('-')) {
|
|
64
|
+
const [start, stop] = schedStr.split('-').map(s => s.trim());
|
|
65
|
+
schedule = { start: start + ' UTC', stop: stop + ' UTC' };
|
|
66
|
+
}
|
|
67
|
+
strategies.push({
|
|
68
|
+
name: stratName,
|
|
69
|
+
pair: pair.toUpperCase(),
|
|
70
|
+
max_allocation: alloc,
|
|
71
|
+
schedule,
|
|
72
|
+
enabled: true,
|
|
73
|
+
});
|
|
74
|
+
totalAlloc += alloc;
|
|
75
|
+
this.log(` ${green('✔')} Added ${bold(stratName)} on ${pair} (${allocPct}%${schedStr !== 'always' ? ', ' + schedStr : ''})`);
|
|
76
|
+
if (totalAlloc >= 1.0) {
|
|
77
|
+
this.log(` ${dim('100% allocated.')}`);
|
|
78
|
+
addMore = false;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const more = await ask(` ${cyan('Add another?')} ${dim('(yes/no)')}: `);
|
|
82
|
+
addMore = more.toLowerCase() === 'yes' || more.toLowerCase() === 'y';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (strategies.length === 0) {
|
|
86
|
+
this.log(`\n ${dim('No strategies added. Cancelled.')}\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Risk limits
|
|
90
|
+
this.log('');
|
|
91
|
+
this.log(` ${bold('Risk Limits')}`);
|
|
92
|
+
this.log('');
|
|
93
|
+
const maxDDStr = await ask(` ${cyan('Max portfolio drawdown %')} ${dim('(15%)')}: `);
|
|
94
|
+
const maxDD = (parseInt(maxDDStr) || 15) / 100;
|
|
95
|
+
const maxNetStr = await ask(` ${cyan('Max net exposure %')} ${dim('(100%)')}: `);
|
|
96
|
+
const maxNet = (parseInt(maxNetStr) || 100) / 100;
|
|
97
|
+
// Alerts
|
|
98
|
+
this.log('');
|
|
99
|
+
this.log(` ${bold('Alerts')}`);
|
|
100
|
+
this.log('');
|
|
101
|
+
const webhookUrl = await ask(` ${cyan('Slack webhook URL')} ${dim('(blank to skip)')}: `);
|
|
102
|
+
// Build YAML
|
|
103
|
+
const alertConfigs = [{ type: 'log', events: ['all'] }];
|
|
104
|
+
if (webhookUrl) {
|
|
105
|
+
alertConfigs.push({
|
|
106
|
+
type: 'webhook',
|
|
107
|
+
url: webhookUrl,
|
|
108
|
+
events: ['trade', 'stop_loss', 'health_drop', 'health_rotation', 'drawdown_warning', 'drawdown_kill', 'session_died'],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const config = {
|
|
112
|
+
name: portfolioName,
|
|
113
|
+
strategies: strategies.map(s => ({
|
|
114
|
+
name: s.name,
|
|
115
|
+
pair: s.pair,
|
|
116
|
+
enabled: s.enabled,
|
|
117
|
+
schedule: s.schedule,
|
|
118
|
+
max_allocation: s.max_allocation,
|
|
119
|
+
})),
|
|
120
|
+
risk: {
|
|
121
|
+
max_net_exposure: maxNet,
|
|
122
|
+
max_gross_exposure: 1.5,
|
|
123
|
+
max_per_asset: 0.8,
|
|
124
|
+
max_drawdown: maxDD,
|
|
125
|
+
},
|
|
126
|
+
rotation: {
|
|
127
|
+
enabled: true,
|
|
128
|
+
pause_grade: 'D',
|
|
129
|
+
stop_grade: 'F',
|
|
130
|
+
check_interval: 5,
|
|
131
|
+
},
|
|
132
|
+
alerts: alertConfigs,
|
|
133
|
+
};
|
|
134
|
+
// Write YAML
|
|
135
|
+
const yamlLines = [];
|
|
136
|
+
yamlLines.push(`# Portfolio: ${portfolioName}`);
|
|
137
|
+
yamlLines.push(`name: ${portfolioName}`);
|
|
138
|
+
yamlLines.push('');
|
|
139
|
+
yamlLines.push('strategies:');
|
|
140
|
+
for (const s of config.strategies) {
|
|
141
|
+
yamlLines.push(` - name: ${s.name}`);
|
|
142
|
+
yamlLines.push(` pair: ${s.pair}`);
|
|
143
|
+
yamlLines.push(` enabled: ${s.enabled}`);
|
|
144
|
+
if (typeof s.schedule === 'object') {
|
|
145
|
+
yamlLines.push(` schedule:`);
|
|
146
|
+
yamlLines.push(` start: "${s.schedule.start}"`);
|
|
147
|
+
yamlLines.push(` stop: "${s.schedule.stop}"`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
yamlLines.push(` schedule: ${s.schedule}`);
|
|
151
|
+
}
|
|
152
|
+
yamlLines.push(` max_allocation: ${s.max_allocation}`);
|
|
153
|
+
}
|
|
154
|
+
yamlLines.push('');
|
|
155
|
+
yamlLines.push('risk:');
|
|
156
|
+
yamlLines.push(` max_net_exposure: ${config.risk.max_net_exposure}`);
|
|
157
|
+
yamlLines.push(` max_gross_exposure: ${config.risk.max_gross_exposure}`);
|
|
158
|
+
yamlLines.push(` max_per_asset: ${config.risk.max_per_asset}`);
|
|
159
|
+
yamlLines.push(` max_drawdown: ${config.risk.max_drawdown}`);
|
|
160
|
+
yamlLines.push('');
|
|
161
|
+
yamlLines.push('rotation:');
|
|
162
|
+
yamlLines.push(` enabled: ${config.rotation.enabled}`);
|
|
163
|
+
yamlLines.push(` pause_grade: ${config.rotation.pause_grade}`);
|
|
164
|
+
yamlLines.push(` stop_grade: ${config.rotation.stop_grade}`);
|
|
165
|
+
yamlLines.push(` check_interval: ${config.rotation.check_interval}`);
|
|
166
|
+
yamlLines.push('');
|
|
167
|
+
yamlLines.push('alerts:');
|
|
168
|
+
for (const a of config.alerts) {
|
|
169
|
+
yamlLines.push(` - type: ${a.type}`);
|
|
170
|
+
if (a.url)
|
|
171
|
+
yamlLines.push(` url: ${a.url}`);
|
|
172
|
+
yamlLines.push(` events: [${a.events.join(', ')}]`);
|
|
173
|
+
}
|
|
174
|
+
const configDir = path.join(getDataDir(), 'algo');
|
|
175
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
176
|
+
const configPath = path.join(configDir, 'portfolio.yaml');
|
|
177
|
+
fs.writeFileSync(configPath, yamlLines.join('\n') + '\n');
|
|
178
|
+
this.log('');
|
|
179
|
+
this.log(` ${green('✔')} Portfolio config saved: ${dim(configPath)}`);
|
|
180
|
+
this.log('');
|
|
181
|
+
this.log(` ${bold('Summary')}`);
|
|
182
|
+
for (const s of strategies) {
|
|
183
|
+
const schedDesc = typeof s.schedule === 'object'
|
|
184
|
+
? `${s.schedule.start.replace(' UTC', '')}-${s.schedule.stop.replace(' UTC', '')} UTC`
|
|
185
|
+
: s.schedule;
|
|
186
|
+
this.log(` ${s.name.padEnd(20)} ${s.pair.padEnd(5)} ${(s.max_allocation * 100).toFixed(0)}% ${dim(schedDesc)}`);
|
|
187
|
+
}
|
|
188
|
+
this.log(` Max drawdown: ${(maxDD * 100).toFixed(0)}% Net limit: ${(maxNet * 100).toFixed(0)}%`);
|
|
189
|
+
if (webhookUrl)
|
|
190
|
+
this.log(` Alerts: Slack webhook configured`);
|
|
191
|
+
this.log('');
|
|
192
|
+
this.log(` ${dim(`Run ${cyan('rift portfolio start')} to begin trading.`)}`);
|
|
193
|
+
this.log('');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PortfolioStart extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
config: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { spawnDaemon, getDataDir } from '../../lib/python-bridge.js';
|
|
6
|
+
import { loadCredentials, hasFullSetup, getAccountAddress } from '../../lib/credentials.js';
|
|
7
|
+
import { red, cyan, dim, greenBg, } from '../../lib/tui.js';
|
|
8
|
+
export default class PortfolioStart extends GatedCommand {
|
|
9
|
+
static description = 'Start the portfolio supervisor to manage multiple live strategies';
|
|
10
|
+
static examples = [
|
|
11
|
+
'$ rift portfolio start',
|
|
12
|
+
'$ rift portfolio start --config ~/my-portfolio.yaml',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
config: Flags.string({ description: 'Path to portfolio.yaml', default: '' }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(PortfolioStart);
|
|
19
|
+
if (!hasFullSetup()) {
|
|
20
|
+
this.log(`\n ${red('✘')} Portfolio requires account setup. Run: ${cyan('rift auth setup')}\n`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const creds = loadCredentials();
|
|
24
|
+
if (!creds) {
|
|
25
|
+
this.log(`\n ${red('✘')} No credentials found.\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Check if supervisor already running
|
|
29
|
+
const pidFile = path.join(getDataDir(), 'algo', 'supervisor.pid');
|
|
30
|
+
if (fs.existsSync(pidFile)) {
|
|
31
|
+
try {
|
|
32
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
this.log(`\n ${greenBg(' ● PORTFOLIO ')} Supervisor already running ${dim(`(PID ${pid})`)}\n`);
|
|
35
|
+
this.log(` ${dim(`Run ${cyan('rift portfolio status')} to view or ${cyan('rift portfolio stop')} to end.`)}\n`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
fs.unlinkSync(pidFile);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check portfolio config exists
|
|
43
|
+
const configPath = flags.config || path.join(getDataDir(), 'algo', 'portfolio.yaml');
|
|
44
|
+
if (!fs.existsSync(configPath)) {
|
|
45
|
+
this.log('');
|
|
46
|
+
this.log(` ${red('✘')} No portfolio config found at: ${dim(configPath)}`);
|
|
47
|
+
this.log(` ${dim(`Run ${cyan('rift portfolio create')} to build one.`)}`);
|
|
48
|
+
this.log('');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Spawn supervisor daemon
|
|
52
|
+
const { pid } = spawnDaemon('portfolio-start', [
|
|
53
|
+
'--config', configPath,
|
|
54
|
+
'--account', getAccountAddress(creds),
|
|
55
|
+
], {
|
|
56
|
+
HYPERLIQUID_PRIVATE_KEY: creds.private_key,
|
|
57
|
+
});
|
|
58
|
+
this.log('');
|
|
59
|
+
this.log(` ${greenBg(' ● PORTFOLIO ')} Supervisor started ${dim(`(PID ${pid})`)}`);
|
|
60
|
+
this.log(` ${dim('Managing strategies in background.')}`);
|
|
61
|
+
this.log(` ${dim(`Run ${cyan('rift portfolio status')} to monitor.`)}`);
|
|
62
|
+
this.log('');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { getDataDir } from '../../lib/python-bridge.js';
|
|
5
|
+
import { green, red, yellow, cyan, bold, dim, greenBg, padEndVis, colorPnl, bar, gradeColor, boldBoxTop, boldBoxBottom, boldBoxDivider, boldBoxRow, } from '../../lib/tui.js';
|
|
6
|
+
export default class PortfolioStatus extends GatedCommand {
|
|
7
|
+
static description = 'Show portfolio supervisor status and all managed strategies';
|
|
8
|
+
static examples = [
|
|
9
|
+
'$ rift portfolio status',
|
|
10
|
+
];
|
|
11
|
+
async run() {
|
|
12
|
+
const stateFile = path.join(getDataDir(), 'algo', 'supervisor.json');
|
|
13
|
+
if (!fs.existsSync(stateFile)) {
|
|
14
|
+
this.log(`\n ${dim('No portfolio supervisor running.')}`);
|
|
15
|
+
this.log(` ${dim(`Run ${cyan('rift portfolio start')} to begin.`)}\n`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let state;
|
|
19
|
+
try {
|
|
20
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
this.log(`\n ${red('✘')} Could not read supervisor state.\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Verify supervisor is alive
|
|
27
|
+
const pidFile = path.join(getDataDir(), 'algo', 'supervisor.pid');
|
|
28
|
+
let alive = false;
|
|
29
|
+
if (fs.existsSync(pidFile)) {
|
|
30
|
+
try {
|
|
31
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
alive = true;
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
const portfolio = state.portfolio || {};
|
|
38
|
+
const strategies = state.strategies || [];
|
|
39
|
+
const iw = 60;
|
|
40
|
+
const row = boldBoxRow(iw);
|
|
41
|
+
this.log('');
|
|
42
|
+
this.log(boldBoxTop(iw));
|
|
43
|
+
const statusBadge = alive ? greenBg(' ● PORTFOLIO ') : dim(' ○ STOPPED ');
|
|
44
|
+
const name = state.name || 'portfolio';
|
|
45
|
+
const started = state.started_at || '';
|
|
46
|
+
this.log(row(` ${statusBadge} ${bold(name)} ${dim(started)}`));
|
|
47
|
+
this.log(boldBoxDivider(iw));
|
|
48
|
+
// Portfolio-level metrics
|
|
49
|
+
const equity = portfolio.total_equity || 0;
|
|
50
|
+
const dd = (portfolio.drawdown_from_peak || 0) * 100;
|
|
51
|
+
const netExp = (portfolio.net_exposure || 0) * 100;
|
|
52
|
+
const grossExp = (portfolio.gross_exposure || 0) * 100;
|
|
53
|
+
this.log(row(''));
|
|
54
|
+
this.log(row(` ${dim('EQUITY')} ${bold('$' + equity.toLocaleString())} ${dim('DD')} ${dd > 5 ? red(dd.toFixed(1) + '%') : dim(dd.toFixed(1) + '%')}`));
|
|
55
|
+
this.log(row(''));
|
|
56
|
+
// Risk gauges
|
|
57
|
+
this.log(row(` ${dim('RISK')}`));
|
|
58
|
+
this.log(row(` Net exposure ${bar(Math.abs(netExp), 100, 20)} ${padEndVis(netExp.toFixed(0) + '%', 6)} / 100%`));
|
|
59
|
+
this.log(row(` Gross exposure ${bar(grossExp, 150, 20)} ${padEndVis(grossExp.toFixed(0) + '%', 6)} / 150%`));
|
|
60
|
+
// Per-asset
|
|
61
|
+
const perAsset = portfolio.per_asset || {};
|
|
62
|
+
for (const [asset, exp] of Object.entries(perAsset)) {
|
|
63
|
+
const expPct = (exp * 100);
|
|
64
|
+
this.log(row(` ${asset.padEnd(16)} ${bar(Math.abs(expPct), 80, 20)} ${padEndVis(expPct.toFixed(0) + '%', 6)} / 80%`));
|
|
65
|
+
}
|
|
66
|
+
this.log(row(''));
|
|
67
|
+
this.log(boldBoxDivider(iw));
|
|
68
|
+
// Strategies
|
|
69
|
+
this.log(row(` ${dim('STRATEGIES')}`));
|
|
70
|
+
this.log(row(''));
|
|
71
|
+
for (const strat of strategies) {
|
|
72
|
+
const statusIcon = strat.status === 'running' ? green('●')
|
|
73
|
+
: strat.status === 'paused' ? yellow('●')
|
|
74
|
+
: strat.status === 'scheduled_off' ? dim('○')
|
|
75
|
+
: red('●');
|
|
76
|
+
const name = (strat.name || '').padEnd(18);
|
|
77
|
+
const pair = (strat.pair || '').padEnd(4);
|
|
78
|
+
const grade = strat.health_grade ? gradeColor(strat.health_grade) : dim('-');
|
|
79
|
+
const alloc = ((strat.allocation || 0) * 100).toFixed(0) + '%';
|
|
80
|
+
if (strat.status === 'running' && strat.position) {
|
|
81
|
+
const pos = strat.position;
|
|
82
|
+
const sideStr = pos.side === 'long' ? green('LONG') : red('SHORT');
|
|
83
|
+
this.log(row(` ${statusIcon} ${dim(name)} ${pair} ${sideStr} ${colorPnl(strat.pnl_pct || 0, '%')} ${grade} ${dim(alloc)}`));
|
|
84
|
+
}
|
|
85
|
+
else if (strat.status === 'running') {
|
|
86
|
+
this.log(row(` ${statusIcon} ${dim(name)} ${pair} ${dim('FLAT')} ${colorPnl(strat.pnl_pct || 0, '%')} ${grade} ${dim(alloc)}`));
|
|
87
|
+
}
|
|
88
|
+
else if (strat.status === 'scheduled_off') {
|
|
89
|
+
const sched = typeof strat.schedule === 'object'
|
|
90
|
+
? `${strat.schedule.start}-${strat.schedule.stop}`
|
|
91
|
+
: String(strat.schedule || '');
|
|
92
|
+
this.log(row(` ${statusIcon} ${dim(name)} ${pair} ${dim('OFF')} ${dim(sched)}`));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.log(row(` ${statusIcon} ${dim(name)} ${pair} ${dim(strat.status || 'stopped')}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.log(row(''));
|
|
99
|
+
// Recent alerts
|
|
100
|
+
const alertsFile = path.join(getDataDir(), 'algo', 'alerts.log');
|
|
101
|
+
if (fs.existsSync(alertsFile)) {
|
|
102
|
+
try {
|
|
103
|
+
const lines = fs.readFileSync(alertsFile, 'utf-8').trim().split('\n').filter(l => l.trim());
|
|
104
|
+
const recent = lines.slice(-5);
|
|
105
|
+
if (recent.length > 0) {
|
|
106
|
+
this.log(boldBoxDivider(iw));
|
|
107
|
+
this.log(row(` ${dim('RECENT ALERTS')}`));
|
|
108
|
+
this.log(row(''));
|
|
109
|
+
for (const line of recent) {
|
|
110
|
+
try {
|
|
111
|
+
const alert = JSON.parse(line);
|
|
112
|
+
const eventColor = alert.event === 'trade' ? cyan
|
|
113
|
+
: alert.event.includes('health') ? yellow
|
|
114
|
+
: alert.event.includes('drawdown') ? red
|
|
115
|
+
: dim;
|
|
116
|
+
this.log(row(` ${dim(alert.time)} ${eventColor(alert.event.padEnd(16))} ${dim(alert.message?.slice(0, 30) || '')}`));
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
this.log(row(''));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
}
|
|
125
|
+
this.log(boldBoxBottom(iw));
|
|
126
|
+
this.log('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
4
|
+
import { getDataDir } from '../../lib/python-bridge.js';
|
|
5
|
+
import { green, red, bold, dim, } from '../../lib/tui.js';
|
|
6
|
+
export default class PortfolioStop extends GatedCommand {
|
|
7
|
+
static description = 'Stop the portfolio supervisor and all managed strategies';
|
|
8
|
+
static examples = [
|
|
9
|
+
'$ rift portfolio stop',
|
|
10
|
+
];
|
|
11
|
+
async run() {
|
|
12
|
+
const pidFile = path.join(getDataDir(), 'algo', 'supervisor.pid');
|
|
13
|
+
if (!fs.existsSync(pidFile)) {
|
|
14
|
+
this.log(`\n ${dim('No portfolio supervisor running.')}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let pid;
|
|
18
|
+
try {
|
|
19
|
+
pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
this.log(`\n ${dim('Supervisor not running (stale PID).')}\n`);
|
|
24
|
+
try {
|
|
25
|
+
fs.unlinkSync(pidFile);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.log(`\n ${dim('Stopping portfolio supervisor')} ${dim(`(PID ${pid})...`)}`);
|
|
31
|
+
this.log(` ${dim('Closing all positions and saving session logs...')}`);
|
|
32
|
+
// Send SIGTERM
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 'SIGTERM');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
this.log(` ${dim('Process already exited.')}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Wait for shutdown (up to 45s — needs time to close all positions)
|
|
41
|
+
for (let i = 0; i < 90; i++) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
await new Promise(r => setTimeout(r, 500));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Show final state
|
|
51
|
+
const stateFile = path.join(getDataDir(), 'algo', 'supervisor.json');
|
|
52
|
+
if (fs.existsSync(stateFile)) {
|
|
53
|
+
try {
|
|
54
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
55
|
+
const portfolio = state.portfolio || {};
|
|
56
|
+
const strategies = state.strategies || [];
|
|
57
|
+
this.log('');
|
|
58
|
+
this.log(` ${green('✔')} Portfolio supervisor stopped.`);
|
|
59
|
+
this.log('');
|
|
60
|
+
this.log(` ${bold('Final State')}`);
|
|
61
|
+
this.log(` Equity: ${bold('$' + (portfolio.total_equity || 0).toLocaleString())}`);
|
|
62
|
+
this.log(` Strategies managed: ${strategies.length}`);
|
|
63
|
+
for (const s of strategies) {
|
|
64
|
+
const pnl = s.pnl_pct || 0;
|
|
65
|
+
const pnlStr = pnl >= 0 ? green(`+${pnl.toFixed(2)}%`) : red(`${pnl.toFixed(2)}%`);
|
|
66
|
+
this.log(` ${dim(s.name)} ${dim(s.pair)} — ${pnlStr} (${s.num_trades || 0} trades)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.log(`\n ${green('✔')} Portfolio supervisor stopped.\n`);
|
|
73
|
+
}
|
|
74
|
+
// Clean up
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(pidFile);
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
this.log('');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class PortfolioBacktest extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
config: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
'strategies-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
4
|
+
import { passthroughToEngine } from '../lib/engine-passthrough.js';
|
|
5
|
+
export default class PortfolioBacktest extends GatedCommand {
|
|
6
|
+
static description = 'Run a multi-strategy portfolio backtest from a portfolio.yaml';
|
|
7
|
+
static examples = [
|
|
8
|
+
'$ rift portfolio-backtest portfolio.yaml',
|
|
9
|
+
'$ rift portfolio-backtest config.yaml --strategies-dir ./strategies',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
config: Args.string({ description: 'Path to portfolio.yaml', required: true }),
|
|
13
|
+
};
|
|
14
|
+
static flags = {
|
|
15
|
+
'strategies-dir': Flags.string({ description: 'Directory with strategy .py files', default: '' }),
|
|
16
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(PortfolioBacktest);
|
|
20
|
+
// The engine subprocess is spawned with cwd=engine/, so relative paths
|
|
21
|
+
// in args would resolve from there instead of the user's terminal.
|
|
22
|
+
// Anchor to the user's cwd before handing the path to Python.
|
|
23
|
+
const configPath = path.resolve(args.config);
|
|
24
|
+
const engineArgs = [configPath];
|
|
25
|
+
if (flags['strategies-dir']) {
|
|
26
|
+
engineArgs.push('--strategies-dir', path.resolve(flags['strategies-dir']));
|
|
27
|
+
}
|
|
28
|
+
await passthroughToEngine({
|
|
29
|
+
command: 'portfolio-backtest',
|
|
30
|
+
args: engineArgs,
|
|
31
|
+
log: (m) => this.log(m),
|
|
32
|
+
error: (m) => this.error(m),
|
|
33
|
+
exit: (c) => this.exit(c),
|
|
34
|
+
jsonOnly: flags.json,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class PortfolioMatrix extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
pairs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
strategies: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
equity: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../lib/engine-passthrough.js';
|
|
4
|
+
export default class PortfolioMatrix extends GatedCommand {
|
|
5
|
+
static description = 'Strategy × pair P&L matrix, correlation matrix, and regime analysis';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift portfolio-matrix',
|
|
8
|
+
'$ rift portfolio-matrix --pairs BTC,ETH,SOL --strategies trend_follow,vol_breakout',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
pairs: Flags.string({ description: 'Comma-separated coins', default: 'BTC,ETH,SOL' }),
|
|
12
|
+
strategies: Flags.string({ description: 'Comma-separated strategies (auto-discovers if empty)', default: '' }),
|
|
13
|
+
equity: Flags.string({ description: 'Starting equity per strategy', default: '10000' }),
|
|
14
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { flags } = await this.parse(PortfolioMatrix);
|
|
18
|
+
const args = ['--pairs', flags.pairs, '--equity', flags.equity];
|
|
19
|
+
if (flags.strategies)
|
|
20
|
+
args.push('--strategies', flags.strategies);
|
|
21
|
+
await passthroughToEngine({
|
|
22
|
+
command: 'portfolio-matrix',
|
|
23
|
+
args,
|
|
24
|
+
log: (m) => this.log(m),
|
|
25
|
+
error: (m) => this.error(m),
|
|
26
|
+
exit: (c) => this.exit(c),
|
|
27
|
+
jsonOnly: flags.json,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class QuickTest extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
strategy: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
pair: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
equity: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
leverage: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
change: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../lib/engine-passthrough.js';
|
|
4
|
+
export default class QuickTest extends GatedCommand {
|
|
5
|
+
static description = 'Fast backtest with delta comparison to the last run — the core iteration loop while tuning a strategy.';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift quick-test trend_follow --pair BTC',
|
|
8
|
+
'$ rift quick-test trend_follow --pair ETH --tf 4h --change "tightened stop to 1.5%"',
|
|
9
|
+
];
|
|
10
|
+
static args = {
|
|
11
|
+
strategy: Args.string({ description: 'Strategy name', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
pair: Flags.string({ description: 'Trading pair', default: 'BTC' }),
|
|
15
|
+
tf: Flags.string({ description: 'Timeframe (auto from config if empty)', default: '' }),
|
|
16
|
+
equity: Flags.string({ description: 'Starting equity', default: '10000' }),
|
|
17
|
+
leverage: Flags.string({ description: 'Leverage multiplier', default: '1' }),
|
|
18
|
+
change: Flags.string({
|
|
19
|
+
description: 'Description of what changed (recorded in the delta history)',
|
|
20
|
+
default: '',
|
|
21
|
+
}),
|
|
22
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(QuickTest);
|
|
26
|
+
const engineArgs = [
|
|
27
|
+
args.strategy,
|
|
28
|
+
'--pair', flags.pair,
|
|
29
|
+
'--equity', flags.equity,
|
|
30
|
+
'--leverage', flags.leverage,
|
|
31
|
+
];
|
|
32
|
+
if (flags.tf)
|
|
33
|
+
engineArgs.push('--tf', flags.tf);
|
|
34
|
+
if (flags.change)
|
|
35
|
+
engineArgs.push('--change', flags.change);
|
|
36
|
+
await passthroughToEngine({
|
|
37
|
+
command: 'quick-test',
|
|
38
|
+
args: engineArgs,
|
|
39
|
+
log: (m) => this.log(m),
|
|
40
|
+
error: (m) => this.error(m),
|
|
41
|
+
exit: (c) => this.exit(c),
|
|
42
|
+
jsonOnly: flags.json,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|