@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,147 @@
|
|
|
1
|
+
import { Flags } 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
|
+
export default class Pairs extends GatedCommand {
|
|
19
|
+
static description = 'Backtest a pairs/spread trade between two assets (e.g. BTC/ETH)';
|
|
20
|
+
// `pairs-backtest` is the engine command name and was the previous
|
|
21
|
+
// wrapper name; alias kept so scripts and docs that referenced it
|
|
22
|
+
// continue to work.
|
|
23
|
+
static aliases = ['pairs-backtest'];
|
|
24
|
+
static examples = [
|
|
25
|
+
'$ rift pairs --a BTC --b ETH --tf 1h',
|
|
26
|
+
'$ rift pairs --a BTC --b ETH --entry-z 2.5 --lookback 336',
|
|
27
|
+
'$ rift pairs --a BTC --b ETH --json # raw JSON for pipelines',
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
a: Flags.string({ description: 'First asset', default: 'BTC' }),
|
|
31
|
+
b: Flags.string({ description: 'Second asset', default: 'ETH' }),
|
|
32
|
+
tf: Flags.string({ description: 'Timeframe', default: '1h' }),
|
|
33
|
+
equity: Flags.integer({ description: 'Starting equity', default: 10000 }),
|
|
34
|
+
lookback: Flags.integer({ description: 'Rolling z-score window (candles)', default: 168 }),
|
|
35
|
+
'entry-z': Flags.string({ description: 'Z-score entry threshold', default: '2.0' }),
|
|
36
|
+
'exit-z': Flags.string({ description: 'Z-score exit threshold', default: '0.5' }),
|
|
37
|
+
'stop-z': Flags.string({ description: 'Z-score stop loss', default: '4.0' }),
|
|
38
|
+
'max-hold': Flags.integer({ description: 'Max hold time (candles)', default: 72 }),
|
|
39
|
+
json: Flags.boolean({ description: 'Emit raw JSON result instead of the rendered panel', default: false }),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { flags } = await this.parse(Pairs);
|
|
43
|
+
if (!flags.json) {
|
|
44
|
+
this.log('');
|
|
45
|
+
this.log(` ${bold('Pairs Trading Backtest')}`);
|
|
46
|
+
this.log(` ${dim(`${flags.a}/${flags.b} spread on ${flags.tf} — z-score entry: ${flags['entry-z']}, exit: ${flags['exit-z']}`)}`);
|
|
47
|
+
this.log('');
|
|
48
|
+
}
|
|
49
|
+
const engineArgs = [
|
|
50
|
+
'--a', flags.a,
|
|
51
|
+
'--b', flags.b,
|
|
52
|
+
'--tf', flags.tf,
|
|
53
|
+
'--equity', String(flags.equity),
|
|
54
|
+
'--lookback', String(flags.lookback),
|
|
55
|
+
'--entry-z', flags['entry-z'],
|
|
56
|
+
'--exit-z', flags['exit-z'],
|
|
57
|
+
'--stop-z', flags['stop-z'],
|
|
58
|
+
'--max-hold', String(flags['max-hold']),
|
|
59
|
+
];
|
|
60
|
+
await runEngine('pairs-backtest', engineArgs, (msg) => {
|
|
61
|
+
if (flags.json) {
|
|
62
|
+
// JSON mode: emit the result payload only, silently consume
|
|
63
|
+
// progress/status so the output is pipe-safe.
|
|
64
|
+
if (msg.type === 'result') {
|
|
65
|
+
const { type: _t, ...rest } = msg;
|
|
66
|
+
this.log(JSON.stringify(rest, null, 2));
|
|
67
|
+
}
|
|
68
|
+
else if (msg.type === 'error') {
|
|
69
|
+
this.error(msg.msg);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (msg.type === 'progress' && msg.msg) {
|
|
74
|
+
process.stdout.write(`\r ${dim(String(msg.msg))}${''.padEnd(20)}`);
|
|
75
|
+
}
|
|
76
|
+
else if (msg.type === 'result') {
|
|
77
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
78
|
+
this.renderResult(msg);
|
|
79
|
+
}
|
|
80
|
+
else if (msg.type === 'error') {
|
|
81
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
82
|
+
this.error(msg.msg);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
renderResult(msg) {
|
|
87
|
+
const w = 55;
|
|
88
|
+
const hr = '─'.repeat(w - 2);
|
|
89
|
+
this.log(` ${dim('┌' + hr + '┐')}`);
|
|
90
|
+
this.log(` ${dim('│')} ${bold('PAIRS TRADING RESULTS')}${' '.repeat(w - 24)}${dim('│')}`);
|
|
91
|
+
this.log(` ${dim('├' + hr + '┤')}`);
|
|
92
|
+
this.log(this.row('Spread', `${msg.asset_a}/${msg.asset_b}`, w));
|
|
93
|
+
this.log(this.row('Interval', String(msg.interval), w));
|
|
94
|
+
this.log(this.row('Avg Hold', `${msg.avg_hold_candles}h`, w));
|
|
95
|
+
this.log(` ${dim('├' + hr + '┤')}`);
|
|
96
|
+
this.log(this.row('Initial', `$${Number(msg.initial_equity).toLocaleString()}`, w));
|
|
97
|
+
this.log(this.row('Final', `$${Number(msg.final_equity).toLocaleString()}`, w));
|
|
98
|
+
this.log(this.rowColored('Return', msg.total_return_pct, '%', w));
|
|
99
|
+
this.log(` ${dim('├' + hr + '┤')}`);
|
|
100
|
+
this.log(this.row('Trades', String(msg.num_trades), w));
|
|
101
|
+
this.log(this.row('Win Rate', `${msg.win_rate}%`, w, msg.win_rate >= 50 ? 'green' : 'red'));
|
|
102
|
+
this.log(this.rowColored('Avg Win', msg.avg_win_pct, '%', w));
|
|
103
|
+
this.log(this.rowColored('Avg Loss', msg.avg_loss_pct, '%', w));
|
|
104
|
+
this.log(` ${dim('├' + hr + '┤')}`);
|
|
105
|
+
this.log(this.rowColored('Max Drawdown', msg.max_drawdown_pct, '%', w));
|
|
106
|
+
this.log(this.row('Sharpe Ratio', String(msg.sharpe_ratio), w, msg.sharpe_ratio > 0.5 ? 'green' : msg.sharpe_ratio > 0 ? 'yellow' : 'red'));
|
|
107
|
+
this.log(this.row('Profit Factor', String(msg.profit_factor), w, msg.profit_factor > 1.5 ? 'green' : msg.profit_factor > 1 ? 'yellow' : 'red'));
|
|
108
|
+
this.log(this.rowColored('Funding P&L', msg.total_funding, '', w));
|
|
109
|
+
this.log(` ${dim('└' + hr + '┘')}`);
|
|
110
|
+
// Interpretation
|
|
111
|
+
this.log('');
|
|
112
|
+
const sharpe = msg.sharpe_ratio;
|
|
113
|
+
const pf = msg.profit_factor;
|
|
114
|
+
const dd = msg.max_drawdown_pct;
|
|
115
|
+
if (sharpe > 0.5 && pf > 1.5 && dd > -10) {
|
|
116
|
+
this.log(` ${green('Strong pairs trade.')} Positive Sharpe with controlled drawdown.`);
|
|
117
|
+
this.log(` ${dim('The spread shows mean-reverting behavior — the edge is structural.')}`);
|
|
118
|
+
}
|
|
119
|
+
else if (sharpe > 0 && pf > 1) {
|
|
120
|
+
this.log(` ${yellow('Moderate pairs trade.')} Positive but consider optimizing parameters.`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.log(` ${red('Weak or negative.')} The spread may not be mean-reverting in this period.`);
|
|
124
|
+
}
|
|
125
|
+
this.log('');
|
|
126
|
+
}
|
|
127
|
+
row(label, value, width, color) {
|
|
128
|
+
const labelStr = ` ${label}:`;
|
|
129
|
+
const cleanVal = value.replace(/\x1b\[[0-9;]*m/g, '');
|
|
130
|
+
const padding = width - labelStr.length - cleanVal.length - 3;
|
|
131
|
+
let coloredVal = value;
|
|
132
|
+
if (color === 'green')
|
|
133
|
+
coloredVal = green(value);
|
|
134
|
+
else if (color === 'red')
|
|
135
|
+
coloredVal = red(value);
|
|
136
|
+
else if (color === 'yellow')
|
|
137
|
+
coloredVal = yellow(value);
|
|
138
|
+
return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredVal} ${dim('│')}`;
|
|
139
|
+
}
|
|
140
|
+
rowColored(label, value, suffix, width) {
|
|
141
|
+
const coloredStr = colorNum(value, suffix);
|
|
142
|
+
const cleanStr = `${value}${suffix}`;
|
|
143
|
+
const labelStr = ` ${label}:`;
|
|
144
|
+
const padding = width - labelStr.length - cleanStr.length - 3;
|
|
145
|
+
return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredStr} ${dim('│')}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PerpClose extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
coin: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../../lib/engine-passthrough.js';
|
|
4
|
+
import { loadCredentials, hasFullSetup, getAccountAddress } from '../../lib/credentials.js';
|
|
5
|
+
export default class PerpClose extends GatedCommand {
|
|
6
|
+
static description = 'Close an open perp position (and cancel any orders for the coin) via reduce-only IOC market order.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'$ rift perp close BTC # close BTC perp position + cancel BTC orders',
|
|
9
|
+
'$ rift perp close # close ALL perp positions + cancel ALL orders',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
coin: Args.string({
|
|
13
|
+
description: 'Coin to close (omit to close all open positions)',
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(PerpClose);
|
|
22
|
+
if (!hasFullSetup()) {
|
|
23
|
+
this.error('Perp close requires wallet setup. Run: rift auth setup');
|
|
24
|
+
}
|
|
25
|
+
const creds = loadCredentials();
|
|
26
|
+
if (!creds) {
|
|
27
|
+
this.error('No credentials. Run: rift auth setup');
|
|
28
|
+
}
|
|
29
|
+
// The engine's `close-all` command reads HYPERLIQUID_PRIVATE_KEY
|
|
30
|
+
// from env (not CLI args, for security — see comment in close-all
|
|
31
|
+
// source). The spawned engine subprocess inherits process.env, so
|
|
32
|
+
// we set it here before passthroughToEngine spawns.
|
|
33
|
+
process.env.HYPERLIQUID_PRIVATE_KEY = creds.private_key;
|
|
34
|
+
try {
|
|
35
|
+
// Delegates to the engine's `close-all` command, which is the
|
|
36
|
+
// direct close path (reduce-only IOC market order). Distinct from
|
|
37
|
+
// `close-position` which writes a command file for a running algo
|
|
38
|
+
// daemon — that's the right call when you have an algo session
|
|
39
|
+
// managing a position; `perp close` is for the manual-trade lifecycle
|
|
40
|
+
// or for emergency cleanup.
|
|
41
|
+
const engineArgs = ['--account', getAccountAddress(creds)];
|
|
42
|
+
if (args.coin)
|
|
43
|
+
engineArgs.push('--coin', args.coin);
|
|
44
|
+
await passthroughToEngine({
|
|
45
|
+
command: 'close-all',
|
|
46
|
+
args: engineArgs,
|
|
47
|
+
log: (m) => this.log(m),
|
|
48
|
+
error: (m) => this.error(m),
|
|
49
|
+
exit: (c) => this.exit(c),
|
|
50
|
+
jsonOnly: flags.json,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
delete process.env.HYPERLIQUID_PRIVATE_KEY;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PerpLong extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
coin: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
size: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
stop: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
leverage: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
3
|
+
export default class PerpLong extends GatedCommand {
|
|
4
|
+
static description = 'Open a LONG position on Hyperliquid perps with a stop loss and live monitor.';
|
|
5
|
+
static examples = [
|
|
6
|
+
'$ rift perp long BTC --size 50 --stop 2 # $50 long, 2% stop',
|
|
7
|
+
'$ rift perp long ETH --size 25 --leverage 3 # $25 long at 3x',
|
|
8
|
+
];
|
|
9
|
+
static args = {
|
|
10
|
+
coin: Args.string({ description: 'Coin (e.g. BTC, ETH, SOL)', required: true }),
|
|
11
|
+
};
|
|
12
|
+
static flags = {
|
|
13
|
+
size: Flags.integer({ description: 'Position size in USD', required: true }),
|
|
14
|
+
stop: Flags.string({ description: 'Stop loss % (default: 2)', default: '2' }),
|
|
15
|
+
leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(PerpLong);
|
|
19
|
+
// Delegate to `rift trade` which already has the rich live monitor
|
|
20
|
+
// dashboard for the manual-trade lifecycle. This wrapper exists so
|
|
21
|
+
// `rift perp long BTC` reads naturally without the user having to
|
|
22
|
+
// type "long" twice or remember the `rift trade <pair> <direction>`
|
|
23
|
+
// positional-arg order.
|
|
24
|
+
// --yes skips the "Type GO" prompt that `rift trade` shows for the
|
|
25
|
+
// legacy `rift trade BTC long` form — `rift perp long BTC` already
|
|
26
|
+
// requires the user to type the direction in the verb, so a second
|
|
27
|
+
// confirmation is redundant. The position-size + stop are still on
|
|
28
|
+
// the command line and the manual-trade daemon's risk disclaimers
|
|
29
|
+
// remain in place.
|
|
30
|
+
await this.config.runCommand('trade', [
|
|
31
|
+
args.coin, 'long',
|
|
32
|
+
'--size', String(flags.size),
|
|
33
|
+
'--stop', flags.stop,
|
|
34
|
+
'--leverage', String(flags.leverage),
|
|
35
|
+
'--yes',
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PerpShort extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
coin: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
size: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
stop: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
leverage: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
3
|
+
export default class PerpShort extends GatedCommand {
|
|
4
|
+
static description = 'Open a SHORT position on Hyperliquid perps with a stop loss and live monitor.';
|
|
5
|
+
static examples = [
|
|
6
|
+
'$ rift perp short BTC --size 50 --stop 2 # $50 short, 2% stop',
|
|
7
|
+
'$ rift perp short ETH --size 25 --leverage 3 # $25 short at 3x',
|
|
8
|
+
];
|
|
9
|
+
static args = {
|
|
10
|
+
coin: Args.string({ description: 'Coin (e.g. BTC, ETH, SOL)', required: true }),
|
|
11
|
+
};
|
|
12
|
+
static flags = {
|
|
13
|
+
size: Flags.integer({ description: 'Position size in USD', required: true }),
|
|
14
|
+
stop: Flags.string({ description: 'Stop loss % (default: 2)', default: '2' }),
|
|
15
|
+
leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(PerpShort);
|
|
19
|
+
await this.config.runCommand('trade', [
|
|
20
|
+
args.coin, 'short',
|
|
21
|
+
'--size', String(flags.size),
|
|
22
|
+
'--stop', flags.stop,
|
|
23
|
+
'--leverage', String(flags.leverage),
|
|
24
|
+
'--yes',
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PerpStatus extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 PerpStatus extends GatedCommand {
|
|
5
|
+
static description = 'Show current perp account state — positions, margin used, unrealized PnL, open orders.';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift perp status',
|
|
8
|
+
'$ rift perp status --json',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(PerpStatus);
|
|
15
|
+
// `state` is the canonical engine command for "what's my account
|
|
16
|
+
// doing right now": positions, equity, margin, algo sessions, etc.
|
|
17
|
+
await passthroughToEngine({
|
|
18
|
+
command: 'state',
|
|
19
|
+
args: [],
|
|
20
|
+
log: (m) => this.log(m),
|
|
21
|
+
error: (m) => this.error(m),
|
|
22
|
+
exit: (c) => this.exit(c),
|
|
23
|
+
jsonOnly: flags.json,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { red, yellow, cyan, bold, dim, } from '../../lib/tui.js';
|
|
6
|
+
export default class PortfolioAlerts extends GatedCommand {
|
|
7
|
+
static description = 'Show recent portfolio alerts';
|
|
8
|
+
static examples = [
|
|
9
|
+
'$ rift portfolio alerts',
|
|
10
|
+
];
|
|
11
|
+
async run() {
|
|
12
|
+
const alertsFile = path.join(getDataDir(), 'algo', 'alerts.log');
|
|
13
|
+
if (!fs.existsSync(alertsFile)) {
|
|
14
|
+
this.log(`\n ${dim('No alerts yet.')}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const lines = fs.readFileSync(alertsFile, 'utf-8').trim().split('\n').filter(l => l.trim());
|
|
18
|
+
if (lines.length === 0) {
|
|
19
|
+
this.log(`\n ${dim('No alerts yet.')}\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.log('');
|
|
23
|
+
this.log(` ${bold('Recent Alerts')} ${dim(`(${lines.length} total)`)}`);
|
|
24
|
+
this.log(` ${dim('─'.repeat(60))}`);
|
|
25
|
+
this.log('');
|
|
26
|
+
// Show last 20
|
|
27
|
+
const recent = lines.slice(-20);
|
|
28
|
+
for (const line of recent) {
|
|
29
|
+
try {
|
|
30
|
+
const alert = JSON.parse(line);
|
|
31
|
+
const event = (alert.event || '').padEnd(18);
|
|
32
|
+
const eventColor = event.includes('trade') ? cyan
|
|
33
|
+
: event.includes('health') ? yellow
|
|
34
|
+
: event.includes('drawdown') ? red
|
|
35
|
+
: event.includes('session_died') ? red
|
|
36
|
+
: event.includes('schedule') ? dim
|
|
37
|
+
: event.includes('risk') ? yellow
|
|
38
|
+
: dim;
|
|
39
|
+
this.log(` ${dim(alert.time || '??:??')} ${eventColor(event)} ${alert.message || ''}`);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Skip unparseable lines
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.log('');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
run(): Promise<void>;
|
|
9
|
+
private renderResult;
|
|
10
|
+
private row;
|
|
11
|
+
private rowColored;
|
|
12
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
4
|
+
import { runEngine } from '../../lib/python-bridge.js';
|
|
5
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
6
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
7
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
8
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
9
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
10
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
11
|
+
function colorNum(val, suffix = '') {
|
|
12
|
+
const str = `${val}${suffix}`;
|
|
13
|
+
if (val > 0)
|
|
14
|
+
return green(str);
|
|
15
|
+
if (val < 0)
|
|
16
|
+
return red(str);
|
|
17
|
+
return yellow(str);
|
|
18
|
+
}
|
|
19
|
+
export default class PortfolioBacktest extends GatedCommand {
|
|
20
|
+
static description = 'Backtest a portfolio of multiple strategies simultaneously';
|
|
21
|
+
static examples = [
|
|
22
|
+
'$ rift portfolio backtest strategies/configs/portfolio_btc.yaml',
|
|
23
|
+
];
|
|
24
|
+
static args = {
|
|
25
|
+
config: Args.string({ description: 'Path to portfolio.yaml config file', required: true }),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { args } = await this.parse(PortfolioBacktest);
|
|
29
|
+
this.log('');
|
|
30
|
+
this.log(` ${bold('Portfolio Backtest')}`);
|
|
31
|
+
this.log(` ${dim(`Config: ${args.config}`)}`);
|
|
32
|
+
this.log('');
|
|
33
|
+
const configPath = path.resolve(args.config);
|
|
34
|
+
await runEngine('portfolio-backtest', [configPath], (msg) => {
|
|
35
|
+
if (msg.type === 'progress' && msg.msg) {
|
|
36
|
+
process.stdout.write(`\r ${dim(String(msg.msg))}${''.padEnd(20)}`);
|
|
37
|
+
}
|
|
38
|
+
else if (msg.type === 'result') {
|
|
39
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
40
|
+
this.renderResult(msg);
|
|
41
|
+
}
|
|
42
|
+
else if (msg.type === 'error') {
|
|
43
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
44
|
+
this.error(msg.msg);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
renderResult(msg) {
|
|
49
|
+
const strategies = msg.strategies;
|
|
50
|
+
const corrMatrix = msg.correlation_matrix;
|
|
51
|
+
// Per-strategy results table
|
|
52
|
+
this.log(dim(' ── Per-Strategy Results ──'));
|
|
53
|
+
this.log('');
|
|
54
|
+
const hdr = ` ${dim('│')} ${'Strategy'.padEnd(20)} ${'Alloc'.padEnd(8)} ${'Return'.padEnd(12)} ${'Sharpe'.padEnd(10)} ${'PF'.padEnd(8)} ${'MaxDD'.padEnd(10)} ${'Trades'.padEnd(8)} ${'Win%'.padEnd(8)} ${dim('│')}`;
|
|
55
|
+
const hrLen = hdr.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
56
|
+
const hr = ` ${dim('─'.repeat(hrLen - 2))}`;
|
|
57
|
+
this.log(hr);
|
|
58
|
+
this.log(hdr);
|
|
59
|
+
this.log(hr);
|
|
60
|
+
for (const s of strategies) {
|
|
61
|
+
const retStr = `${s.return_pct}%`;
|
|
62
|
+
const ddStr = `${s.max_drawdown_pct}%`;
|
|
63
|
+
let row = ` ${dim('│')} ${bold(String(s.name).padEnd(20))} `;
|
|
64
|
+
row += `${String(s.allocation).padEnd(8)} `;
|
|
65
|
+
row += `${colorNum(s.return_pct, '%')}${' '.repeat(Math.max(1, 12 - retStr.length))} `;
|
|
66
|
+
row += `${colorNum(s.sharpe)}${' '.repeat(Math.max(1, 10 - String(s.sharpe).length))} `;
|
|
67
|
+
row += `${String(s.profit_factor).padEnd(8)} `;
|
|
68
|
+
row += `${colorNum(s.max_drawdown_pct, '%')}${' '.repeat(Math.max(1, 10 - ddStr.length))} `;
|
|
69
|
+
row += `${String(s.num_trades).padEnd(8)} `;
|
|
70
|
+
row += `${String(s.win_rate).padEnd(8)} `;
|
|
71
|
+
row += dim('│');
|
|
72
|
+
this.log(row);
|
|
73
|
+
}
|
|
74
|
+
this.log(hr);
|
|
75
|
+
this.log('');
|
|
76
|
+
// Portfolio summary box
|
|
77
|
+
const w = 55;
|
|
78
|
+
const boxHr = '─'.repeat(w - 2);
|
|
79
|
+
this.log(` ${dim('┌' + boxHr + '┐')}`);
|
|
80
|
+
this.log(` ${dim('│')} ${bold('PORTFOLIO SUMMARY')}${' '.repeat(w - 20)}${dim('│')}`);
|
|
81
|
+
this.log(` ${dim('├' + boxHr + '┤')}`);
|
|
82
|
+
this.log(this.row('Initial Equity', `$${Number(msg.initial_equity).toLocaleString()}`, w));
|
|
83
|
+
this.log(this.row('Final Equity', `$${Number(msg.final_equity).toLocaleString()}`, w));
|
|
84
|
+
this.log(this.rowColored('Portfolio Return', msg.total_return_pct, '%', w));
|
|
85
|
+
this.log(this.rowColored('Portfolio Sharpe', msg.portfolio_sharpe, '', w));
|
|
86
|
+
this.log(this.rowColored('Portfolio Max Drawdown', msg.portfolio_max_drawdown_pct, '%', w));
|
|
87
|
+
this.log(this.row('Total Trades', String(msg.total_trades), w));
|
|
88
|
+
this.log(this.row('Strategies', String(strategies.length), w));
|
|
89
|
+
this.log(` ${dim('└' + boxHr + '┘')}`);
|
|
90
|
+
this.log('');
|
|
91
|
+
// Correlation matrix
|
|
92
|
+
if (corrMatrix && corrMatrix.strategies.length > 1) {
|
|
93
|
+
this.log(dim(' ── Strategy Correlation ──'));
|
|
94
|
+
this.log('');
|
|
95
|
+
const names = corrMatrix.strategies;
|
|
96
|
+
const matrix = corrMatrix.matrix;
|
|
97
|
+
// Header
|
|
98
|
+
let corrHdr = ` ${''.padEnd(20)}`;
|
|
99
|
+
for (const n of names) {
|
|
100
|
+
corrHdr += `${n.slice(0, 12).padEnd(14)}`;
|
|
101
|
+
}
|
|
102
|
+
this.log(dim(corrHdr));
|
|
103
|
+
// Rows
|
|
104
|
+
for (let i = 0; i < names.length; i++) {
|
|
105
|
+
let corrRow = ` ${names[i].padEnd(20)}`;
|
|
106
|
+
for (let j = 0; j < names.length; j++) {
|
|
107
|
+
const val = matrix[i][j];
|
|
108
|
+
const valStr = val.toFixed(3);
|
|
109
|
+
if (i === j) {
|
|
110
|
+
corrRow += dim(valStr.padEnd(14));
|
|
111
|
+
}
|
|
112
|
+
else if (val > 0.7) {
|
|
113
|
+
corrRow += red(valStr.padEnd(14)); // high correlation = warning
|
|
114
|
+
}
|
|
115
|
+
else if (val < 0.3) {
|
|
116
|
+
corrRow += green(valStr.padEnd(14)); // low correlation = good
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
corrRow += yellow(valStr.padEnd(14));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.log(corrRow);
|
|
123
|
+
}
|
|
124
|
+
this.log('');
|
|
125
|
+
// Check for high correlations
|
|
126
|
+
for (let i = 0; i < names.length; i++) {
|
|
127
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
128
|
+
if (matrix[i][j] > 0.7) {
|
|
129
|
+
this.log(` ${yellow('!')} ${names[i]} and ${names[j]} are highly correlated (${matrix[i][j].toFixed(3)}) — diversification benefit is limited`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const allLow = matrix.every((row, i) => row.every((val, j) => i === j || val < 0.5));
|
|
134
|
+
if (allLow) {
|
|
135
|
+
this.log(` ${green('✔')} Strategy correlations are low — good portfolio diversification`);
|
|
136
|
+
}
|
|
137
|
+
this.log('');
|
|
138
|
+
}
|
|
139
|
+
// Interpretation
|
|
140
|
+
const portfolioReturn = msg.total_return_pct;
|
|
141
|
+
const portfolioSharpe = msg.portfolio_sharpe;
|
|
142
|
+
const portfolioDD = msg.portfolio_max_drawdown_pct;
|
|
143
|
+
this.log(dim(' ── Interpretation ──'));
|
|
144
|
+
this.log('');
|
|
145
|
+
if (portfolioSharpe > 0.5 && portfolioDD > -10) {
|
|
146
|
+
this.log(` ${green('Strong portfolio.')} Positive Sharpe with controlled drawdown.`);
|
|
147
|
+
}
|
|
148
|
+
else if (portfolioSharpe > 0 && portfolioDD > -20) {
|
|
149
|
+
this.log(` ${yellow('Moderate portfolio.')} Positive returns but drawdown needs attention.`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.log(` ${red('Weak portfolio.')} Consider adjusting allocations or strategy selection.`);
|
|
153
|
+
}
|
|
154
|
+
// Compare portfolio to best individual strategy
|
|
155
|
+
const bestStrat = strategies.reduce((best, s) => (s.sharpe > best.sharpe ? s : best), strategies[0]);
|
|
156
|
+
if (portfolioSharpe > bestStrat.sharpe) {
|
|
157
|
+
this.log(` ${green('Portfolio Sharpe (' + portfolioSharpe.toFixed(3) + ') exceeds best individual strategy (' + bestStrat.name + ': ' + bestStrat.sharpe.toFixed(3) + ').')}`);
|
|
158
|
+
this.log(` ${dim('Diversification is adding value.')}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.log(` ${dim(`Best individual Sharpe: ${bestStrat.name} (${bestStrat.sharpe.toFixed(3)}) vs portfolio (${portfolioSharpe.toFixed(3)})`)}`);
|
|
162
|
+
}
|
|
163
|
+
this.log('');
|
|
164
|
+
}
|
|
165
|
+
row(label, value, width) {
|
|
166
|
+
const labelStr = ` ${label}:`;
|
|
167
|
+
const cleanVal = value.replace(/\x1b\[[0-9;]*m/g, '');
|
|
168
|
+
const padding = width - labelStr.length - cleanVal.length - 3;
|
|
169
|
+
return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${value} ${dim('│')}`;
|
|
170
|
+
}
|
|
171
|
+
rowColored(label, value, suffix, width) {
|
|
172
|
+
const coloredStr = colorNum(value, suffix);
|
|
173
|
+
const cleanStr = `${value}${suffix}`;
|
|
174
|
+
const labelStr = ` ${label}:`;
|
|
175
|
+
const padding = width - labelStr.length - cleanStr.length - 3;
|
|
176
|
+
return ` ${dim('│')}${labelStr}${' '.repeat(Math.max(1, padding))}${coloredStr} ${dim('│')}`;
|
|
177
|
+
}
|
|
178
|
+
}
|