@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,191 @@
1
+ import { Flags, Args } from '@oclif/core';
2
+ import { GatedCommand } from '../lib/base-command.js';
3
+ import { runEngine } from '../lib/python-bridge.js';
4
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
5
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
6
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
7
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
8
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
9
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
10
+ function colorNum(val, suffix = '') {
11
+ const str = `${val}${suffix}`;
12
+ if (val > 0)
13
+ return green(str);
14
+ if (val < 0)
15
+ return red(str);
16
+ return yellow(str);
17
+ }
18
+ function gradeBadge(ratio) {
19
+ if (ratio >= 0.7)
20
+ return green(bold('ROBUST'));
21
+ if (ratio >= 0.4)
22
+ return yellow(bold('MODERATE'));
23
+ if (ratio > 0)
24
+ return red(bold('WEAK'));
25
+ return red(bold('OVERFIT'));
26
+ }
27
+ export default class WalkForward extends GatedCommand {
28
+ static description = 'Run walk-forward analysis to test strategy robustness';
29
+ // Engine command and docs/guide use the hyphenated form. File basename
30
+ // `walkforward` (one word) stays for backwards compatibility.
31
+ static aliases = ['walk-forward'];
32
+ static examples = [
33
+ '$ rift walk-forward trend_follow --pair BTC --tf 1h --wf 3m/1m',
34
+ '$ rift walk-forward trend_follow --pair BTC --tf 4h --wf 6m/2m',
35
+ ];
36
+ static args = {
37
+ strategy: Args.string({ description: 'Strategy name', required: true }),
38
+ };
39
+ static flags = {
40
+ pair: Flags.string({ description: 'Trading pair', default: 'BTC-PERP' }),
41
+ tf: Flags.string({ description: 'Timeframe', default: '1h' }),
42
+ wf: Flags.string({
43
+ description: "Walk-forward config: train/test (e.g. 6m/3m). " +
44
+ "Default uses the strategy's recommended_train_months / " +
45
+ "recommended_test_months (or 3m/1m if unset).",
46
+ default: '',
47
+ }),
48
+ equity: Flags.integer({ description: 'Starting equity per window', default: 10000 }),
49
+ leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
50
+ };
51
+ async run() {
52
+ const { args, flags } = await this.parse(WalkForward);
53
+ this.log('');
54
+ this.log(` ${bold('Walk-Forward Analysis')}`);
55
+ const wfDisplay = flags.wf || "strategy default";
56
+ this.log(` ${dim(`${args.strategy} on ${flags.pair} ${flags.tf} — ${wfDisplay} windows`)}`);
57
+ this.log('');
58
+ const engineArgs = [
59
+ args.strategy,
60
+ '--pair', flags.pair,
61
+ '--tf', flags.tf,
62
+ '--equity', String(flags.equity),
63
+ '--leverage', String(flags.leverage),
64
+ ];
65
+ // Only forward --wf when the user actually passed one. Otherwise let
66
+ // the engine pick the strategy's recommended_train_months / _test_months.
67
+ if (flags.wf) {
68
+ engineArgs.push('--wf', flags.wf);
69
+ }
70
+ await runEngine('walk-forward', engineArgs, (msg) => {
71
+ if (msg.type === 'progress' && msg.msg) {
72
+ process.stdout.write(`\r ${dim(String(msg.msg))}${''.padEnd(20)}`);
73
+ }
74
+ else if (msg.type === 'result') {
75
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
76
+ this.renderResult(msg);
77
+ }
78
+ else if (msg.type === 'error') {
79
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
80
+ this.error(msg.msg);
81
+ }
82
+ });
83
+ }
84
+ renderResult(msg) {
85
+ const windows = msg.windows;
86
+ const is = msg.in_sample;
87
+ const oos = msg.out_of_sample;
88
+ const degradation = msg.degradation_ratio;
89
+ const pctProfitable = msg.pct_profitable_windows;
90
+ const numWindows = msg.num_windows;
91
+ // Per-window results table
92
+ this.log(dim(' ── Per-Window Results ──'));
93
+ this.log('');
94
+ const hdr = ` ${dim('│')} ${'#'.padEnd(3)} ${'Train Period'.padEnd(24)} ${'Test Period'.padEnd(24)} ${'IS Return'.padEnd(12)} ${'OOS Return'.padEnd(12)} ${'OOS Sharpe'.padEnd(12)} ${dim('│')}`;
95
+ const hr = ` ${dim('─'.repeat(hdr.replace(/\x1b\[[0-9;]*m/g, '').length - 2))}`;
96
+ this.log(hr);
97
+ this.log(hdr);
98
+ this.log(hr);
99
+ for (const w of windows) {
100
+ const isRet = colorNum(w.in_sample.return_pct, '%');
101
+ const oosRet = colorNum(w.out_of_sample.return_pct, '%');
102
+ const oosSharpe = colorNum(w.out_of_sample.sharpe);
103
+ const isRetClean = `${w.in_sample.return_pct}%`;
104
+ const oosRetClean = `${w.out_of_sample.return_pct}%`;
105
+ const oosSharpeClean = `${w.out_of_sample.sharpe}`;
106
+ const trainPeriod = `${w.train_period.start} → ${w.train_period.end}`;
107
+ const testPeriod = `${w.test_period.start} → ${w.test_period.end}`;
108
+ this.log(` ${dim('│')} ${String(w.window).padEnd(3)} ${trainPeriod.padEnd(24)} ${testPeriod.padEnd(24)} ${isRet}${' '.repeat(Math.max(1, 12 - isRetClean.length))} ${oosRet}${' '.repeat(Math.max(1, 12 - oosRetClean.length))} ${oosSharpe}${' '.repeat(Math.max(1, 12 - oosSharpeClean.length))} ${dim('│')}`);
109
+ }
110
+ this.log(hr);
111
+ this.log('');
112
+ // Summary box
113
+ const w = 55;
114
+ const boxHr = '─'.repeat(w - 2);
115
+ this.log(` ${dim('┌' + boxHr + '┐')}`);
116
+ this.log(` ${dim('│')} ${bold('WALK-FORWARD SUMMARY')}${' '.repeat(w - 23)}${dim('│')}`);
117
+ this.log(` ${dim('├' + boxHr + '┤')}`);
118
+ this.log(this.row('Strategy', String(msg.strategy), w));
119
+ this.log(this.row('Pair / Interval', `${msg.pair} ${msg.interval}`, w));
120
+ this.log(this.row('Config', String(msg.config), w));
121
+ this.log(this.row('Windows', String(numWindows), w));
122
+ this.log(` ${dim('├' + boxHr + '┤')}`);
123
+ this.log(` ${dim('│')} ${dim('IN-SAMPLE (train periods)')}${' '.repeat(w - 29)}${dim('│')}`);
124
+ this.log(` ${dim('├' + boxHr + '┤')}`);
125
+ this.log(this.rowColored('Avg Return', is.avg_return_pct, '%', w));
126
+ this.log(this.rowColored('Avg Sharpe', is.avg_sharpe, '', w));
127
+ this.log(this.row('Avg Win Rate', `${is.avg_win_rate}%`, w));
128
+ this.log(this.rowColored('Avg Max Drawdown', is.avg_max_drawdown_pct, '%', w));
129
+ this.log(this.row('Total Trades', String(is.total_trades), w));
130
+ this.log(` ${dim('├' + boxHr + '┤')}`);
131
+ this.log(` ${dim('│')} ${bold('OUT-OF-SAMPLE (test periods — unseen data)')}${' '.repeat(w - 46)}${dim('│')}`);
132
+ this.log(` ${dim('├' + boxHr + '┤')}`);
133
+ this.log(this.rowColored('Avg Return', oos.avg_return_pct, '%', w));
134
+ this.log(this.rowColored('Avg Sharpe', oos.avg_sharpe, '', w));
135
+ this.log(this.row('Avg Win Rate', `${oos.avg_win_rate}%`, w));
136
+ this.log(this.rowColored('Avg Max Drawdown', oos.avg_max_drawdown_pct, '%', w));
137
+ this.log(this.row('Total Trades', String(oos.total_trades), w));
138
+ this.log(this.rowColored('Combined OOS Return', oos.combined_return_pct, '%', w));
139
+ this.log(` ${dim('├' + boxHr + '┤')}`);
140
+ this.log(` ${dim('│')} ${bold('ROBUSTNESS')}${' '.repeat(w - 14)}${dim('│')}`);
141
+ this.log(` ${dim('├' + boxHr + '┤')}`);
142
+ // Degradation ratio with badge
143
+ const badge = gradeBadge(degradation);
144
+ const degStr = `${degradation} — ${badge}`;
145
+ // Can't easily calc ANSI-free length of badge, so just pad generously
146
+ this.log(` ${dim('│')} Degradation Ratio: ${degStr}${' '.repeat(Math.max(1, 10))}${dim('│')}`);
147
+ this.log(this.row('Profitable Windows', `${pctProfitable}%`, w, pctProfitable >= 60 ? 'green' : pctProfitable >= 40 ? 'yellow' : 'red'));
148
+ this.log(` ${dim('└' + boxHr + '┘')}`);
149
+ this.log('');
150
+ // Interpretation
151
+ this.log(dim(' ── Interpretation ──'));
152
+ this.log('');
153
+ if (degradation >= 0.7) {
154
+ this.log(` ${green('Strategy shows strong out-of-sample performance.')}`);
155
+ this.log(` ${dim('The strategy maintains most of its edge on unseen data — this is a good sign for live trading.')}`);
156
+ }
157
+ else if (degradation >= 0.4) {
158
+ this.log(` ${yellow('Strategy shows moderate degradation on unseen data.')}`);
159
+ this.log(` ${dim('Some of the backtest performance is likely from curve-fitting. Consider simplifying parameters.')}`);
160
+ }
161
+ else if (degradation > 0) {
162
+ this.log(` ${red('Strategy shows significant degradation on unseen data.')}`);
163
+ this.log(` ${dim('Most of the backtest edge disappears out-of-sample. High risk of failure in live trading.')}`);
164
+ }
165
+ else {
166
+ this.log(` ${red('Strategy is likely overfit.')}`);
167
+ this.log(` ${dim('Out-of-sample performance is negative — the strategy loses money on new data. Do not trade this.')}`);
168
+ }
169
+ this.log('');
170
+ }
171
+ row(label, value, width, color) {
172
+ const labelStr = ` ${label}:`;
173
+ const cleanVal = value.replace(/\x1b\[[0-9;]*m/g, '');
174
+ const padding = width - labelStr.length - cleanVal.length - 3;
175
+ let coloredVal = value;
176
+ if (color === 'green')
177
+ coloredVal = green(value);
178
+ else if (color === 'red')
179
+ coloredVal = red(value);
180
+ else if (color === 'yellow')
181
+ coloredVal = yellow(value);
182
+ return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredVal} ${dim('│')}`;
183
+ }
184
+ rowColored(label, value, suffix, width) {
185
+ const coloredStr = colorNum(value, suffix);
186
+ const cleanStr = `${value}${suffix}`;
187
+ const labelStr = ` ${label}:`;
188
+ const padding = width - labelStr.length - cleanStr.length - 3;
189
+ return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredStr} ${dim('│')}`;
190
+ }
191
+ }
@@ -0,0 +1,12 @@
1
+ import { GatedCommand } from '../lib/base-command.js';
2
+ export default class Withdraw extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ amount: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ destination: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,55 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { GatedCommand } from '../lib/base-command.js';
3
+ import { requestWithdrawal, postToHyperliquid, getExistingSession } from '../lib/walletconnect.js';
4
+ import { loadCredentials, getAccountAddress } from '../lib/credentials.js';
5
+ import { green, red, cyan, dim } from '../lib/tui.js';
6
+ export default class Withdraw extends GatedCommand {
7
+ static description = 'Withdraw USDC from Hyperliquid to Arbitrum';
8
+ static examples = [
9
+ '$ rift withdraw 100',
10
+ '$ rift withdraw 50 --destination 0x1234...',
11
+ ];
12
+ static args = {
13
+ amount: Args.string({ description: 'USDC amount to withdraw', required: true }),
14
+ };
15
+ static flags = {
16
+ destination: Flags.string({ description: 'Arbitrum address to receive USDC (defaults to main wallet)', default: '' }),
17
+ };
18
+ async run() {
19
+ const { args, flags } = await this.parse(Withdraw);
20
+ const amount = args.amount;
21
+ const isMainnet = true;
22
+ const creds = loadCredentials();
23
+ if (!creds) {
24
+ this.log(` ${red('✘')} No wallet configured. Run: ${cyan('rift auth setup')}`);
25
+ return;
26
+ }
27
+ const destination = flags.destination || getAccountAddress(creds);
28
+ // Check for existing WalletConnect session
29
+ const session = await getExistingSession();
30
+ if (!session) {
31
+ this.log(` ${red('✘')} No wallet session. Run: ${cyan('rift auth setup')} to reconnect.`);
32
+ return;
33
+ }
34
+ this.log('');
35
+ this.log(` Withdrawing $${amount} USDC to Arbitrum...`);
36
+ this.log(` ${dim('→ Approve in your wallet (check your phone)')}`);
37
+ this.log('');
38
+ const result = await requestWithdrawal(amount, destination, isMainnet);
39
+ if (!result.success) {
40
+ this.log(` ${red('✘')} Withdrawal failed: ${result.error}`);
41
+ return;
42
+ }
43
+ // Post to Hyperliquid
44
+ try {
45
+ const response = await postToHyperliquid(result.action, result.signature, result.nonce, isMainnet);
46
+ this.log(` ${green('✔')} Withdrawal submitted`);
47
+ this.log(` ${dim(`$${amount} USDC will arrive on Arbitrum in ~2 minutes`)}`);
48
+ this.log(` ${dim('$1 fee deducted by Hyperliquid')}`);
49
+ this.log('');
50
+ }
51
+ catch (error) {
52
+ this.log(` ${red('✘')} Failed to submit withdrawal: ${error.message}`);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,13 @@
1
+ import { GatedCommand } from '../lib/base-command.js';
2
+ export default class WorkbenchCreate extends GatedCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ template: 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,39 @@
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 WorkbenchCreate extends GatedCommand {
5
+ static description = 'Create a new custom strategy from a workbench template (config-as-data — no Python required).';
6
+ static examples = [
7
+ '$ rift workbench-create my_strategy',
8
+ '$ rift workbench-create rsi_revert --template single_signal_example',
9
+ ];
10
+ static args = {
11
+ // Engine validator requires letters/numbers/underscores only — reject
12
+ // hyphens up front so the user sees a clear error from oclif's parser
13
+ // rather than the engine's "Invalid strategy name" reply.
14
+ name: Args.string({
15
+ description: 'Name for the new strategy (letters, numbers, underscores)',
16
+ required: true,
17
+ }),
18
+ };
19
+ static flags = {
20
+ template: Flags.string({
21
+ description: 'Template to seed from',
22
+ options: ['blank', 'single_signal_example'],
23
+ default: 'blank',
24
+ }),
25
+ json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(WorkbenchCreate);
29
+ const engineArgs = [args.name, '--template', flags.template];
30
+ await passthroughToEngine({
31
+ command: 'workbench-create',
32
+ args: engineArgs,
33
+ log: (m) => this.log(m),
34
+ error: (m) => this.error(m),
35
+ exit: (c) => this.exit(c),
36
+ jsonOnly: flags.json,
37
+ });
38
+ }
39
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Hyperliquid account abstraction mode detection + collateral reading.
3
+ *
4
+ * TS mirror of packages/data/src/rift_data/account_mode.py. The two MUST
5
+ * agree on what counts as "tradeable collateral" per mode, or Python and
6
+ * TS surfaces will show different numbers to the same user.
7
+ *
8
+ * Modes:
9
+ * - 'standard' Spot and perp are separate. Only perp counts.
10
+ * - 'unified' UI default. Spot USDC IS perp collateral.
11
+ * - 'portfolio_margin' Pooled USDC + LTV-weighted assets. Per HL docs,
12
+ * perp dex user state is "not meaningful" here;
13
+ * real collateral lives in spot. v0.1 counts USDC
14
+ * only — non-USDC PM collateral (HYPE/BTC/USDH at
15
+ * oracle*LTV) under-counted; LTV table not exposed
16
+ * via info endpoints.
17
+ * - 'unknown' Future HL mode we don't recognize. Treated as
18
+ * unified (sum) for safety.
19
+ *
20
+ * Detection uses HL's /info endpoint with type='userAbstraction'. There's
21
+ * a confusingly-named 'userDexAbstraction' endpoint — that's a DIFFERENT
22
+ * mode (DEX Abstraction, being discontinued). Do not confuse.
23
+ */
24
+ export type AccountMode = 'standard' | 'unified' | 'portfolio_margin' | 'unknown';
25
+ export interface CollateralBreakdown {
26
+ mode: AccountMode;
27
+ perpAccountValue: number;
28
+ perpMarginUsed: number;
29
+ perpAvailable: number;
30
+ spotUsdc: number;
31
+ /** What sizing logic / gates should use. */
32
+ total: number;
33
+ /** True if spot USDC is NOT counted (Standard mode). */
34
+ perpOnly: boolean;
35
+ }
36
+ export declare const hlBaseUrl: (_isMainnet?: boolean) => string;
37
+ export declare function queryAccountMode(baseUrl: string, address: string): Promise<AccountMode>;
38
+ /**
39
+ * Read the wallet's available collateral, mode-aware. One source of truth
40
+ * for "how much can this wallet trade with" in TS surfaces.
41
+ *
42
+ * Three HL info calls in parallel: perp state, spot state, mode.
43
+ */
44
+ export declare function readCollateral(baseUrl: string, address: string): Promise<CollateralBreakdown>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Hyperliquid account abstraction mode detection + collateral reading.
3
+ *
4
+ * TS mirror of packages/data/src/rift_data/account_mode.py. The two MUST
5
+ * agree on what counts as "tradeable collateral" per mode, or Python and
6
+ * TS surfaces will show different numbers to the same user.
7
+ *
8
+ * Modes:
9
+ * - 'standard' Spot and perp are separate. Only perp counts.
10
+ * - 'unified' UI default. Spot USDC IS perp collateral.
11
+ * - 'portfolio_margin' Pooled USDC + LTV-weighted assets. Per HL docs,
12
+ * perp dex user state is "not meaningful" here;
13
+ * real collateral lives in spot. v0.1 counts USDC
14
+ * only — non-USDC PM collateral (HYPE/BTC/USDH at
15
+ * oracle*LTV) under-counted; LTV table not exposed
16
+ * via info endpoints.
17
+ * - 'unknown' Future HL mode we don't recognize. Treated as
18
+ * unified (sum) for safety.
19
+ *
20
+ * Detection uses HL's /info endpoint with type='userAbstraction'. There's
21
+ * a confusingly-named 'userDexAbstraction' endpoint — that's a DIFFERENT
22
+ * mode (DEX Abstraction, being discontinued). Do not confuse.
23
+ */
24
+ const HL_TO_FRIENDLY = {
25
+ disabled: 'standard',
26
+ unifiedAccount: 'unified',
27
+ portfolioMargin: 'portfolio_margin',
28
+ };
29
+ const _hlPost = async (baseUrl, body) => {
30
+ const resp = await fetch(`${baseUrl}/info`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify(body),
34
+ });
35
+ if (!resp.ok)
36
+ throw new Error(`HL info ${resp.status}`);
37
+ return resp.json();
38
+ };
39
+ export const hlBaseUrl = (_isMainnet = true) => 'https://api.hyperliquid.xyz';
40
+ export async function queryAccountMode(baseUrl, address) {
41
+ const raw = await _hlPost(baseUrl, { type: 'userAbstraction', user: address.toLowerCase() });
42
+ if (typeof raw !== 'string')
43
+ return 'unknown';
44
+ return HL_TO_FRIENDLY[raw] ?? 'unknown';
45
+ }
46
+ /**
47
+ * Read the wallet's available collateral, mode-aware. One source of truth
48
+ * for "how much can this wallet trade with" in TS surfaces.
49
+ *
50
+ * Three HL info calls in parallel: perp state, spot state, mode.
51
+ */
52
+ export async function readCollateral(baseUrl, address) {
53
+ const addr = address.toLowerCase();
54
+ const [perp, spot, modeRaw] = await Promise.all([
55
+ _hlPost(baseUrl, { type: 'clearinghouseState', user: addr }),
56
+ _hlPost(baseUrl, { type: 'spotClearinghouseState', user: addr }),
57
+ _hlPost(baseUrl, { type: 'userAbstraction', user: addr }),
58
+ ]);
59
+ const mode = typeof modeRaw === 'string' ? (HL_TO_FRIENDLY[modeRaw] ?? 'unknown') : 'unknown';
60
+ const summary = perp?.marginSummary ?? {};
61
+ const perpAccountValue = parseFloat(summary.accountValue ?? '0');
62
+ const perpMarginUsed = parseFloat(summary.totalMarginUsed ?? '0');
63
+ const perpAvailable = perpAccountValue - perpMarginUsed;
64
+ let spotUsdc = 0;
65
+ for (const b of spot?.balances ?? []) {
66
+ if (b?.coin === 'USDC') {
67
+ spotUsdc = parseFloat(b.total ?? '0');
68
+ break;
69
+ }
70
+ }
71
+ let total;
72
+ switch (mode) {
73
+ case 'standard':
74
+ total = perpAvailable;
75
+ break;
76
+ case 'unified':
77
+ case 'portfolio_margin':
78
+ // HL docs: under both modes, perp state is "not meaningful";
79
+ // real collateral is in spot. v0.1 counts USDC only — see file
80
+ // header for the PM non-USDC caveat.
81
+ total = perpAvailable + spotUsdc;
82
+ break;
83
+ default:
84
+ // unknown: sum, matching Python's safest-default behavior
85
+ total = perpAvailable + spotUsdc;
86
+ }
87
+ return {
88
+ mode,
89
+ perpAccountValue,
90
+ perpMarginUsed,
91
+ perpAvailable,
92
+ spotUsdc,
93
+ total,
94
+ perpOnly: mode === 'standard',
95
+ };
96
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * AI-powered backtest analysis using Claude API.
3
+ */
4
+ export declare function analyzeBacktest(resultData: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * AI-powered backtest analysis using Claude API.
3
+ */
4
+ import Anthropic from '@anthropic-ai/sdk';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ const CONFIG_PATH = path.join(process.env.HOME || '~', '.rift', 'config.json');
8
+ function loadAIConfig() {
9
+ // Check env var first
10
+ const envKey = process.env.RIFT_AI_API_KEY || process.env.ANTHROPIC_API_KEY;
11
+ if (envKey) {
12
+ return { apiKey: envKey, model: 'claude-sonnet-4-20250514' };
13
+ }
14
+ // Check config file
15
+ if (fs.existsSync(CONFIG_PATH)) {
16
+ try {
17
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
18
+ const ai = config.ai;
19
+ if (ai?.api_key) {
20
+ return { apiKey: ai.api_key, model: ai.model || 'claude-sonnet-4-20250514' };
21
+ }
22
+ }
23
+ catch { }
24
+ }
25
+ return null;
26
+ }
27
+ const SYSTEM_PROMPT = `You are a senior quantitative analyst reviewing backtest results for a trading strategy on Hyperliquid perpetual futures. Your job is to give a concise, actionable assessment.
28
+
29
+ Be direct and specific. No fluff. Focus on:
30
+ 1. Is this strategy viable for real trading? Why or why not?
31
+ 2. What are the critical risk issues?
32
+ 3. What specific parameter changes would improve it?
33
+ 4. What's the one thing the trader should fix first?
34
+
35
+ Keep your response under 200 words. Use plain language. If the strategy would blow up a real account, say so clearly.`;
36
+ export async function analyzeBacktest(resultData) {
37
+ const config = loadAIConfig();
38
+ if (!config) {
39
+ return 'No AI API key configured. Run: rift config set ai.api_key <your-anthropic-key>\nOr set RIFT_AI_API_KEY or ANTHROPIC_API_KEY environment variable.';
40
+ }
41
+ const client = new Anthropic({ apiKey: config.apiKey });
42
+ // Build a clean summary for the LLM
43
+ const { chart, export: _, type, command, ...metrics } = resultData;
44
+ const trades = resultData.export?.trades || [];
45
+ const prompt = `Analyze this backtest result:
46
+
47
+ ${JSON.stringify(metrics, null, 2)}
48
+
49
+ Number of trades in detail: ${trades.length}
50
+ ${trades.length > 0 ? `First trade: ${JSON.stringify(trades[0])}
51
+ Last trade: ${JSON.stringify(trades[trades.length - 1])}` : 'No trades executed.'}
52
+
53
+ ${trades.length === 0 ? 'The strategy made zero trades. This likely means the entry conditions were never met for this pair/timeframe combination.' : ''}`;
54
+ const response = await client.messages.create({
55
+ model: config.model,
56
+ max_tokens: 500,
57
+ system: SYSTEM_PROMPT,
58
+ messages: [{ role: 'user', content: prompt }],
59
+ });
60
+ const textBlock = response.content.find(b => b.type === 'text');
61
+ return textBlock ? textBlock.text : 'No analysis returned.';
62
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Base command for RIFT — every CLI command extends `GatedCommand`.
3
+ *
4
+ * The historical name "GatedCommand" is kept to avoid churning 17 import
5
+ * sites; it doesn't actually gate anything in v0.1. Acts as the central
6
+ * point for cross-cutting CLI behavior:
7
+ *
8
+ * Persistent status footer (Phase 0 polish item #5)
9
+ * - Renders a single-line footer at the end of every command's output
10
+ * showing system state (live ready / research-only / kill switch / etc.)
11
+ * - TTY-only — suppressed for piped output so `rift list-data | jq`
12
+ * still produces clean JSON
13
+ * - Opt out per command by setting `static skipFooter = true`
14
+ * (e.g., `home` which renders its own bottom-of-screen status)
15
+ *
16
+ * Future hook points (Phase 1+):
17
+ * - Fee-gating prompts (where the name originally came from)
18
+ * - Telemetry emit
19
+ * - Auth state checks before live commands
20
+ */
21
+ import { Command } from '@oclif/core';
22
+ export declare abstract class GatedCommand extends Command {
23
+ /**
24
+ * Subclass override: skip the auto-rendered status footer.
25
+ * For commands that render their own bottom-of-screen status (home)
26
+ * or emit raw structured data where a trailing footer would be wrong.
27
+ */
28
+ static skipFooter: boolean;
29
+ init(): Promise<void>;
30
+ /**
31
+ * oclif lifecycle: called after run() succeeds AND after catch() on error.
32
+ * Footer renders regardless of outcome — TTY check protects piped output.
33
+ */
34
+ protected finally(err: Error | undefined): Promise<unknown>;
35
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Base command for RIFT — every CLI command extends `GatedCommand`.
3
+ *
4
+ * The historical name "GatedCommand" is kept to avoid churning 17 import
5
+ * sites; it doesn't actually gate anything in v0.1. Acts as the central
6
+ * point for cross-cutting CLI behavior:
7
+ *
8
+ * Persistent status footer (Phase 0 polish item #5)
9
+ * - Renders a single-line footer at the end of every command's output
10
+ * showing system state (live ready / research-only / kill switch / etc.)
11
+ * - TTY-only — suppressed for piped output so `rift list-data | jq`
12
+ * still produces clean JSON
13
+ * - Opt out per command by setting `static skipFooter = true`
14
+ * (e.g., `home` which renders its own bottom-of-screen status)
15
+ *
16
+ * Future hook points (Phase 1+):
17
+ * - Fee-gating prompts (where the name originally came from)
18
+ * - Telemetry emit
19
+ * - Auth state checks before live commands
20
+ */
21
+ import { Command } from '@oclif/core';
22
+ import { printStatusFooterIfTTYWithSpacing } from './status-footer.js';
23
+ export class GatedCommand extends Command {
24
+ /**
25
+ * Subclass override: skip the auto-rendered status footer.
26
+ * For commands that render their own bottom-of-screen status (home)
27
+ * or emit raw structured data where a trailing footer would be wrong.
28
+ */
29
+ static skipFooter = false;
30
+ async init() {
31
+ await super.init();
32
+ }
33
+ /**
34
+ * oclif lifecycle: called after run() succeeds AND after catch() on error.
35
+ * Footer renders regardless of outcome — TTY check protects piped output.
36
+ */
37
+ async finally(err) {
38
+ const ctor = this.constructor;
39
+ if (!ctor.skipFooter) {
40
+ try {
41
+ printStatusFooterIfTTYWithSpacing();
42
+ }
43
+ catch {
44
+ // Never let footer rendering break command execution
45
+ }
46
+ }
47
+ return super.finally(err);
48
+ }
49
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Wallet credential management — canonical snake_case schema, matching
3
+ * what the Python engine writes via `rift agent-pair` (rift_trade.api_wallet
4
+ * → rift_core.keys.APIWalletKey, serialized via Pydantic).
5
+ *
6
+ * File location: ~/.rift/credentials (no extension, matches Python)
7
+ * Format: single-account JSON (snake_case fields).
8
+ *
9
+ * The loader is tolerant of two legacy formats:
10
+ * (a) ~/.rift/credentials.json with camelCase fields (old TS auth.ts writer)
11
+ * (b) Multi-account wrapped: {"default": {…}}
12
+ * Both are migrated to the canonical form on first read.
13
+ *
14
+ * The Python engine is the authoritative writer. The TS `rift auth setup`
15
+ * flow also writes to the same path + schema.
16
+ */
17
+ export interface Credentials {
18
+ address: string;
19
+ private_key: string;
20
+ network: 'mainnet';
21
+ name?: string;
22
+ registered_at?: string;
23
+ registered_tx?: string | null;
24
+ account_address?: string;
25
+ type?: 'api-wallet' | 'generated' | 'walletconnect';
26
+ agent_approved?: boolean;
27
+ builder_fee_approved?: boolean;
28
+ }
29
+ export declare function loadCredentials(): Credentials | null;
30
+ export declare function saveCredentials(creds: Credentials): void;
31
+ export declare function hasCredentials(): boolean;
32
+ /**
33
+ * Returns true if the wallet is ready for live trading (mainnet-only).
34
+ *
35
+ * - File must exist with `address` + `private_key` (pairing complete).
36
+ * - `agent_approved` defaults to true when the file exists — the file is
37
+ * only written after a successful approveAgent transaction.
38
+ * - `builder_fee_approved` must be explicit `true`. Absent → user is
39
+ * prompted to run `rift approve-builder-fee` before their first trade.
40
+ */
41
+ export declare function hasFullSetup(): boolean;
42
+ /** Main wallet address — falls back to API wallet address when not stored
43
+ * (older Python-paired wallets don't include account_address). */
44
+ export declare function getAccountAddress(creds: Credentials): string;
45
+ export declare function maskKey(key: string): string;
46
+ export declare function maskAddress(addr: string): string;