@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,227 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { runEngine } from '../lib/python-bridge.js';
|
|
4
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
5
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
6
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
7
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
8
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
9
|
+
const CATALOG = {
|
|
10
|
+
'Research & backtesting': [
|
|
11
|
+
{ name: 'backtest', desc: 'Run a backtest on cached candle data', promoted: true },
|
|
12
|
+
{ name: 'compare', desc: 'Compare multiple strategies head-to-head', promoted: true },
|
|
13
|
+
{ name: 'sweep', desc: 'Run a parameter sweep', promoted: true },
|
|
14
|
+
{ name: 'smart-sweep', desc: 'Bayesian-optimized parameter sweep', promoted: false },
|
|
15
|
+
{ name: 'montecarlo', desc: 'Monte Carlo simulation', promoted: true },
|
|
16
|
+
{ name: 'walk-forward', desc: 'Walk-forward validation', promoted: true },
|
|
17
|
+
{ name: 'research', desc: 'Full research pipeline (backtest + WF + MC + gates)', promoted: true },
|
|
18
|
+
{ name: 'quick-test', desc: 'Quick sanity backtest', promoted: true },
|
|
19
|
+
{ name: 'portfolio-backtest', desc: 'Multi-strategy portfolio backtest', promoted: true },
|
|
20
|
+
{ name: 'portfolio-matrix', desc: 'Strategy-pair correlation matrix', promoted: true },
|
|
21
|
+
{ name: 'cross-asset', desc: 'Cross-pair / cross-asset correlation research', promoted: true },
|
|
22
|
+
{ name: 'feature-importance', desc: 'Permutation feature importance for a strategy', promoted: false },
|
|
23
|
+
{ name: 'indicator-stats', desc: 'Indicator IC / decay statistics', promoted: false },
|
|
24
|
+
{ name: 'signal-stats', desc: 'Signal hit rate, IC, decay', promoted: false },
|
|
25
|
+
{ name: 'signal-decay', desc: 'Alpha-decay curve for a signal', promoted: false },
|
|
26
|
+
{ name: 'signal-backfill', desc: 'Backfill signal scores over history', promoted: false },
|
|
27
|
+
{ name: 'history', desc: 'Show backtest history', promoted: false },
|
|
28
|
+
],
|
|
29
|
+
'Validation & promotion': [
|
|
30
|
+
{ name: 'validate-strategy', desc: 'Lint a strategy file (SDK validator)', promoted: false },
|
|
31
|
+
{ name: 'verify', desc: 'Verify strategy vs buy-and-hold over a date range', promoted: true },
|
|
32
|
+
{ name: 'tearsheet', desc: 'Generate a strategy tearsheet (md)', promoted: false },
|
|
33
|
+
{ name: 'audit', desc: 'Export compliance-grade audit trail (csv/json)', promoted: true },
|
|
34
|
+
{ name: 'experiments', desc: 'List recent experiment runs', promoted: false },
|
|
35
|
+
{ name: 'experiment-revert', desc: 'Revert a strategy to an earlier experiment snapshot', promoted: false },
|
|
36
|
+
{ name: 'save-optimized', desc: 'Save a parameter-optimized strategy config', promoted: false },
|
|
37
|
+
{ name: 'versions', desc: 'List strategy version history', promoted: false },
|
|
38
|
+
],
|
|
39
|
+
'Workbench (strategy authoring)': [
|
|
40
|
+
{ name: 'workbench-create', desc: 'Create a new workbench strategy', promoted: true },
|
|
41
|
+
{ name: 'workbench-list', desc: 'List workbench strategies', promoted: false },
|
|
42
|
+
{ name: 'workbench-show', desc: 'Show a workbench strategy', promoted: false },
|
|
43
|
+
{ name: 'workbench-update', desc: 'Update workbench config', promoted: false },
|
|
44
|
+
{ name: 'workbench-delete', desc: 'Delete a workbench strategy', promoted: false },
|
|
45
|
+
{ name: 'workbench-generate', desc: 'Generate a strategy from a workbench config', promoted: false },
|
|
46
|
+
{ name: 'workbench-templates', desc: 'List workbench templates', promoted: false },
|
|
47
|
+
{ name: 'workbench-components', desc: 'List workbench signal components', promoted: false },
|
|
48
|
+
],
|
|
49
|
+
'Data & discovery': [
|
|
50
|
+
{ name: 'sync', desc: 'Sync HL S3 archive into local cache', promoted: true },
|
|
51
|
+
{ name: 'fetch', desc: 'Fetch candles from HL REST API', promoted: false },
|
|
52
|
+
{ name: 'fetch-multi', desc: 'Fetch multiple pairs', promoted: false },
|
|
53
|
+
{ name: 'list-pairs', desc: 'List all HL trading pairs', promoted: false },
|
|
54
|
+
{ name: 'list-data', desc: 'Inventory of cached data', promoted: false },
|
|
55
|
+
{ name: 'data-inventory', desc: 'Detailed cache inventory + freshness', promoted: true },
|
|
56
|
+
{ name: 'diff', desc: 'Diff two backtest runs', promoted: false },
|
|
57
|
+
{ name: 'collect', desc: 'Start persistent data collector daemon', promoted: false },
|
|
58
|
+
{ name: 'collect-status', desc: 'Collector daemon status', promoted: false },
|
|
59
|
+
{ name: 'indicators', desc: 'Compute an indicator on cached data', promoted: false },
|
|
60
|
+
{ name: 'funding-browser', desc: 'Browse funding-rate history across pairs', promoted: true },
|
|
61
|
+
{ name: 'order-flow', desc: 'Inspect order-flow microstructure', promoted: false },
|
|
62
|
+
{ name: 'regime', desc: 'Detect market regime (HMM / changepoints)', promoted: false },
|
|
63
|
+
],
|
|
64
|
+
'Trading & execution': [
|
|
65
|
+
{ name: 'algo', desc: 'Run an automated strategy', promoted: true },
|
|
66
|
+
{ name: 'algo-status', desc: 'Algo session status', promoted: false },
|
|
67
|
+
{ name: 'algo-stop', desc: 'Stop a running algo', promoted: false },
|
|
68
|
+
{ name: 'recon', desc: 'Reconnaissance / dry-run trading', promoted: false },
|
|
69
|
+
{ name: 'manual-trade', desc: 'Place a manual trade (engine-side)', promoted: false },
|
|
70
|
+
{ name: 'test-trade', desc: 'Minimum-size test trade for connectivity', promoted: true },
|
|
71
|
+
{ name: 'buy', desc: 'Direct buy order', promoted: false },
|
|
72
|
+
{ name: 'sell', desc: 'Direct sell order', promoted: false },
|
|
73
|
+
{ name: 'scan', desc: 'Scan multiple pairs for entry signals', promoted: false },
|
|
74
|
+
{ name: 'scout', desc: 'Confluence-ranked opportunity scout', promoted: true },
|
|
75
|
+
{ name: 'close-position', desc: 'Close a single position', promoted: false },
|
|
76
|
+
{ name: 'close-all', desc: 'Close every open position', promoted: false },
|
|
77
|
+
{ name: 'tighten-stop', desc: 'Move a stop loss closer to price', promoted: false },
|
|
78
|
+
{ name: 'reduce-position', desc: 'Reduce position size', promoted: false },
|
|
79
|
+
],
|
|
80
|
+
'Portfolio': [
|
|
81
|
+
{ name: 'portfolio-start', desc: 'Launch portfolio supervisor + strategies', promoted: false },
|
|
82
|
+
{ name: 'portfolio-status', desc: 'Portfolio runtime status', promoted: false },
|
|
83
|
+
{ name: 'portfolio-stop', desc: 'Stop portfolio + all strategies', promoted: false },
|
|
84
|
+
{ name: 'tca', desc: 'Transaction cost analysis on session log', promoted: false },
|
|
85
|
+
{ name: 'attribution', desc: 'PnL attribution (alpha vs costs vs funding)', promoted: false },
|
|
86
|
+
{ name: 'report', desc: 'Generate portfolio performance report', promoted: false },
|
|
87
|
+
{ name: 'var', desc: 'Portfolio Value-at-Risk', promoted: false },
|
|
88
|
+
{ name: 'pairs-backtest', desc: 'Pairs-trading portfolio backtest', promoted: true },
|
|
89
|
+
],
|
|
90
|
+
'Account & wallet': [
|
|
91
|
+
{ name: 'balance', desc: 'Show HL account balance', promoted: false },
|
|
92
|
+
{ name: 'holdings', desc: 'Show open positions', promoted: false },
|
|
93
|
+
{ name: 'state', desc: 'Full account state snapshot', promoted: false },
|
|
94
|
+
{ name: 'transfer', desc: 'Transfer USDC between HL spot and perp', promoted: true },
|
|
95
|
+
{ name: 'agent-pair', desc: 'Pair an API wallet to your main wallet', promoted: false },
|
|
96
|
+
{ name: 'agent-rotate', desc: 'Rotate the API wallet', promoted: false },
|
|
97
|
+
{ name: 'agent-status', desc: 'Show paired API wallet status', promoted: false },
|
|
98
|
+
{ name: 'account-mode-status', desc: 'Show HL account mode (isolated/cross/etc.)', promoted: false },
|
|
99
|
+
{ name: 'account-mode-set', desc: 'Set HL account mode', promoted: false },
|
|
100
|
+
{ name: 'token-issue', desc: 'Issue an authorization token', promoted: false },
|
|
101
|
+
{ name: 'token-list', desc: 'List authorization tokens', promoted: false },
|
|
102
|
+
{ name: 'token-revoke', desc: 'Revoke an authorization token', promoted: false },
|
|
103
|
+
{ name: 'token-show', desc: 'Show a specific token', promoted: false },
|
|
104
|
+
],
|
|
105
|
+
'Learning & lessons': [
|
|
106
|
+
{ name: 'lessons', desc: 'Show captured trading lessons', promoted: true },
|
|
107
|
+
{ name: 'add-lesson', desc: 'Add a manual lesson entry', promoted: false },
|
|
108
|
+
{ name: 'guide', desc: 'Interactive learning guide', promoted: true },
|
|
109
|
+
],
|
|
110
|
+
'System & admin': [
|
|
111
|
+
{ name: 'doctor', desc: 'System health check', promoted: true },
|
|
112
|
+
{ name: 'health', desc: 'Lightweight health snapshot', promoted: false },
|
|
113
|
+
{ name: 'version', desc: 'Show RIFT version', promoted: false },
|
|
114
|
+
{ name: 'strategies', desc: 'List registered strategies', promoted: false },
|
|
115
|
+
{ name: 'check-api', desc: 'Verify HL API reachability', promoted: false },
|
|
116
|
+
{ name: 'set-proxy', desc: 'Configure HTTP proxy', promoted: false },
|
|
117
|
+
{ name: 'clear-proxy', desc: 'Clear HTTP proxy setting', promoted: false },
|
|
118
|
+
{ name: 'auth', desc: 'Wallet auth setup', promoted: true },
|
|
119
|
+
{ name: 'approve-builder-fee', desc: 'On-chain builder-fee approval', promoted: false },
|
|
120
|
+
{ name: 'check-builder-fee', desc: 'Verify builder-fee approval state', promoted: false },
|
|
121
|
+
{ name: 'api-start', desc: 'Start the HTTP API server', promoted: false },
|
|
122
|
+
{ name: 'api-stop', desc: 'Stop the HTTP API server', promoted: false },
|
|
123
|
+
{ name: 'watchdog', desc: 'Start the kill-switch watchdog', promoted: false },
|
|
124
|
+
{ name: 'watchdog-stop', desc: 'Stop the watchdog', promoted: false },
|
|
125
|
+
{ name: 'watchdog-events', desc: 'Show recent watchdog events', promoted: false },
|
|
126
|
+
{ name: 'cost', desc: 'Pre-trade cost estimate', promoted: true },
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
export default class More extends GatedCommand {
|
|
130
|
+
static description = 'Discover and run every engine command — including those without a top-level `rift <cmd>` wrapper';
|
|
131
|
+
static examples = [
|
|
132
|
+
'$ rift more # list all engine commands by category',
|
|
133
|
+
'$ rift more funding-browser BTC # run the funding-browser command',
|
|
134
|
+
'$ rift more verify <bundle-id> # verify a sealed bundle',
|
|
135
|
+
];
|
|
136
|
+
// Pure passthrough — `rift more` reads raw argv directly so unknown flags
|
|
137
|
+
// intended for the engine command (e.g. `--local-main-key`) aren't
|
|
138
|
+
// intercepted by oclif's flag parser. The first arg after `more` is the
|
|
139
|
+
// engine command name; everything else is forwarded verbatim.
|
|
140
|
+
static args = {
|
|
141
|
+
command: Args.string({ description: 'Engine command name (omit to list)' }),
|
|
142
|
+
};
|
|
143
|
+
static strict = false;
|
|
144
|
+
async run() {
|
|
145
|
+
// Bypass oclif's flag parser. process.argv is:
|
|
146
|
+
// [node, run.js, "more", <engineCmd>, <...passthroughArgs>]
|
|
147
|
+
const rawArgs = process.argv.slice(3);
|
|
148
|
+
// No command → render the catalog.
|
|
149
|
+
if (rawArgs.length === 0) {
|
|
150
|
+
this.renderCatalog();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const [command, ...passthrough] = rawArgs;
|
|
154
|
+
// Track whether the engine surfaced its own error message. If it did,
|
|
155
|
+
// we suppress the generic "Engine exited with code N" footer on
|
|
156
|
+
// non-zero exit — the user already saw the helpful message.
|
|
157
|
+
let surfacedError = false;
|
|
158
|
+
try {
|
|
159
|
+
await runEngine(command, passthrough, (msg) => {
|
|
160
|
+
// Mirror the engine's NDJSON to the user. For programmatic use,
|
|
161
|
+
// they can pipe `rift more <cmd> --json …`; here we just surface
|
|
162
|
+
// what the engine emits in human-readable form.
|
|
163
|
+
const type = msg.type;
|
|
164
|
+
if (type === 'progress' && msg.msg) {
|
|
165
|
+
this.log(dim(` ${msg.msg}`));
|
|
166
|
+
}
|
|
167
|
+
else if (type === 'status' && msg.msg) {
|
|
168
|
+
this.log(` ${msg.msg}`);
|
|
169
|
+
}
|
|
170
|
+
else if (type === 'error' && msg.msg) {
|
|
171
|
+
// Log + flag, but do NOT call this.error() here — it throws
|
|
172
|
+
// a CLIError synchronously inside an async readline callback,
|
|
173
|
+
// which oclif's lifecycle does not catch reliably. We let the
|
|
174
|
+
// engine's non-zero exit propagate via runEngine's rejection
|
|
175
|
+
// path, and exit(1) silently from the catch below.
|
|
176
|
+
this.log(` ${red('Error:')} ${msg.msg}`);
|
|
177
|
+
surfacedError = true;
|
|
178
|
+
}
|
|
179
|
+
else if (type === 'result') {
|
|
180
|
+
// The engine emits a structured result — print as pretty JSON
|
|
181
|
+
// so the user can pipe it or read it.
|
|
182
|
+
const { type: _t, ...rest } = msg;
|
|
183
|
+
this.log(JSON.stringify(rest, null, 2));
|
|
184
|
+
}
|
|
185
|
+
else if (msg.msg) {
|
|
186
|
+
this.log(` ${msg.msg}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (surfacedError) {
|
|
192
|
+
// The engine already printed a human-readable error above.
|
|
193
|
+
// Exit non-zero silently — no second confusing footer.
|
|
194
|
+
this.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Genuine engine crash with no structured error — re-raise via
|
|
197
|
+
// this.error so the user sees the stderr-extracted message.
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
renderCatalog() {
|
|
202
|
+
this.log('');
|
|
203
|
+
this.log(` ${bold('RIFT — Full engine command catalog')}`);
|
|
204
|
+
this.log(` ${dim('─'.repeat(64))}`);
|
|
205
|
+
this.log('');
|
|
206
|
+
this.log(` Commands marked ${green('●')} have a first-class ${cyan('rift <name>')} wrapper.`);
|
|
207
|
+
this.log(` All others are runnable via ${cyan('rift more <name> [args...]')}.`);
|
|
208
|
+
this.log('');
|
|
209
|
+
let total = 0;
|
|
210
|
+
let promoted = 0;
|
|
211
|
+
for (const [section, entries] of Object.entries(CATALOG)) {
|
|
212
|
+
this.log(` ${bold(section)}`);
|
|
213
|
+
const maxName = Math.max(...entries.map(e => e.name.length));
|
|
214
|
+
for (const e of entries) {
|
|
215
|
+
total++;
|
|
216
|
+
if (e.promoted)
|
|
217
|
+
promoted++;
|
|
218
|
+
const marker = e.promoted ? green('●') : ' ';
|
|
219
|
+
const name = e.name.padEnd(maxName + 2);
|
|
220
|
+
this.log(` ${marker} ${cyan(name)} ${dim(e.desc)}`);
|
|
221
|
+
}
|
|
222
|
+
this.log('');
|
|
223
|
+
}
|
|
224
|
+
this.log(` ${dim(`${total} engine commands · ${promoted} promoted as rift <cmd>`)}`);
|
|
225
|
+
this.log('');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class New 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
|
+
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private scaffoldStrategy;
|
|
13
|
+
private scaffoldSignal;
|
|
14
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { runEngine } from '../lib/python-bridge.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
10
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
11
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
12
|
+
/**
|
|
13
|
+
* Walk up from `start` to find the repo root. We identify the root by the
|
|
14
|
+
* presence of `pnpm-workspace.yaml` — this file only exists at the top of
|
|
15
|
+
* the monorepo. Walking up looking for `engine/pyproject.toml` (the prior
|
|
16
|
+
* heuristic) was buggy because `packages/engine/pyproject.toml` also exists
|
|
17
|
+
* and would match first, dropping scaffolded files into the wrong dir.
|
|
18
|
+
*/
|
|
19
|
+
function findRepoRoot(start) {
|
|
20
|
+
let dir = path.resolve(start);
|
|
21
|
+
for (let i = 0; i < 12; i++) {
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')))
|
|
23
|
+
return dir;
|
|
24
|
+
const parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir)
|
|
26
|
+
break;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
// Fallback: cwd. Better than landing in the wrong subdirectory silently.
|
|
30
|
+
return path.resolve('.');
|
|
31
|
+
}
|
|
32
|
+
export default class New extends GatedCommand {
|
|
33
|
+
static description = 'Scaffold a new trading strategy or scout signal';
|
|
34
|
+
static examples = [
|
|
35
|
+
'$ rift new my-strategy',
|
|
36
|
+
'$ rift new bollinger-breakout',
|
|
37
|
+
'$ rift new my-momentum-signal --type signal',
|
|
38
|
+
];
|
|
39
|
+
static args = {
|
|
40
|
+
name: Args.string({ description: 'Name (lowercase, hyphens ok)', required: true }),
|
|
41
|
+
};
|
|
42
|
+
static flags = {
|
|
43
|
+
type: Flags.string({
|
|
44
|
+
description: 'What to scaffold: "strategy" (default) or "signal"',
|
|
45
|
+
options: ['strategy', 'signal'],
|
|
46
|
+
default: 'strategy',
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
async run() {
|
|
50
|
+
const { args, flags } = await this.parse(New);
|
|
51
|
+
if (flags.type === 'signal') {
|
|
52
|
+
return this.scaffoldSignal(args.name);
|
|
53
|
+
}
|
|
54
|
+
return this.scaffoldStrategy(args.name);
|
|
55
|
+
}
|
|
56
|
+
// ── Strategy scaffold (existing behavior) ──────────────────────
|
|
57
|
+
async scaffoldStrategy(rawName) {
|
|
58
|
+
const name = rawName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
59
|
+
const className = name.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
60
|
+
const configName = `${className}Config`;
|
|
61
|
+
// Find repo root via `pnpm-workspace.yaml` (only present at the root —
|
|
62
|
+
// walking up from packages/cli/dist/commands looking for engine/pyproject.toml
|
|
63
|
+
// would otherwise match packages/engine/pyproject.toml first).
|
|
64
|
+
const strategiesDir = path.join(findRepoRoot(__dirname), 'strategies');
|
|
65
|
+
fs.mkdirSync(strategiesDir, { recursive: true });
|
|
66
|
+
const stratDir = path.join(strategiesDir, name);
|
|
67
|
+
if (fs.existsSync(stratDir)) {
|
|
68
|
+
this.error(`Strategy "${name}" already exists at ${stratDir}`);
|
|
69
|
+
}
|
|
70
|
+
fs.mkdirSync(stratDir, { recursive: true });
|
|
71
|
+
// strategy.py
|
|
72
|
+
const strategyPy = `"""${className} strategy."""
|
|
73
|
+
|
|
74
|
+
from dataclasses import dataclass
|
|
75
|
+
|
|
76
|
+
from rift.strategy import EMA, RSI, Candle, Indicator, Signal, Strategy, StrategyState, register
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ${configName}:
|
|
81
|
+
ema_period: int = 20
|
|
82
|
+
rsi_period: int = 14
|
|
83
|
+
entry_threshold: float = 30.0
|
|
84
|
+
exit_threshold: float = 70.0
|
|
85
|
+
leverage: float = 1.0
|
|
86
|
+
stop_loss_pct: float = 0.02
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@register("${name}")
|
|
90
|
+
class ${className}(Strategy):
|
|
91
|
+
"""${className} — describe your strategy here."""
|
|
92
|
+
|
|
93
|
+
config_class = ${configName}
|
|
94
|
+
|
|
95
|
+
def on_candle(self, candle: Candle, state: StrategyState) -> Signal | None:
|
|
96
|
+
ema = state.ema
|
|
97
|
+
rsi = state.rsi
|
|
98
|
+
|
|
99
|
+
if ema == 0 or rsi == 0:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Entry logic — customize this
|
|
103
|
+
if state.position == 0 and rsi < self.config.entry_threshold and candle.close > ema:
|
|
104
|
+
return Signal.long(size=self.position_size(), sl=self.config.stop_loss_pct)
|
|
105
|
+
|
|
106
|
+
# Exit logic — customize this
|
|
107
|
+
if state.position > 0 and rsi > self.config.exit_threshold:
|
|
108
|
+
return Signal.close()
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def indicators(self) -> dict[str, Indicator]:
|
|
113
|
+
return {
|
|
114
|
+
"ema": EMA(self.config.ema_period),
|
|
115
|
+
"rsi": RSI(self.config.rsi_period),
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
// config.yaml
|
|
119
|
+
const configYaml = `# ${className} default configuration
|
|
120
|
+
strategy: ${name}
|
|
121
|
+
|
|
122
|
+
params:
|
|
123
|
+
ema_period: 20
|
|
124
|
+
rsi_period: 14
|
|
125
|
+
entry_threshold: 30.0
|
|
126
|
+
exit_threshold: 70.0
|
|
127
|
+
leverage: 1.0
|
|
128
|
+
stop_loss_pct: 0.02
|
|
129
|
+
`;
|
|
130
|
+
// sweep.yaml
|
|
131
|
+
const sweepYaml = `# Parameter sweep configuration for ${className}
|
|
132
|
+
strategy: ${name}
|
|
133
|
+
|
|
134
|
+
sweep:
|
|
135
|
+
ema_period: [10, 15, 20, 30, 50]
|
|
136
|
+
rsi_period: [7, 14, 21]
|
|
137
|
+
entry_threshold: [20.0, 25.0, 30.0, 35.0]
|
|
138
|
+
exit_threshold: [60.0, 65.0, 70.0, 75.0]
|
|
139
|
+
stop_loss_pct: [0.01, 0.02, 0.03]
|
|
140
|
+
`;
|
|
141
|
+
// README.md
|
|
142
|
+
const readme = `# ${className}
|
|
143
|
+
|
|
144
|
+
## Description
|
|
145
|
+
Describe your strategy logic here.
|
|
146
|
+
|
|
147
|
+
## Parameters
|
|
148
|
+
| Parameter | Default | Description |
|
|
149
|
+
|-----------|---------|-------------|
|
|
150
|
+
| ema_period | 20 | EMA lookback period |
|
|
151
|
+
| rsi_period | 14 | RSI lookback period |
|
|
152
|
+
| entry_threshold | 30.0 | RSI level to enter |
|
|
153
|
+
| exit_threshold | 70.0 | RSI level to exit |
|
|
154
|
+
| leverage | 1.0 | Position leverage |
|
|
155
|
+
| stop_loss_pct | 0.02 | Stop loss percentage |
|
|
156
|
+
|
|
157
|
+
## Usage
|
|
158
|
+
\`\`\`bash
|
|
159
|
+
rift backtest ${name} --pair BTC --tf 1h
|
|
160
|
+
rift compare ${name},trend_follow --pair BTC --tf 4h
|
|
161
|
+
\`\`\`
|
|
162
|
+
`;
|
|
163
|
+
fs.writeFileSync(path.join(stratDir, 'strategy.py'), strategyPy);
|
|
164
|
+
fs.writeFileSync(path.join(stratDir, 'config.yaml'), configYaml);
|
|
165
|
+
fs.writeFileSync(path.join(stratDir, 'sweep.yaml'), sweepYaml);
|
|
166
|
+
fs.writeFileSync(path.join(stratDir, 'README.md'), readme);
|
|
167
|
+
this.log('');
|
|
168
|
+
this.log(` ${green('✔')} Strategy ${bold(name)} created at:`);
|
|
169
|
+
this.log('');
|
|
170
|
+
this.log(` ${stratDir}/`);
|
|
171
|
+
this.log(` ├── strategy.py ${dim('— your strategy code')}`);
|
|
172
|
+
this.log(` ├── config.yaml ${dim('— default parameters')}`);
|
|
173
|
+
this.log(` ├── sweep.yaml ${dim('— parameter ranges for optimization')}`);
|
|
174
|
+
this.log(` └── README.md ${dim('— documentation')}`);
|
|
175
|
+
this.log('');
|
|
176
|
+
// Validate the scaffolded strategy
|
|
177
|
+
const strategyFile = path.join(stratDir, 'strategy.py');
|
|
178
|
+
this.log(` ${dim('Validating...')}`);
|
|
179
|
+
try {
|
|
180
|
+
await runEngine('validate-strategy', [strategyFile], (msg) => {
|
|
181
|
+
if (msg.type === 'result') {
|
|
182
|
+
if (msg.status === 'ok') {
|
|
183
|
+
this.log(` ${green('✔')} Strategy ${bold(String(msg.name))} registered with indicators: ${dim(String(msg.indicators.join(', ')))}`);
|
|
184
|
+
}
|
|
185
|
+
else if (msg.status === 'warn') {
|
|
186
|
+
this.log(` ${yellow('!')} Warning: ${msg.error || msg.errors.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.log(` ${red('✘')} ${msg.error}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
this.log(` ${yellow('!')} Could not validate (Python engine issue). Strategy files are created.`);
|
|
196
|
+
}
|
|
197
|
+
this.log('');
|
|
198
|
+
this.log(` ${dim('Next steps:')}`);
|
|
199
|
+
this.log(` 1. Edit ${bold('strategy.py')} with your trading logic`);
|
|
200
|
+
this.log(` 2. Run: ${cyan('rift backtest ' + name + ' --pair BTC --tf 1h')}`);
|
|
201
|
+
this.log(` 3. Optimize: ${cyan('rift sweep ' + name + ' --config sweep.yaml')}`);
|
|
202
|
+
this.log('');
|
|
203
|
+
function cyan(s) { return `\x1b[36m${s}\x1b[0m`; }
|
|
204
|
+
function yellow(s) { return `\x1b[33m${s}\x1b[0m`; }
|
|
205
|
+
function red(s) { return `\x1b[31m${s}\x1b[0m`; }
|
|
206
|
+
}
|
|
207
|
+
// ── Signal scaffold (Custom-signal SDK) ───────────────────────
|
|
208
|
+
async scaffoldSignal(rawName) {
|
|
209
|
+
const name = rawName.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
210
|
+
const signalsDir = path.join(findRepoRoot(__dirname), 'strategies', 'signals');
|
|
211
|
+
fs.mkdirSync(signalsDir, { recursive: true });
|
|
212
|
+
const signalFile = path.join(signalsDir, `${name}.py`);
|
|
213
|
+
if (fs.existsSync(signalFile)) {
|
|
214
|
+
this.error(`Signal "${name}" already exists at ${signalFile}`);
|
|
215
|
+
}
|
|
216
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
217
|
+
// Signal scaffolds are intentionally single-file (no config.yaml, no
|
|
218
|
+
// sweep.yaml). The signal IS the unit; users register one per file
|
|
219
|
+
// and the file's @signal(...) decorator is the entire surface.
|
|
220
|
+
const signalPy = `"""${name} — custom scout signal.
|
|
221
|
+
|
|
222
|
+
Register with @signal(...). \`rift scout\` discovers this file automatically
|
|
223
|
+
from strategies/signals/ (and ~/.rift/signals/) at scan time.
|
|
224
|
+
|
|
225
|
+
See docs/signals/AUTHORING.md for the full guide.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
from __future__ import annotations
|
|
229
|
+
|
|
230
|
+
from rift_strategies_sdk import signal, SignalResult
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@signal(
|
|
234
|
+
name="${name}",
|
|
235
|
+
category="momentum", # one of: funding, momentum, microstructure,
|
|
236
|
+
# volatility, cross_pair, seasonality
|
|
237
|
+
description="${name} — describe what this signal detects",
|
|
238
|
+
weight=1.0, # higher = more influence in aggregation
|
|
239
|
+
)
|
|
240
|
+
def ${name}(coin: str, state: dict) -> SignalResult:
|
|
241
|
+
"""Return a SignalResult with score in [-1, +1].
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
coin: ticker symbol, e.g. "BTC"
|
|
245
|
+
state: dict of available market state. Common keys:
|
|
246
|
+
- mid_price, funding_rate, predicted_funding, open_interest
|
|
247
|
+
- oracle_price, premium, atr_pct, day_volume
|
|
248
|
+
- cvd, volume_delta, relative_volume
|
|
249
|
+
- candles_1h, candles_5m (lists of {o,h,l,c,v,t})
|
|
250
|
+
- indicators (dict of pre-computed indicator values)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
SignalResult with score=+1 (strong long), 0 (no opinion),
|
|
254
|
+
or -1 (strong short). Score=0 is silently dropped by the
|
|
255
|
+
aggregator, so always return a meaningful score when fired.
|
|
256
|
+
"""
|
|
257
|
+
# ──────────────────────────────────────────────────────────────
|
|
258
|
+
# Replace this with your detection logic.
|
|
259
|
+
#
|
|
260
|
+
# Example: extreme funding rate as a contrarian signal.
|
|
261
|
+
# ──────────────────────────────────────────────────────────────
|
|
262
|
+
funding = float(state.get("funding_rate") or 0.0)
|
|
263
|
+
|
|
264
|
+
# Extreme positive funding → over-leveraged longs → fade
|
|
265
|
+
if funding > 0.0005:
|
|
266
|
+
return SignalResult(
|
|
267
|
+
name="${name}",
|
|
268
|
+
score=-0.5,
|
|
269
|
+
reason=f"Extreme positive funding {funding * 100:.3f}% — fade longs",
|
|
270
|
+
category="momentum",
|
|
271
|
+
confidence=0.6,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Extreme negative funding → over-leveraged shorts → fade
|
|
275
|
+
if funding < -0.0005:
|
|
276
|
+
return SignalResult(
|
|
277
|
+
name="${name}",
|
|
278
|
+
score=+0.5,
|
|
279
|
+
reason=f"Extreme negative funding {funding * 100:.3f}% — fade shorts",
|
|
280
|
+
category="momentum",
|
|
281
|
+
confidence=0.6,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return SignalResult(
|
|
285
|
+
name="${name}",
|
|
286
|
+
score=0.0,
|
|
287
|
+
reason="Funding within normal range",
|
|
288
|
+
category="momentum",
|
|
289
|
+
confidence=0.0,
|
|
290
|
+
)
|
|
291
|
+
`;
|
|
292
|
+
fs.writeFileSync(signalFile, signalPy);
|
|
293
|
+
this.log('');
|
|
294
|
+
this.log(` \x1b[32m✔\x1b[0m Signal ${bold(name)} created at:`);
|
|
295
|
+
this.log('');
|
|
296
|
+
this.log(` ${signalFile}`);
|
|
297
|
+
this.log('');
|
|
298
|
+
this.log(` ${dim('Next steps:')}`);
|
|
299
|
+
this.log(` 1. Edit ${bold(name + '.py')} with your detection logic`);
|
|
300
|
+
this.log(` 2. Run: ${cyan('rift scout --top 5 --min 1 --no-soak')} (your signal fires alongside the 9 built-ins)`);
|
|
301
|
+
this.log(` 3. Use ${cyan('rift signal-stats')} to see how often it fires`);
|
|
302
|
+
this.log('');
|
|
303
|
+
this.log(` ${dim('Place user-only signals at ~/.rift/signals/<name>.py (not committed to the repo).')}`);
|
|
304
|
+
this.log('');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class Pairs extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static aliases: string[];
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
a: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
b: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
equity: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
lookback: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'entry-z': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'exit-z': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'stop-z': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'max-hold': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
private renderResult;
|
|
20
|
+
private row;
|
|
21
|
+
private rowColored;
|
|
22
|
+
}
|