@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,1176 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { runEngine } from '../lib/python-bridge.js';
|
|
7
|
+
/**
|
|
8
|
+
* Collect the final "result" message from the engine, ignoring progress messages.
|
|
9
|
+
*/
|
|
10
|
+
function collectResult(command, args) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let result = null;
|
|
13
|
+
runEngine(command, args, (msg) => {
|
|
14
|
+
if (msg.type === 'result') {
|
|
15
|
+
result = msg;
|
|
16
|
+
}
|
|
17
|
+
else if (msg.type === 'error') {
|
|
18
|
+
reject(new Error(msg.msg));
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
.then(() => {
|
|
22
|
+
if (result) {
|
|
23
|
+
resolve(result);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
reject(new Error('No result returned from engine'));
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.catch(reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Clean internal fields from engine result */
|
|
33
|
+
function cleanResult(result) {
|
|
34
|
+
const { type, command, ...clean } = result;
|
|
35
|
+
return clean;
|
|
36
|
+
}
|
|
37
|
+
export default class Serve extends GatedCommand {
|
|
38
|
+
static description = 'Start RIFT as an MCP server for AI agent integration';
|
|
39
|
+
static examples = [
|
|
40
|
+
'$ rift serve',
|
|
41
|
+
];
|
|
42
|
+
static flags = {
|
|
43
|
+
debug: Flags.boolean({ description: 'Enable debug logging to stderr', default: false }),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(Serve);
|
|
47
|
+
const server = new McpServer({
|
|
48
|
+
name: 'rift',
|
|
49
|
+
version: '0.1.0',
|
|
50
|
+
});
|
|
51
|
+
// Validated schemas — prevent path traversal and injection
|
|
52
|
+
const safeStrategy = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/, 'Invalid strategy name: letters, numbers, underscores only');
|
|
53
|
+
const safePair = z.string().regex(/^[a-zA-Z0-9/:]{1,20}(-PERP)?$/i, 'Invalid pair name');
|
|
54
|
+
const safeTimeframe = z.string().regex(/^[0-9]+[mhdwM]$/, 'Invalid timeframe (e.g. 1h, 4h, 1d)');
|
|
55
|
+
const safeCoin = z.string().regex(/^[a-zA-Z0-9]{1,10}$/, 'Invalid coin name');
|
|
56
|
+
// ═══════════════════════════════════════════
|
|
57
|
+
// GROUP 1: Core tools (updated)
|
|
58
|
+
// ═══════════════════════════════════════════
|
|
59
|
+
server.tool('backtest', 'Run a backtest of a trading strategy on historical Hyperliquid data. Supports crypto perps and TradFi (SP500, TSLA, CL, GOLD). Returns performance metrics, diagnostics, and regime analysis.', {
|
|
60
|
+
strategy: z.string().describe('Strategy name (any registered or workbench strategy)'),
|
|
61
|
+
pair: z.string().default('BTC-PERP').describe('Trading pair (e.g. BTC-PERP, ETH-PERP, SOL-PERP, HYPE-PERP)'),
|
|
62
|
+
timeframe: z.string().optional().describe('Candle timeframe (auto-detected from strategy if omitted). Options: 1m, 5m, 15m, 30m, 1h, 4h'),
|
|
63
|
+
equity: z.number().default(10000).describe('Starting equity in USDC'),
|
|
64
|
+
all_pairs: z.boolean().default(false).describe('If true, test across top 10 pairs by volume and rank results by Sharpe ratio'),
|
|
65
|
+
}, async ({ strategy, pair, timeframe, equity, all_pairs }) => {
|
|
66
|
+
try {
|
|
67
|
+
const args = [strategy, '--pair', pair, '--equity', String(equity)];
|
|
68
|
+
if (timeframe)
|
|
69
|
+
args.push('--tf', timeframe);
|
|
70
|
+
if (all_pairs)
|
|
71
|
+
args.push('--all-pairs', '--top', '10');
|
|
72
|
+
const result = await collectResult('backtest', args);
|
|
73
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
server.tool('compare', 'Compare multiple trading strategies head-to-head on the same data. Returns side-by-side metrics and identifies the best by both return and Sharpe ratio.', {
|
|
80
|
+
strategies: z.string().describe('Comma-separated strategy names'),
|
|
81
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
82
|
+
timeframe: z.string().default('1h').describe('Candle timeframe'),
|
|
83
|
+
equity: z.number().default(10000).describe('Starting equity in USDC'),
|
|
84
|
+
}, async ({ strategies, pair, timeframe, equity }) => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await collectResult('compare', [
|
|
87
|
+
strategies, '--pair', pair, '--tf', timeframe, '--equity', String(equity),
|
|
88
|
+
]);
|
|
89
|
+
const results = result.results;
|
|
90
|
+
const bestReturn = results.reduce((a, b) => a.total_return_pct > b.total_return_pct ? a : b);
|
|
91
|
+
const bestSharpe = results.reduce((a, b) => a.sharpe_ratio > b.sharpe_ratio ? a : b);
|
|
92
|
+
return { content: [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify({
|
|
95
|
+
results,
|
|
96
|
+
best_by_return: { strategy: bestReturn.strategy, return_pct: bestReturn.total_return_pct },
|
|
97
|
+
best_by_sharpe: { strategy: bestSharpe.strategy, sharpe: bestSharpe.sharpe_ratio },
|
|
98
|
+
}, null, 2),
|
|
99
|
+
}] };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.tool('list_strategies', 'List all available trading strategies (validated + custom workbench strategies) with configurations and descriptions.', {}, async () => {
|
|
106
|
+
try {
|
|
107
|
+
const result = await collectResult('strategies', []);
|
|
108
|
+
// Also get workbench strategies
|
|
109
|
+
let workbenchStrategies = [];
|
|
110
|
+
try {
|
|
111
|
+
const wbResult = await collectResult('workbench-list', []);
|
|
112
|
+
workbenchStrategies = wbResult.strategies || [];
|
|
113
|
+
}
|
|
114
|
+
catch { /* no workbench strategies */ }
|
|
115
|
+
return { content: [{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: JSON.stringify({
|
|
118
|
+
validated: result.strategies,
|
|
119
|
+
custom: workbenchStrategies,
|
|
120
|
+
}, null, 2),
|
|
121
|
+
}] };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.tool('fetch_data', 'Download and cache candle data + funding rates from Hyperliquid. Usually not needed — backtest and research auto-fetch. Use this to pre-cache data for a specific date range.', {
|
|
128
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
129
|
+
timeframe: z.string().default('1h').describe('Candle timeframe'),
|
|
130
|
+
start: z.string().optional().describe('Start date YYYY-MM-DD (optional)'),
|
|
131
|
+
}, async ({ pair, timeframe, start }) => {
|
|
132
|
+
try {
|
|
133
|
+
const args = [pair, '--tf', timeframe];
|
|
134
|
+
if (start)
|
|
135
|
+
args.push('--start', start);
|
|
136
|
+
const result = await collectResult('fetch', args);
|
|
137
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
server.tool('cost', 'Estimate pre-trade cost for a hypothetical trade: fees + funding + impact + slippage. Returns breakdown in bps and USD, plus ADV-utilization warning. Use to answer "what will it cost me to trade $X of <coin> right now?" without actually executing.', {
|
|
144
|
+
pair: safePair.describe('Trading pair (BTC, ETH-PERP, etc.)'),
|
|
145
|
+
notional_usd: z.number().positive().describe('Trade size in USD notional'),
|
|
146
|
+
side: z.enum(['buy', 'sell', 'long', 'short']).default('buy').describe('Trade direction'),
|
|
147
|
+
timeframe: z.string().default('1h').describe('Candle interval for ADV / vol calc'),
|
|
148
|
+
hold_hours: z.number().nonnegative().default(0).describe('Holding period in hours (for funding accrual estimate)'),
|
|
149
|
+
maker: z.boolean().default(false).describe('Treat as maker (post-only) instead of taker'),
|
|
150
|
+
spot: z.boolean().default(false).describe('Treat as spot trade instead of perp'),
|
|
151
|
+
include_builder_fee: z.boolean().default(true).describe('Include RIFT builder fee'),
|
|
152
|
+
tier_volume_14d_usd: z.number().nonnegative().default(0).describe('Your 14d HL volume USD for fee-tier lookup'),
|
|
153
|
+
}, async ({ pair, notional_usd, side, timeframe, hold_hours, maker, spot, include_builder_fee, tier_volume_14d_usd }) => {
|
|
154
|
+
try {
|
|
155
|
+
const args = [pair, String(notional_usd)];
|
|
156
|
+
args.push('--side', side);
|
|
157
|
+
args.push('--tf', timeframe);
|
|
158
|
+
args.push('--hold', String(hold_hours));
|
|
159
|
+
if (maker)
|
|
160
|
+
args.push('--maker');
|
|
161
|
+
if (spot)
|
|
162
|
+
args.push('--spot');
|
|
163
|
+
if (!include_builder_fee)
|
|
164
|
+
args.push('--no-builder-fee');
|
|
165
|
+
args.push('--tier-vol-14d', String(tier_volume_14d_usd));
|
|
166
|
+
const result = await collectResult('cost', args);
|
|
167
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
server.tool('list_data', 'List all locally cached candle datasets with pair, timeframe, candle count, and date range.', {}, async () => {
|
|
174
|
+
try {
|
|
175
|
+
const result = await collectResult('list-data', []);
|
|
176
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
server.tool('doctor', 'Check RIFT system health. Verifies Python, dependencies, API connectivity, cached data, and strategies.', {}, async () => {
|
|
183
|
+
try {
|
|
184
|
+
const result = await collectResult('doctor', []);
|
|
185
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.checks, null, 2) }] };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// ═══════════════════════════════════════════
|
|
192
|
+
// GROUP 2: Validation pipeline
|
|
193
|
+
// ═══════════════════════════════════════════
|
|
194
|
+
server.tool('research', 'Run the full validation pipeline on a strategy: backtest + walk-forward analysis + Monte Carlo simulation + multi-pair test. Returns a grade (A/B/C/D/F) with detailed metrics. This is the most comprehensive strategy assessment tool. Supports config overrides for testing optimized parameters.', {
|
|
195
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
196
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
197
|
+
timeframe: z.string().optional().describe('Timeframe (auto-detected if omitted)'),
|
|
198
|
+
equity: z.number().default(10000).describe('Starting equity'),
|
|
199
|
+
config_overrides: z.string().optional().describe('JSON string of config param overrides (e.g. \'{"stop_loss_pct": 0.03, "max_hold_candles": 24}\')'),
|
|
200
|
+
}, async ({ strategy, pair, timeframe, equity, config_overrides }) => {
|
|
201
|
+
try {
|
|
202
|
+
const args = [strategy, '--pair', pair, '--equity', String(equity)];
|
|
203
|
+
if (timeframe)
|
|
204
|
+
args.push('--tf', timeframe);
|
|
205
|
+
if (config_overrides)
|
|
206
|
+
args.push('--config-overrides', config_overrides);
|
|
207
|
+
const result = await collectResult('research', args);
|
|
208
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
server.tool('walk_forward', 'Run walk-forward analysis to test strategy robustness. Splits data into rolling train/test windows and measures out-of-sample performance degradation. ROBUST (>0.7) means the strategy generalizes well.', {
|
|
215
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
216
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
217
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
218
|
+
config: z.string().default('3m/1m').describe('Train/test config (e.g. 3m/1m = 3 months train, 1 month test)'),
|
|
219
|
+
equity: z.number().default(10000).describe('Starting equity per window'),
|
|
220
|
+
}, async ({ strategy, pair, timeframe, config, equity }) => {
|
|
221
|
+
try {
|
|
222
|
+
const result = await collectResult('walk-forward', [
|
|
223
|
+
strategy, '--pair', pair, '--tf', timeframe, '--wf', config, '--equity', String(equity),
|
|
224
|
+
]);
|
|
225
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
server.tool('montecarlo', 'Run Monte Carlo simulation — bootstrap resample the trade sequence 10,000 times to estimate probability of profit, ruin, and return distribution percentiles.', {
|
|
232
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
233
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
234
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
235
|
+
runs: z.number().default(10000).describe('Number of simulations'),
|
|
236
|
+
equity: z.number().default(10000).describe('Starting equity'),
|
|
237
|
+
}, async ({ strategy, pair, timeframe, runs, equity }) => {
|
|
238
|
+
try {
|
|
239
|
+
const result = await collectResult('montecarlo', [
|
|
240
|
+
strategy, '--pair', pair, '--tf', timeframe, '--runs', String(runs), '--equity', String(equity),
|
|
241
|
+
]);
|
|
242
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// ═══════════════════════════════════════════
|
|
249
|
+
// GROUP 3: Optimization
|
|
250
|
+
// ═══════════════════════════════════════════
|
|
251
|
+
server.tool('sweep', 'Run a parameter sweep to find optimal strategy settings. Tests all parameter combinations and ranks by Sharpe ratio (or return/profit factor). Returns the top results with their exact parameter values.', {
|
|
252
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
253
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
254
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
255
|
+
top: z.number().default(5).describe('Number of top results to return'),
|
|
256
|
+
rank_by: z.enum(['sharpe', 'return', 'profit_factor']).default('sharpe').describe('Ranking metric'),
|
|
257
|
+
}, async ({ strategy, pair, timeframe, top, rank_by }) => {
|
|
258
|
+
try {
|
|
259
|
+
const result = await collectResult('sweep', [
|
|
260
|
+
strategy, '--pair', pair, '--tf', timeframe, '--top', String(top), '--rank', rank_by,
|
|
261
|
+
]);
|
|
262
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
server.tool('save_optimized', 'Save an optimized strategy by copying a base strategy and replacing its config defaults with new parameter values. Creates a new .py file in the workbench directory that can be backtested, researched, and traded.', {
|
|
269
|
+
base_strategy: safeStrategy.describe('Base strategy to copy from'),
|
|
270
|
+
new_name: z.string().describe('Name for the new strategy (snake_case)'),
|
|
271
|
+
params: z.string().describe('JSON string of optimized parameters (e.g. \'{"stop_loss_pct": 0.03, "entry_dev": 3.75}\')'),
|
|
272
|
+
}, async ({ base_strategy, new_name, params }) => {
|
|
273
|
+
try {
|
|
274
|
+
const result = await collectResult('save-optimized', [base_strategy, new_name, params]);
|
|
275
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// ═══════════════════════════════════════════
|
|
282
|
+
// GROUP 4: Strategy building (workbench)
|
|
283
|
+
// ═══════════════════════════════════════════
|
|
284
|
+
server.tool('workbench_create', 'Create a new custom strategy from a template. Templates: "funding", "vwap_reversion", "trend_follow", "blank". 56 indicators available including cross-asset, multi-timeframe, and adaptive. No Python knowledge needed.', {
|
|
285
|
+
name: safeStrategy.describe('Strategy name (snake_case)'),
|
|
286
|
+
template: z.enum(['funding', 'vwap_reversion', 'trend_follow', 'blank']).default('blank').describe('Template to start from'),
|
|
287
|
+
}, async ({ name, template }) => {
|
|
288
|
+
try {
|
|
289
|
+
const result = await collectResult('workbench-create', [name, '--template', template]);
|
|
290
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
server.tool('workbench_update', 'Update a custom strategy config and regenerate the Python code. Pass the full config JSON with modified entry/exit conditions, risk settings, or filters.', {
|
|
297
|
+
name: safeStrategy.describe('Strategy name to update'),
|
|
298
|
+
config: z.string().describe('Full config JSON string'),
|
|
299
|
+
}, async ({ name, config }) => {
|
|
300
|
+
try {
|
|
301
|
+
const result = await collectResult('workbench-update', [name, config]);
|
|
302
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
server.tool('workbench_show', 'Show a custom strategy\'s current config — entry/exit conditions, risk settings, filters, and version.', {
|
|
309
|
+
name: safeStrategy.describe('Strategy name'),
|
|
310
|
+
}, async ({ name }) => {
|
|
311
|
+
try {
|
|
312
|
+
const result = await collectResult('workbench-show', [name]);
|
|
313
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
server.tool('quick_test', 'Fast backtest with automatic delta comparison to the last test. Every test is logged to the experiment database. Use this for rapid iteration — change a parameter, quick test, see if it improved.', {
|
|
320
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
321
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
322
|
+
change_description: z.string().optional().describe('Description of what changed (logged for experiment tracking)'),
|
|
323
|
+
}, async ({ strategy, pair, change_description }) => {
|
|
324
|
+
try {
|
|
325
|
+
const args = [strategy, '--pair', pair];
|
|
326
|
+
if (change_description)
|
|
327
|
+
args.push('--change', change_description);
|
|
328
|
+
const result = await collectResult('quick-test', args);
|
|
329
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
// ═══════════════════════════════════════════
|
|
336
|
+
// GROUP 5: Market intelligence
|
|
337
|
+
// ═══════════════════════════════════════════
|
|
338
|
+
server.tool('indicator_stats', 'Get real market statistics for a trading pair from cached data. Shows distributions and recommended values for: funding rate, RSI, VWAP z-score, ADX, volume ratio, EMA distance, ATR. Use this to understand market conditions and choose appropriate strategy parameters.', {
|
|
339
|
+
pair: z.string().default('BTC').describe('Trading pair (without -PERP suffix)'),
|
|
340
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
341
|
+
}, async ({ pair, timeframe }) => {
|
|
342
|
+
try {
|
|
343
|
+
const result = await collectResult('indicator-stats', ['--pair', pair, '--tf', timeframe]);
|
|
344
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
// ═══════════════════════════════════════════
|
|
351
|
+
// GROUP 6: Experiment tracking
|
|
352
|
+
// ═══════════════════════════════════════════
|
|
353
|
+
server.tool('experiments', 'View experiment history for a strategy. Shows every quick test result with config snapshot, metrics, version, and what changed. Use this to track iteration progress and avoid repeating experiments.', {
|
|
354
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
355
|
+
limit: z.number().default(20).describe('Number of experiments to return'),
|
|
356
|
+
}, async ({ strategy, limit }) => {
|
|
357
|
+
try {
|
|
358
|
+
const result = await collectResult('experiments', [strategy, '--limit', String(limit)]);
|
|
359
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
server.tool('smart_sweep', 'Smart parameter optimization using Bayesian search (Optuna). Finds optimal strategy parameters in ~50-80 trials instead of testing every combination. 10x faster than grid sweep.', {
|
|
366
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
367
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
368
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
369
|
+
trials: z.number().default(80).describe('Number of optimization trials (50-100 recommended)'),
|
|
370
|
+
target: z.enum(['sharpe', 'return', 'calmar']).default('sharpe').describe('What to optimize for'),
|
|
371
|
+
}, async ({ strategy, pair, timeframe, trials, target }) => {
|
|
372
|
+
try {
|
|
373
|
+
const result = await collectResult('smart-sweep', [strategy, '--pair', pair, '--tf', timeframe, '--trials', String(trials), '--target', target]);
|
|
374
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
server.tool('feature_importance', 'Discover which indicators predict profitable trades using XGBoost. Trains a classifier on all indicator values at trade entry points, ranks features by predictive power. Reveals hidden patterns.', {
|
|
381
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
382
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
383
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
384
|
+
}, async ({ strategy, pair, timeframe }) => {
|
|
385
|
+
try {
|
|
386
|
+
const result = await collectResult('feature-importance', [strategy, '--pair', pair, '--tf', timeframe]);
|
|
387
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
server.tool('tearsheet', 'Generate a professional HTML performance tearsheet with 30+ charts: equity curve, drawdowns, monthly returns, rolling Sharpe, return distribution. Opens at ~/.rift/reports/.', {
|
|
394
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
395
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
396
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
397
|
+
}, async ({ strategy, pair, timeframe }) => {
|
|
398
|
+
try {
|
|
399
|
+
const result = await collectResult('tearsheet', [strategy, '--pair', pair, '--tf', timeframe]);
|
|
400
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
server.tool('health', 'Check strategy health using institutional-grade decay detection. Returns a 0-100 score with CUSUM change detection, factor decomposition (alpha vs beta), statistical decay testing, and execution quality analysis. Grade A-F with recommendation (continue/reduce/pause/stop).', {
|
|
407
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
408
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
409
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
410
|
+
}, async ({ strategy, pair, timeframe }) => {
|
|
411
|
+
try {
|
|
412
|
+
const result = await collectResult('health', [strategy, '--pair', pair, '--tf', timeframe]);
|
|
413
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
// ═══════════════════════════════════════════
|
|
420
|
+
// GROUP 7: Live trading (daemon)
|
|
421
|
+
// ═══════════════════════════════════════════
|
|
422
|
+
server.tool('algo_start', 'Start an algo trading session as a background daemon. The trading engine runs persistently — it survives disconnects, app closes, and session changes. Returns immediately with the daemon PID. Use algo_status to monitor and algo_stop to end.', {
|
|
423
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
424
|
+
pair: z.string().default('BTC').describe('Trading pair (e.g. BTC, ETH, SOL)'),
|
|
425
|
+
timeframe: z.string().optional().describe('Candle timeframe (default: strategy default)'),
|
|
426
|
+
equity: z.number().default(0).describe('Starting equity in USDC (0 = auto-detect from account)'),
|
|
427
|
+
}, async ({ strategy, pair, timeframe, equity }) => {
|
|
428
|
+
try {
|
|
429
|
+
const { spawnDaemon, getAlgoPidsDir } = await import('../lib/python-bridge.js');
|
|
430
|
+
const { loadCredentials, hasFullSetup, getAccountAddress } = await import('../lib/credentials.js');
|
|
431
|
+
const fs = await import('node:fs');
|
|
432
|
+
const path = await import('node:path');
|
|
433
|
+
if (!hasFullSetup()) {
|
|
434
|
+
return { content: [{ type: 'text', text: 'Error: Account not set up. Run: rift auth setup' }], isError: true };
|
|
435
|
+
}
|
|
436
|
+
const creds = loadCredentials();
|
|
437
|
+
if (!creds) {
|
|
438
|
+
return { content: [{ type: 'text', text: 'Error: No credentials found. Run: rift auth setup' }], isError: true };
|
|
439
|
+
}
|
|
440
|
+
// Check if already running
|
|
441
|
+
const coin = pair.replace(/-PERP/i, '').toUpperCase();
|
|
442
|
+
const key = `${strategy}_${coin}`;
|
|
443
|
+
const pidFile = path.join(getAlgoPidsDir(), `${key}.pid`);
|
|
444
|
+
if (fs.existsSync(pidFile)) {
|
|
445
|
+
try {
|
|
446
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
447
|
+
process.kill(pid, 0);
|
|
448
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'already_running', key, pid }, null, 2) }] };
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
fs.unlinkSync(pidFile);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const engineArgs = [strategy, '--pair', pair, '--equity', String(equity), '--account', getAccountAddress(creds)];
|
|
455
|
+
if (timeframe)
|
|
456
|
+
engineArgs.push('--tf', timeframe);
|
|
457
|
+
const { pid } = spawnDaemon('algo', engineArgs, { HYPERLIQUID_PRIVATE_KEY: creds.private_key });
|
|
458
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
459
|
+
status: 'started',
|
|
460
|
+
key,
|
|
461
|
+
pid,
|
|
462
|
+
strategy,
|
|
463
|
+
pair: coin,
|
|
464
|
+
msg: `Algo trading daemon started. Use algo_status to monitor, algo_stop to end.`,
|
|
465
|
+
}, null, 2) }] };
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
server.tool('algo_status', 'Get the current state of all running algo trading sessions. Returns equity, P&L, position details, trade count, health score, and more. Works whether the session was started from CLI, MCP, or any other interface.', {}, async () => {
|
|
472
|
+
try {
|
|
473
|
+
const result = await collectResult('algo-status', []);
|
|
474
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
server.tool('algo_stop', 'Stop a running algo trading session. Sends a graceful shutdown signal — the daemon closes any open position, saves the session log, and exits. Returns the final session summary.', {
|
|
481
|
+
strategy: safeStrategy.describe('Strategy name to stop'),
|
|
482
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
483
|
+
}, async ({ strategy, pair }) => {
|
|
484
|
+
try {
|
|
485
|
+
const result = await collectResult('algo-stop', ['--strategy', strategy, '--pair', pair]);
|
|
486
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
// ═══════════════════════════════════════════
|
|
493
|
+
// GROUP 8: Portfolio management
|
|
494
|
+
// ═══════════════════════════════════════════
|
|
495
|
+
server.tool('portfolio_start', 'Start the portfolio supervisor daemon to manage multiple algo trading strategies simultaneously. Coordinates risk across strategies, enforces scheduling, monitors health, auto-rotates decaying strategies, and fires alerts. Configure via portfolio.yaml.', {
|
|
496
|
+
config_path: z.string().optional().describe('Path to portfolio.yaml (default: ~/.rift/algo/portfolio.yaml)'),
|
|
497
|
+
}, async ({ config_path }) => {
|
|
498
|
+
try {
|
|
499
|
+
const { spawnDaemon, getDataDir } = await import('../lib/python-bridge.js');
|
|
500
|
+
const { loadCredentials, hasFullSetup, getAccountAddress } = await import('../lib/credentials.js');
|
|
501
|
+
const fs = await import('node:fs');
|
|
502
|
+
const path = await import('node:path');
|
|
503
|
+
if (!hasFullSetup()) {
|
|
504
|
+
return { content: [{ type: 'text', text: 'Error: Account not set up. Run: rift auth setup' }], isError: true };
|
|
505
|
+
}
|
|
506
|
+
const creds = loadCredentials();
|
|
507
|
+
if (!creds) {
|
|
508
|
+
return { content: [{ type: 'text', text: 'Error: No credentials' }], isError: true };
|
|
509
|
+
}
|
|
510
|
+
// Check if already running
|
|
511
|
+
const pidFile = path.join(getDataDir(), 'algo', 'supervisor.pid');
|
|
512
|
+
if (fs.existsSync(pidFile)) {
|
|
513
|
+
try {
|
|
514
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
515
|
+
process.kill(pid, 0);
|
|
516
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'already_running', pid }, null, 2) }] };
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
fs.unlinkSync(pidFile);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const cfgPath = config_path || path.join(getDataDir(), 'algo', 'portfolio.yaml');
|
|
523
|
+
const args = ['--config', cfgPath, '--account', getAccountAddress(creds)];
|
|
524
|
+
const { pid } = spawnDaemon('portfolio-start', args, { HYPERLIQUID_PRIVATE_KEY: creds.private_key });
|
|
525
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
526
|
+
status: 'started', pid,
|
|
527
|
+
msg: 'Portfolio supervisor started. Use portfolio_status to monitor.',
|
|
528
|
+
}, null, 2) }] };
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
server.tool('portfolio_status', 'Get the full state of the portfolio supervisor: all managed strategies, positions, aggregate risk metrics (net/gross exposure, per-asset, drawdown), health scores, and recent alerts.', {}, async () => {
|
|
535
|
+
try {
|
|
536
|
+
const result = await collectResult('portfolio-status', []);
|
|
537
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
server.tool('portfolio_stop', 'Stop the portfolio supervisor and all managed strategy daemons. Gracefully closes all positions, saves session logs, and returns final portfolio summary.', {}, async () => {
|
|
544
|
+
try {
|
|
545
|
+
const result = await collectResult('portfolio-stop', []);
|
|
546
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
server.tool('portfolio_alerts', 'Get recent portfolio alerts — trades, stop losses, health drops, drawdown warnings, session failures, and scheduling events.', {
|
|
553
|
+
limit: z.number().default(20).describe('Number of recent alerts to return'),
|
|
554
|
+
}, async ({ limit }) => {
|
|
555
|
+
try {
|
|
556
|
+
const fs = await import('node:fs');
|
|
557
|
+
const path = await import('node:path');
|
|
558
|
+
const { getDataDir } = await import('../lib/python-bridge.js');
|
|
559
|
+
const alertsFile = path.join(getDataDir(), 'algo', 'alerts.log');
|
|
560
|
+
if (!fs.existsSync(alertsFile)) {
|
|
561
|
+
return { content: [{ type: 'text', text: JSON.stringify({ alerts: [] }, null, 2) }] };
|
|
562
|
+
}
|
|
563
|
+
const lines = fs.readFileSync(alertsFile, 'utf-8').trim().split('\n').filter((l) => l.trim());
|
|
564
|
+
const alerts = lines.slice(-limit).map((l) => {
|
|
565
|
+
try {
|
|
566
|
+
return JSON.parse(l);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}).filter(Boolean);
|
|
572
|
+
return { content: [{ type: 'text', text: JSON.stringify({ alerts }, null, 2) }] };
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
// ═══════════════════════════════════════════
|
|
579
|
+
// GROUP 9: Analytics & Reporting
|
|
580
|
+
// ═══════════════════════════════════════════
|
|
581
|
+
server.tool('tca_report', 'Transaction Cost Analysis — analyze execution quality across algo trading sessions. Shows slippage (bps), market impact, fee costs, TWAP vs IOC comparison, post-fill markouts at t+1s/10s/60s/300s (positive = trader edge, negative = adverse selection), and grades execution A-F relative to asset volatility.', {
|
|
582
|
+
session: z.string().optional().describe('Path to specific session log (default: all sessions)'),
|
|
583
|
+
}, async ({ session }) => {
|
|
584
|
+
try {
|
|
585
|
+
const args = session ? ['--session', session] : [];
|
|
586
|
+
const result = await collectResult('tca', args);
|
|
587
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
server.tool('pnl_attribution', 'Decompose P&L into components: alpha (strategy edge), beta (market exposure), funding income, and execution costs (slippage + fees). Uses linear regression to separate true alpha from market drift.', {
|
|
594
|
+
session: z.string().optional().describe('Path to specific session log (default: all sessions)'),
|
|
595
|
+
}, async ({ session }) => {
|
|
596
|
+
try {
|
|
597
|
+
const args = session ? ['--session', session] : [];
|
|
598
|
+
const result = await collectResult('attribution', args);
|
|
599
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
server.tool('generate_report', 'Generate an HTML performance report with equity curves, TCA summary, PnL attribution waterfall, trade log, and a substrate.stats tearsheet (bootstrap CIs, PSR). Returns the file path.', {
|
|
606
|
+
portfolio: z.boolean().default(false).describe('Generate portfolio-level report (all strategies combined)'),
|
|
607
|
+
period: z.enum(['all', 'daily', 'weekly']).default('all').describe('Report period'),
|
|
608
|
+
session: z.string().optional().describe('Path to specific session log'),
|
|
609
|
+
}, async ({ portfolio, period, session }) => {
|
|
610
|
+
try {
|
|
611
|
+
const args = [];
|
|
612
|
+
if (portfolio)
|
|
613
|
+
args.push('--portfolio');
|
|
614
|
+
if (period !== 'all')
|
|
615
|
+
args.push('--period', period);
|
|
616
|
+
if (session)
|
|
617
|
+
args.push('--session', session);
|
|
618
|
+
const result = await collectResult('report', args);
|
|
619
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
// ═══════════════════════════════════════════
|
|
626
|
+
// GROUP 10: REST API
|
|
627
|
+
// ═══════════════════════════════════════════
|
|
628
|
+
server.tool('api_start', 'Start the RIFT REST API server for dashboard and PMS integration. Returns the URL and auth token. Endpoints: /status, /positions, /trades, /alerts, /tca, /attribution, /health, /equity.', {
|
|
629
|
+
port: z.number().default(8420).describe('Port to listen on'),
|
|
630
|
+
require_auth: z.boolean().default(false).describe('Require auth token on all endpoints (not just POST)'),
|
|
631
|
+
}, async ({ port, require_auth }) => {
|
|
632
|
+
try {
|
|
633
|
+
const { spawnDaemon } = await import('../lib/python-bridge.js');
|
|
634
|
+
const fs = await import('node:fs');
|
|
635
|
+
const path = await import('node:path');
|
|
636
|
+
const { getDataDir } = await import('../lib/python-bridge.js');
|
|
637
|
+
// Check if already running
|
|
638
|
+
const pidFile = path.join(getDataDir(), 'algo', 'api.pid');
|
|
639
|
+
if (fs.existsSync(pidFile)) {
|
|
640
|
+
try {
|
|
641
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim());
|
|
642
|
+
process.kill(pid, 0);
|
|
643
|
+
const tokenFile = path.join(getDataDir(), 'api_token');
|
|
644
|
+
const token = fs.existsSync(tokenFile) ? fs.readFileSync(tokenFile, 'utf-8').trim() : '';
|
|
645
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'already_running', pid, url: `http://localhost:${port}`, token }, null, 2) }] };
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
fs.unlinkSync(pidFile);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const args = ['--port', String(port)];
|
|
652
|
+
if (require_auth)
|
|
653
|
+
args.push('--require-auth');
|
|
654
|
+
spawnDaemon('api-start', args);
|
|
655
|
+
// Wait briefly for token file
|
|
656
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
657
|
+
const tokenFile = path.join(getDataDir(), 'api_token');
|
|
658
|
+
const token = fs.existsSync(tokenFile) ? fs.readFileSync(tokenFile, 'utf-8').trim() : 'generating...';
|
|
659
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
660
|
+
status: 'started',
|
|
661
|
+
url: `http://localhost:${port}`,
|
|
662
|
+
token,
|
|
663
|
+
endpoints: ['/status', '/positions', '/trades', '/alerts', '/tca', '/attribution', '/health', '/equity'],
|
|
664
|
+
}, null, 2) }] };
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
// ═══════════════════════════════════════════
|
|
671
|
+
// GROUP 11: Risk & Compliance
|
|
672
|
+
// ═══════════════════════════════════════════
|
|
673
|
+
server.tool('var_report', 'Compute Value at Risk — "what is the most I can lose in 24h at 95% confidence?" Uses Cornish-Fisher adjustment for crypto fat tails. Includes CVaR (Expected Shortfall).', {
|
|
674
|
+
horizon: z.enum(['1h', '24h', '7d']).default('24h').describe('VaR time horizon'),
|
|
675
|
+
}, async ({ horizon }) => {
|
|
676
|
+
try {
|
|
677
|
+
const result = await collectResult('var', ['--horizon', horizon]);
|
|
678
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
server.tool('audit_export', 'Export compliance-grade trade log as CSV or JSON. Two rows per trade (OPEN/CLOSE) with timestamps, order IDs, fill prices, slippage, fees, wallet addresses. Suitable for prime broker reporting.', {
|
|
685
|
+
format: z.enum(['csv', 'json']).default('csv').describe('Export format'),
|
|
686
|
+
days: z.number().default(30).describe('Days of history to include'),
|
|
687
|
+
strategy: z.string().optional().describe('Filter by strategy name'),
|
|
688
|
+
}, async ({ format, days, strategy }) => {
|
|
689
|
+
try {
|
|
690
|
+
const args = ['--export', format, '--last', String(days)];
|
|
691
|
+
if (strategy)
|
|
692
|
+
args.push('--strategy', strategy);
|
|
693
|
+
const result = await collectResult('audit', args);
|
|
694
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
server.tool('strategy_versions', 'Show strategy version history — config snapshots and code hashes recorded at each algo session start. Use --diff to see what changed between versions.', {
|
|
701
|
+
strategy: z.string().optional().describe('Filter by strategy name'),
|
|
702
|
+
diff: z.boolean().default(false).describe('Show changes between last two versions'),
|
|
703
|
+
}, async ({ strategy, diff }) => {
|
|
704
|
+
try {
|
|
705
|
+
const args = [];
|
|
706
|
+
if (strategy)
|
|
707
|
+
args.push('--strategy', strategy);
|
|
708
|
+
if (diff)
|
|
709
|
+
args.push('--diff');
|
|
710
|
+
const result = await collectResult('versions', args);
|
|
711
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
// ═══════════════════════════════════════════
|
|
718
|
+
// GROUP 12: Retail — Scout & Trade
|
|
719
|
+
// ═══════════════════════════════════════════
|
|
720
|
+
server.tool('scout', 'Scan the market and rank trading opportunities by confluence. Analyzes top coins across 5 dimensions: funding, momentum, volatility, positioning, and cross-exchange signals. Returns ranked list with entry/stop/target levels.', {
|
|
721
|
+
top: z.number().default(20).describe('Number of coins to scan'),
|
|
722
|
+
timeframe: z.string().default('1h').describe('Timeframe for indicator computation'),
|
|
723
|
+
min_confluence: z.number().default(2).describe('Minimum confluence score (1-5)'),
|
|
724
|
+
}, async ({ top, timeframe, min_confluence }) => {
|
|
725
|
+
try {
|
|
726
|
+
const result = await collectResult('scout', ['--top', String(top), '--tf', timeframe, '--min', String(min_confluence)]);
|
|
727
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
server.tool('manual_trade', 'Place a manual trade with stop loss on Hyperliquid. The trade stays open until explicitly closed via algo_stop. Use scout first to find opportunities, then execute the best one.', {
|
|
734
|
+
coin: safeCoin.describe('Coin to trade (e.g. BTC, ETH, SOL)'),
|
|
735
|
+
side: z.enum(['long', 'short']).describe('Trade direction'),
|
|
736
|
+
// size_usd and stop_pct are intentionally REQUIRED — no default.
|
|
737
|
+
// An invisible default on a real-money trade is a footgun for AI
|
|
738
|
+
// agents that don't pass every field. Leverage stays defaulted at
|
|
739
|
+
// 1 because 1x (no leverage) is the conservative no-op.
|
|
740
|
+
size_usd: z.number().positive().describe('Position size in USD (required — no default; this places a real trade)'),
|
|
741
|
+
stop_pct: z.number().positive().describe('Stop loss percentage (required — e.g. 2 means 2%)'),
|
|
742
|
+
leverage: z.number().default(1).describe('Leverage multiplier (default 1 = no leverage)'),
|
|
743
|
+
}, async ({ coin, side, size_usd, stop_pct, leverage }) => {
|
|
744
|
+
try {
|
|
745
|
+
const { loadCredentials, hasFullSetup, getAccountAddress } = await import('../lib/credentials.js');
|
|
746
|
+
if (!hasFullSetup()) {
|
|
747
|
+
return { content: [{ type: 'text', text: 'Error: Account not set up. Run: rift auth setup' }], isError: true };
|
|
748
|
+
}
|
|
749
|
+
const creds = loadCredentials();
|
|
750
|
+
if (!creds) {
|
|
751
|
+
return { content: [{ type: 'text', text: 'Error: No credentials' }], isError: true };
|
|
752
|
+
}
|
|
753
|
+
const { spawnDaemon } = await import('../lib/python-bridge.js');
|
|
754
|
+
const { getAccountAddress: getAcct } = await import('../lib/credentials.js');
|
|
755
|
+
const args = [coin, side, '--size', String(size_usd), '--stop', String(stop_pct / 100), '--leverage', String(leverage), '--account', getAcct(creds)];
|
|
756
|
+
const { pid } = spawnDaemon('manual-trade', args, { HYPERLIQUID_PRIVATE_KEY: creds.private_key });
|
|
757
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
758
|
+
status: 'placed',
|
|
759
|
+
coin, side, size_usd, stop_pct, leverage, pid,
|
|
760
|
+
msg: `${side.toUpperCase()} ${coin} $${size_usd} placed. Use algo_status to monitor, algo_stop to close.`,
|
|
761
|
+
}, null, 2) }] };
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
// ── Canonical perp-namespace aliases for manual_trade. The
|
|
768
|
+
// `manual_trade` tool above remains registered for back-compat;
|
|
769
|
+
// perp_long / perp_short / perp_close make the verb match the
|
|
770
|
+
// actual action and pair naturally with spot_buy / spot_sell.
|
|
771
|
+
const perpOpen = async (side, coin, size_usd, stop_pct, leverage) => {
|
|
772
|
+
try {
|
|
773
|
+
const { loadCredentials, hasFullSetup, getAccountAddress } = await import('../lib/credentials.js');
|
|
774
|
+
if (!hasFullSetup()) {
|
|
775
|
+
return { content: [{ type: 'text', text: 'Error: Account not set up. Run: rift auth setup' }], isError: true };
|
|
776
|
+
}
|
|
777
|
+
const creds = loadCredentials();
|
|
778
|
+
if (!creds) {
|
|
779
|
+
return { content: [{ type: 'text', text: 'Error: No credentials' }], isError: true };
|
|
780
|
+
}
|
|
781
|
+
const { spawnDaemon } = await import('../lib/python-bridge.js');
|
|
782
|
+
const args = [coin, side, '--size', String(size_usd), '--stop', String(stop_pct / 100), '--leverage', String(leverage), '--account', getAccountAddress(creds)];
|
|
783
|
+
const { pid } = spawnDaemon('manual-trade', args, { HYPERLIQUID_PRIVATE_KEY: creds.private_key });
|
|
784
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
785
|
+
status: 'placed',
|
|
786
|
+
coin, side, size_usd, stop_pct, leverage, pid,
|
|
787
|
+
msg: `${side.toUpperCase()} ${coin} $${size_usd} placed (stop ${stop_pct}%, ${leverage}x). Daemon monitors until stop hit or SIGTERM.`,
|
|
788
|
+
}, null, 2) }] };
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
server.tool('perp_long', 'Open a LONG perp position with stop loss on Hyperliquid. The daemon monitors until stop hits or you call perp_close. Pairs naturally with spot_buy / spot_sell — note that "perp_long BTC" and "spot_buy BTC" are TOTALLY DIFFERENT actions even though both buy BTC.', {
|
|
795
|
+
coin: safeCoin.describe('Coin to long (e.g. BTC, ETH, SOL)'),
|
|
796
|
+
size_usd: z.number().positive().describe('Position size in USD (required)'),
|
|
797
|
+
stop_pct: z.number().positive().describe('Stop loss percentage (required — e.g. 2 means 2%)'),
|
|
798
|
+
leverage: z.number().default(1).describe('Leverage multiplier (default 1 = no leverage)'),
|
|
799
|
+
}, async ({ coin, size_usd, stop_pct, leverage }) => await perpOpen('long', coin, size_usd, stop_pct, leverage));
|
|
800
|
+
server.tool('perp_short', 'Open a SHORT perp position with stop loss on Hyperliquid. The daemon monitors until stop hits or you call perp_close. CAUTION: "perp_short BTC" OPENS A NEW SHORT — it does NOT close an existing long. To close a long, use perp_close.', {
|
|
801
|
+
coin: safeCoin.describe('Coin to short (e.g. BTC, ETH, SOL)'),
|
|
802
|
+
size_usd: z.number().positive().describe('Position size in USD (required)'),
|
|
803
|
+
stop_pct: z.number().positive().describe('Stop loss percentage (required — e.g. 2 means 2%)'),
|
|
804
|
+
leverage: z.number().default(1).describe('Leverage multiplier (default 1 = no leverage)'),
|
|
805
|
+
}, async ({ coin, size_usd, stop_pct, leverage }) => await perpOpen('short', coin, size_usd, stop_pct, leverage));
|
|
806
|
+
server.tool('perp_close', 'Close an open perp position (and cancel any orders for the coin) via reduce-only IOC market order. Pass coin to close a specific position; omit to close ALL perp positions. Use this for manual-trade lifecycle exit or emergency cleanup.', {
|
|
807
|
+
coin: z.string().default('').describe('Coin to close (e.g. BTC) — omit/empty to close ALL'),
|
|
808
|
+
}, async ({ coin }) => {
|
|
809
|
+
try {
|
|
810
|
+
const { loadCredentials, hasFullSetup, getAccountAddress } = await import('../lib/credentials.js');
|
|
811
|
+
if (!hasFullSetup()) {
|
|
812
|
+
return { content: [{ type: 'text', text: 'Error: Account not set up. Run: rift auth setup' }], isError: true };
|
|
813
|
+
}
|
|
814
|
+
const creds = loadCredentials();
|
|
815
|
+
if (!creds) {
|
|
816
|
+
return { content: [{ type: 'text', text: 'Error: No credentials' }], isError: true };
|
|
817
|
+
}
|
|
818
|
+
// close-all reads HL_PRIVATE_KEY from env (for security — not from
|
|
819
|
+
// CLI args). The subprocess inherits process.env so set it here.
|
|
820
|
+
// Also needs --account for HL queries (agent address ≠ main address).
|
|
821
|
+
process.env.HYPERLIQUID_PRIVATE_KEY = creds.private_key;
|
|
822
|
+
try {
|
|
823
|
+
const args = ['--account', getAccountAddress(creds)];
|
|
824
|
+
if (coin)
|
|
825
|
+
args.push('--coin', coin);
|
|
826
|
+
const result = await collectResult('close-all', args);
|
|
827
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
delete process.env.HYPERLIQUID_PRIVATE_KEY;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
// ═══════════════════════════════════════════
|
|
838
|
+
// GROUP 13: Spot Trading
|
|
839
|
+
// ═══════════════════════════════════════════
|
|
840
|
+
server.tool('buy', 'Buy a token on the Hyperliquid spot market. Simple spot purchase — no leverage, no margin. 1% builder fee on sell side only.', {
|
|
841
|
+
coin: safeCoin.describe('Token to buy (e.g. HYPE, ETH, BTC)'),
|
|
842
|
+
amount: z.number().default(0).describe('USDC amount to spend'),
|
|
843
|
+
size: z.number().default(0).describe('Token amount to buy (alternative to amount)'),
|
|
844
|
+
}, async ({ coin, amount, size }) => {
|
|
845
|
+
try {
|
|
846
|
+
const args = [coin];
|
|
847
|
+
if (amount > 0)
|
|
848
|
+
args.push('--amount', String(amount));
|
|
849
|
+
if (size > 0)
|
|
850
|
+
args.push('--size', String(size));
|
|
851
|
+
const result = await collectResult('buy', args);
|
|
852
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
server.tool('sell', 'Sell a token from spot holdings. 1% builder fee applies on sell side.', {
|
|
859
|
+
coin: safeCoin.describe('Token to sell (e.g. HYPE, ETH, BTC)'),
|
|
860
|
+
amount: z.number().default(0).describe('Token amount to sell (0 = all)'),
|
|
861
|
+
pct: z.number().default(0).describe('Percentage to sell (e.g. 50 = half)'),
|
|
862
|
+
}, async ({ coin, amount, pct }) => {
|
|
863
|
+
try {
|
|
864
|
+
const args = [coin];
|
|
865
|
+
if (amount > 0)
|
|
866
|
+
args.push('--amount', String(amount));
|
|
867
|
+
if (pct > 0)
|
|
868
|
+
args.push('--pct', String(pct));
|
|
869
|
+
const result = await collectResult('sell', args);
|
|
870
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
// ── Canonical spot-namespace aliases. The `buy` / `sell` tools above
|
|
877
|
+
// remain registered for back-compat with existing AI prompts; the
|
|
878
|
+
// `spot_buy` / `spot_sell` names disambiguate vs perp trading on the
|
|
879
|
+
// AI agent surface (`spot_sell BTC` = exit BTC holdings; `perp_short
|
|
880
|
+
// BTC` = open a short — two very different actions).
|
|
881
|
+
server.tool('spot_buy', 'Buy a token on the Hyperliquid SPOT market. Same as the legacy `buy` tool — disambiguates vs perp trading. Simple purchase, no leverage. 1% builder fee on sell side only.', {
|
|
882
|
+
coin: safeCoin.describe('Token to buy (e.g. HYPE, ETH, BTC — auto-resolves to UBTC/UETH on HL spot)'),
|
|
883
|
+
amount: z.number().default(0).describe('USDC amount to spend'),
|
|
884
|
+
size: z.number().default(0).describe('Token amount to buy (alternative to amount)'),
|
|
885
|
+
}, async ({ coin, amount, size }) => {
|
|
886
|
+
try {
|
|
887
|
+
const args = [coin];
|
|
888
|
+
if (amount > 0)
|
|
889
|
+
args.push('--amount', String(amount));
|
|
890
|
+
if (size > 0)
|
|
891
|
+
args.push('--size', String(size));
|
|
892
|
+
const result = await collectResult('buy', args);
|
|
893
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
server.tool('spot_sell', 'Sell a token from SPOT holdings back to USDC. Same as the legacy `sell` tool — disambiguates vs perp trading. 1% builder fee applies.', {
|
|
900
|
+
coin: safeCoin.describe('Token to sell (e.g. HYPE, ETH, BTC)'),
|
|
901
|
+
amount: z.number().default(0).describe('Token amount to sell (0 = all)'),
|
|
902
|
+
pct: z.number().default(0).describe('Percentage to sell (e.g. 50 = half)'),
|
|
903
|
+
}, async ({ coin, amount, pct }) => {
|
|
904
|
+
try {
|
|
905
|
+
const args = [coin];
|
|
906
|
+
if (amount > 0)
|
|
907
|
+
args.push('--amount', String(amount));
|
|
908
|
+
if (pct > 0)
|
|
909
|
+
args.push('--pct', String(pct));
|
|
910
|
+
const result = await collectResult('sell', args);
|
|
911
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
server.tool('holdings', 'View spot token holdings with current prices, USD values, and P&L.', {}, async () => {
|
|
918
|
+
try {
|
|
919
|
+
const result = await collectResult('holdings', []);
|
|
920
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
server.tool('balance', 'Show combined spot and perps wallet balances — total equity across both accounts.', {}, async () => {
|
|
927
|
+
try {
|
|
928
|
+
const result = await collectResult('balance', []);
|
|
929
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
server.tool('transfer', 'Transfer USDC between spot and perps on Hyperliquid. Requires wallet approval via push notification.', {
|
|
936
|
+
amount: z.number().describe('USDC amount to transfer'),
|
|
937
|
+
direction: z.enum(['to-perps', 'to-spot']).default('to-perps').describe('Transfer direction'),
|
|
938
|
+
}, async ({ amount, direction }) => {
|
|
939
|
+
try {
|
|
940
|
+
const args = [String(amount)];
|
|
941
|
+
if (direction === 'to-spot')
|
|
942
|
+
args.push('--to-spot');
|
|
943
|
+
else
|
|
944
|
+
args.push('--to-perps');
|
|
945
|
+
const result = await collectResult('transfer', args);
|
|
946
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
server.tool('withdraw', 'Withdraw USDC from Hyperliquid to Arbitrum. Requires wallet approval via push notification. $1 fee.', {
|
|
953
|
+
amount: z.number().describe('USDC amount to withdraw'),
|
|
954
|
+
}, async ({ amount }) => {
|
|
955
|
+
try {
|
|
956
|
+
const result = await collectResult('withdraw', [String(amount)]);
|
|
957
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
server.tool('deposit', 'Deposit USDC from Arbitrum to Hyperliquid via bridge. Requires 2 wallet approvals (permit + transaction). Minimum 5 USDC.', {
|
|
964
|
+
amount: z.number().describe('USDC amount to deposit (minimum 5)'),
|
|
965
|
+
}, async ({ amount }) => {
|
|
966
|
+
try {
|
|
967
|
+
const result = await collectResult('deposit', [String(amount)]);
|
|
968
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
// ═══════════════════════════════════════════
|
|
975
|
+
// GROUP 14: AI Intelligence Layer
|
|
976
|
+
// ═══════════════════════════════════════════
|
|
977
|
+
server.tool('state', 'Full project snapshot — registered strategies, running algo sessions, validated edge, recent lessons, alerts, auth status, and data inventory. Call this first in any new conversation for instant context.', {}, async () => {
|
|
978
|
+
try {
|
|
979
|
+
const result = await collectResult('state', []);
|
|
980
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
server.tool('lessons', 'Query lessons learned from past research and trading. Prevents repeating failed experiments. Auto-recorded after research and walk-forward.', {
|
|
987
|
+
coin: safeCoin.default('').describe('Filter by coin'),
|
|
988
|
+
strategy: safeStrategy.default('').describe('Filter by strategy'),
|
|
989
|
+
limit: z.number().default(20).describe('Number of lessons'),
|
|
990
|
+
}, async ({ coin, strategy, limit }) => {
|
|
991
|
+
try {
|
|
992
|
+
const args = ['--limit', String(limit)];
|
|
993
|
+
if (coin)
|
|
994
|
+
args.push('--coin', coin);
|
|
995
|
+
if (strategy)
|
|
996
|
+
args.push('--strategy', strategy);
|
|
997
|
+
const result = await collectResult('lessons', args);
|
|
998
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
server.tool('add_lesson', 'Manually record a lesson learned from research or trading.', {
|
|
1005
|
+
coin: z.string().describe('Coin tested'),
|
|
1006
|
+
approach: z.string().describe('What was tried'),
|
|
1007
|
+
result: z.enum(['pass', 'fail']).describe('Outcome'),
|
|
1008
|
+
reason: z.string().default('').describe('Why it passed/failed'),
|
|
1009
|
+
}, async ({ coin, approach, result, reason }) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const args = ['--coin', coin, '--approach', approach, '--result', result];
|
|
1012
|
+
if (reason)
|
|
1013
|
+
args.push('--reason', reason);
|
|
1014
|
+
const res = await collectResult('add-lesson', args);
|
|
1015
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(res), null, 2) }] };
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
server.tool('verify', 'Compare strategy performance vs buy-and-hold on a specific date range. Shows alpha (excess return) and verdict.', {
|
|
1022
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
1023
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
1024
|
+
from: z.string().default('').describe('Start date YYYY-MM-DD'),
|
|
1025
|
+
to: z.string().default('').describe('End date YYYY-MM-DD'),
|
|
1026
|
+
}, async ({ strategy, pair, from: fromDate, to: toDate }) => {
|
|
1027
|
+
try {
|
|
1028
|
+
const args = [strategy, '--pair', pair];
|
|
1029
|
+
if (fromDate)
|
|
1030
|
+
args.push('--from', fromDate);
|
|
1031
|
+
if (toDate)
|
|
1032
|
+
args.push('--to', toDate);
|
|
1033
|
+
const result = await collectResult('verify', args);
|
|
1034
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
server.tool('scan', 'Scan all indicators for predictive power against forward returns. Ranks by information coefficient (Spearman). Discovers edge from raw data without writing a strategy.', {
|
|
1041
|
+
pair: safePair.default('BTC').describe('Trading pair'),
|
|
1042
|
+
timeframe: z.string().default('1h').describe('Timeframe'),
|
|
1043
|
+
forward: z.string().default('4h').describe('Forward return horizon (e.g. 1h, 4h, 24h)'),
|
|
1044
|
+
}, async ({ pair, timeframe, forward }) => {
|
|
1045
|
+
try {
|
|
1046
|
+
const result = await collectResult('scan', ['--pair', pair, '--tf', timeframe, '--forward', forward]);
|
|
1047
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
server.tool('data_inventory', 'Show all available data — coins, timeframes, candle counts, date ranges. Includes crypto and TradFi tickers.', {}, async () => {
|
|
1054
|
+
try {
|
|
1055
|
+
const result = await collectResult('data-inventory', []);
|
|
1056
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
server.tool('history', 'List past algo trading sessions with P&L, trades, and outcomes.', {
|
|
1063
|
+
limit: z.number().default(10).describe('Number of sessions'),
|
|
1064
|
+
strategy: safeStrategy.default('').describe('Filter by strategy'),
|
|
1065
|
+
}, async ({ limit, strategy }) => {
|
|
1066
|
+
try {
|
|
1067
|
+
const args = ['--limit', String(limit)];
|
|
1068
|
+
if (strategy)
|
|
1069
|
+
args.push('--strategy', strategy);
|
|
1070
|
+
const result = await collectResult('history', args);
|
|
1071
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
// ═══════════════════════════════════════════
|
|
1078
|
+
// GROUP 15: Position Management
|
|
1079
|
+
// ═══════════════════════════════════════════
|
|
1080
|
+
server.tool('close_position', 'Close position on a running algo trading session. Sends command via IPC — daemon closes at market.', {
|
|
1081
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
1082
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
1083
|
+
}, async ({ strategy, pair }) => {
|
|
1084
|
+
try {
|
|
1085
|
+
const result = await collectResult('close-position', [strategy, '--pair', pair]);
|
|
1086
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
server.tool('tighten_stop', 'Update stop loss price on a running algo session.', {
|
|
1093
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
1094
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
1095
|
+
price: z.number().describe('New stop loss price'),
|
|
1096
|
+
}, async ({ strategy, pair, price }) => {
|
|
1097
|
+
try {
|
|
1098
|
+
const result = await collectResult('tighten-stop', [strategy, '--pair', pair, '--price', String(price)]);
|
|
1099
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
server.tool('reduce_position', 'Reduce position size on a running algo session (e.g. close half).', {
|
|
1106
|
+
strategy: safeStrategy.describe('Strategy name'),
|
|
1107
|
+
pair: safePair.default('BTC-PERP').describe('Trading pair'),
|
|
1108
|
+
pct: z.number().default(50).describe('Percentage to close (50 = half)'),
|
|
1109
|
+
}, async ({ strategy, pair, pct }) => {
|
|
1110
|
+
try {
|
|
1111
|
+
const result = await collectResult('reduce-position', [strategy, '--pair', pair, '--pct', String(pct)]);
|
|
1112
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
// ═══════════════════════════════════════════
|
|
1119
|
+
// GROUP 16: Watchdog
|
|
1120
|
+
// ═══════════════════════════════════════════
|
|
1121
|
+
server.tool('watchdog_events', 'Query recent market watchdog events — funding extremes, premium divergence, volume spikes.', {
|
|
1122
|
+
since: z.string().default('24h').describe('Time window (e.g. 1h, 24h, 7d)'),
|
|
1123
|
+
coin: safeCoin.default('').describe('Filter by coin'),
|
|
1124
|
+
}, async ({ since, coin }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const args = ['--since', since];
|
|
1127
|
+
if (coin)
|
|
1128
|
+
args.push('--coin', coin);
|
|
1129
|
+
const result = await collectResult('watchdog-events', args);
|
|
1130
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
server.tool('guide', 'Print the 9-step research-to-trade journey. Start here if new to RIFT.', {}, async () => {
|
|
1137
|
+
try {
|
|
1138
|
+
const result = await collectResult('guide', []);
|
|
1139
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
server.tool('auth_setup', 'Set up wallet authentication non-interactively. Works from any interface (MCP, mobile, web).', {
|
|
1146
|
+
key: z.string().describe('Hyperliquid API wallet private key (0x-prefixed)'),
|
|
1147
|
+
account: z.string().default('').describe('Main wallet address (optional, derived from key if empty)'),
|
|
1148
|
+
}, async ({ key, account }) => {
|
|
1149
|
+
try {
|
|
1150
|
+
const args = ['setup', '--key', key];
|
|
1151
|
+
if (account)
|
|
1152
|
+
args.push('--account', account);
|
|
1153
|
+
const result = await collectResult('auth', args);
|
|
1154
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
server.tool('auth_status', 'Check wallet authentication status.', {}, async () => {
|
|
1161
|
+
try {
|
|
1162
|
+
const result = await collectResult('auth', ['status']);
|
|
1163
|
+
return { content: [{ type: 'text', text: JSON.stringify(cleanResult(result), null, 2) }] };
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
// Connect via stdio transport
|
|
1170
|
+
const transport = new StdioServerTransport();
|
|
1171
|
+
if (flags.debug) {
|
|
1172
|
+
console.error('RIFT MCP server starting on stdio');
|
|
1173
|
+
}
|
|
1174
|
+
await server.connect(transport);
|
|
1175
|
+
}
|
|
1176
|
+
}
|