@nexstone/rift-cli 0.1.1

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