hedgequantx 2.9.19 → 2.9.20
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/package.json +1 -1
- package/src/app.js +42 -64
- package/src/lib/m/hqx-2b.js +7 -0
- package/src/lib/m/index.js +138 -0
- package/src/lib/m/ultra-scalping.js +7 -0
- package/src/menus/connect.js +14 -17
- package/src/menus/dashboard.js +58 -76
- package/src/pages/accounts.js +38 -49
- package/src/pages/algo/copy-trading.js +546 -178
- package/src/pages/algo/index.js +18 -75
- package/src/pages/algo/one-account.js +322 -57
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +19 -22
- package/src/pages/positions.js +19 -22
- package/src/pages/stats/index.js +15 -16
- package/src/pages/user.js +7 -11
- package/src/services/ai-supervision/health.js +35 -47
- package/src/services/index.js +1 -9
- package/src/services/rithmic/accounts.js +8 -6
- package/src/ui/box.js +9 -5
- package/src/ui/index.js +5 -18
- package/src/ui/menu.js +4 -4
- package/src/pages/ai-agents-ui.js +0 -388
- package/src/pages/ai-agents.js +0 -494
- package/src/pages/ai-models.js +0 -389
- package/src/pages/algo/algo-executor.js +0 -307
- package/src/pages/algo/copy-executor.js +0 -331
- package/src/pages/algo/custom-strategy.js +0 -313
- package/src/services/ai-supervision/consensus.js +0 -284
- package/src/services/ai-supervision/context.js +0 -275
- package/src/services/ai-supervision/directive.js +0 -167
- package/src/services/ai-supervision/index.js +0 -359
- package/src/services/ai-supervision/parser.js +0 -278
- package/src/services/ai-supervision/symbols.js +0 -259
- package/src/services/cliproxy/index.js +0 -256
- package/src/services/cliproxy/installer.js +0 -111
- package/src/services/cliproxy/manager.js +0 -387
- package/src/services/llmproxy/index.js +0 -166
- package/src/services/llmproxy/manager.js +0 -411
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copy Trading Executor - Execution engine for copy trading
|
|
3
|
-
* Handles signal processing, order placement, and AI supervision
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const readline = require('readline');
|
|
7
|
-
const { connections } = require('../../services');
|
|
8
|
-
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
9
|
-
const { SupervisionEngine } = require('../../services/ai-supervision');
|
|
10
|
-
const { M1 } = require('../../lib/m/s1');
|
|
11
|
-
const { MarketDataFeed } = require('../../lib/data');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Launch Copy Trading execution
|
|
15
|
-
*/
|
|
16
|
-
const launchCopyTrading = async (config) => {
|
|
17
|
-
const { lead, followers, contract, dailyTarget, maxRisk, showNames, supervisionConfig } = config;
|
|
18
|
-
|
|
19
|
-
// Initialize AI Supervision if configured
|
|
20
|
-
const supervisionEnabled = supervisionConfig?.supervisionEnabled && supervisionConfig?.agents?.length > 0;
|
|
21
|
-
const supervisionEngine = supervisionEnabled ? new SupervisionEngine(supervisionConfig) : null;
|
|
22
|
-
const aiContext = { recentTicks: [], recentSignals: [], maxTicks: 100 };
|
|
23
|
-
|
|
24
|
-
const leadAccount = lead.account;
|
|
25
|
-
const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
|
|
26
|
-
const leadName = showNames
|
|
27
|
-
? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
|
|
28
|
-
: 'HQX Lead *****';
|
|
29
|
-
const symbolName = contract.name;
|
|
30
|
-
const contractId = contract.id;
|
|
31
|
-
const tickSize = contract.tickSize || 0.25;
|
|
32
|
-
|
|
33
|
-
const followerNames = followers.map((f, i) =>
|
|
34
|
-
showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const ui = new AlgoUI({
|
|
38
|
-
subtitle: supervisionEnabled ? 'HQX Copy + AI' : 'HQX Copy Trading',
|
|
39
|
-
mode: 'copy-trading'
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const stats = {
|
|
43
|
-
accountName: leadName,
|
|
44
|
-
followerNames,
|
|
45
|
-
symbol: symbolName,
|
|
46
|
-
qty: lead.contracts,
|
|
47
|
-
followerQty: followers[0]?.contracts || lead.contracts,
|
|
48
|
-
target: dailyTarget,
|
|
49
|
-
risk: maxRisk,
|
|
50
|
-
propfirm: leadAccount.propfirm || 'Unknown',
|
|
51
|
-
platform: leadAccount.platform || 'Rithmic',
|
|
52
|
-
pnl: 0,
|
|
53
|
-
followerPnl: 0,
|
|
54
|
-
trades: 0,
|
|
55
|
-
wins: 0,
|
|
56
|
-
losses: 0,
|
|
57
|
-
latency: 0,
|
|
58
|
-
connected: false,
|
|
59
|
-
startTime: Date.now(),
|
|
60
|
-
followersCount: followers.length
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
let running = true;
|
|
64
|
-
let stopReason = null;
|
|
65
|
-
let startingPnL = null;
|
|
66
|
-
let currentPosition = 0;
|
|
67
|
-
let pendingOrder = false;
|
|
68
|
-
let tickCount = 0;
|
|
69
|
-
|
|
70
|
-
// Initialize Strategy
|
|
71
|
-
const strategy = new M1({ tickSize });
|
|
72
|
-
strategy.initialize(contractId, tickSize);
|
|
73
|
-
|
|
74
|
-
// Initialize Market Data Feed
|
|
75
|
-
const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
|
|
76
|
-
|
|
77
|
-
// Log startup
|
|
78
|
-
ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
|
|
79
|
-
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${lead.contracts}/${followers[0]?.contracts}`);
|
|
80
|
-
ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
81
|
-
if (supervisionEnabled) ui.addLog('info', `AI: ${supervisionEngine.getActiveCount()} agents`);
|
|
82
|
-
ui.addLog('info', 'Connecting...');
|
|
83
|
-
|
|
84
|
-
// Handle strategy signals
|
|
85
|
-
strategy.on('signal', async (signal) => {
|
|
86
|
-
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
87
|
-
|
|
88
|
-
let { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
89
|
-
|
|
90
|
-
aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
|
|
91
|
-
if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
|
|
92
|
-
|
|
93
|
-
ui.addLog('signal', `${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
94
|
-
|
|
95
|
-
// AI Supervision
|
|
96
|
-
if (supervisionEnabled && supervisionEngine) {
|
|
97
|
-
const result = await supervisionEngine.supervise({
|
|
98
|
-
symbolId: symbolName,
|
|
99
|
-
signal: { direction, entry, stopLoss, takeProfit, confidence },
|
|
100
|
-
recentTicks: aiContext.recentTicks,
|
|
101
|
-
recentSignals: aiContext.recentSignals,
|
|
102
|
-
stats,
|
|
103
|
-
config: { dailyTarget, maxRisk }
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (result.decision === 'reject') {
|
|
107
|
-
ui.addLog('info', `AI rejected: ${result.reason}`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Apply optimizations
|
|
112
|
-
if (result.optimizedSignal?.aiOptimized) {
|
|
113
|
-
const opt = result.optimizedSignal;
|
|
114
|
-
if (opt.entry) entry = opt.entry;
|
|
115
|
-
if (opt.stopLoss) stopLoss = opt.stopLoss;
|
|
116
|
-
if (opt.takeProfit) takeProfit = opt.takeProfit;
|
|
117
|
-
}
|
|
118
|
-
ui.addLog('info', `AI ${result.decision} (${result.confidence}%)`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Execute orders
|
|
122
|
-
pendingOrder = true;
|
|
123
|
-
try {
|
|
124
|
-
const orderSide = direction === 'long' ? 0 : 1;
|
|
125
|
-
|
|
126
|
-
// Place order on LEAD
|
|
127
|
-
const leadResult = await leadService.placeOrder({
|
|
128
|
-
accountId: leadAccount.accountId,
|
|
129
|
-
contractId,
|
|
130
|
-
type: 2,
|
|
131
|
-
side: orderSide,
|
|
132
|
-
size: lead.contracts
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (leadResult.success) {
|
|
136
|
-
currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
|
|
137
|
-
stats.trades++;
|
|
138
|
-
ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x`);
|
|
139
|
-
|
|
140
|
-
// Place orders on FOLLOWERS
|
|
141
|
-
await placeFollowerOrders(followers, contractId, orderSide, direction, ui);
|
|
142
|
-
|
|
143
|
-
// Bracket orders on lead
|
|
144
|
-
if (stopLoss && takeProfit) {
|
|
145
|
-
await placeBracketOrders(leadService, leadAccount, contractId, direction, lead.contracts, stopLoss, takeProfit);
|
|
146
|
-
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
ui.addLog('error', `Lead failed: ${leadResult.error}`);
|
|
150
|
-
}
|
|
151
|
-
} catch (e) {
|
|
152
|
-
ui.addLog('error', `Order error: ${e.message}`);
|
|
153
|
-
}
|
|
154
|
-
pendingOrder = false;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Handle market data ticks
|
|
158
|
-
marketFeed.on('tick', (tick) => {
|
|
159
|
-
tickCount++;
|
|
160
|
-
const latencyStart = Date.now();
|
|
161
|
-
|
|
162
|
-
aiContext.recentTicks.push(tick);
|
|
163
|
-
if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
|
|
164
|
-
|
|
165
|
-
strategy.processTick({
|
|
166
|
-
contractId: tick.contractId || contractId,
|
|
167
|
-
price: tick.price,
|
|
168
|
-
bid: tick.bid,
|
|
169
|
-
ask: tick.ask,
|
|
170
|
-
volume: tick.volume || 1,
|
|
171
|
-
side: tick.lastTradeSide || 'unknown',
|
|
172
|
-
timestamp: tick.timestamp || Date.now()
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
stats.latency = Date.now() - latencyStart;
|
|
176
|
-
if (tickCount % 100 === 0) ui.addLog('info', `#${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
marketFeed.on('connected', () => { stats.connected = true; ui.addLog('success', 'Connected!'); });
|
|
180
|
-
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
181
|
-
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Disconnected'); });
|
|
182
|
-
|
|
183
|
-
// Connect to market data
|
|
184
|
-
try {
|
|
185
|
-
const token = leadService.token || leadService.getToken?.();
|
|
186
|
-
const pk = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
187
|
-
await marketFeed.connect(token, pk, contractId);
|
|
188
|
-
await marketFeed.subscribe(symbolName, contractId);
|
|
189
|
-
} catch (e) {
|
|
190
|
-
ui.addLog('error', `Connect failed: ${e.message}`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Poll P&L
|
|
194
|
-
const pollPnL = async () => {
|
|
195
|
-
try {
|
|
196
|
-
const res = await leadService.getTradingAccounts();
|
|
197
|
-
if (res.success && res.accounts) {
|
|
198
|
-
const acc = res.accounts.find(a => a.accountId === leadAccount.accountId);
|
|
199
|
-
if (acc?.profitAndLoss !== undefined) {
|
|
200
|
-
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
201
|
-
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (stats.pnl >= dailyTarget) {
|
|
205
|
-
stopReason = 'target';
|
|
206
|
-
running = false;
|
|
207
|
-
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
208
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
209
|
-
stopReason = 'risk';
|
|
210
|
-
running = false;
|
|
211
|
-
ui.addLog('error', `RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
212
|
-
}
|
|
213
|
-
} catch (e) { /* silent */ }
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// Start intervals
|
|
217
|
-
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
218
|
-
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
219
|
-
pollPnL();
|
|
220
|
-
|
|
221
|
-
// Keyboard handler
|
|
222
|
-
const cleanupKeys = setupKeyHandler(() => { running = false; stopReason = 'manual'; });
|
|
223
|
-
|
|
224
|
-
// Wait for stop
|
|
225
|
-
await new Promise(resolve => {
|
|
226
|
-
const check = setInterval(() => {
|
|
227
|
-
if (!running) { clearInterval(check); resolve(); }
|
|
228
|
-
}, 100);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Cleanup
|
|
232
|
-
clearInterval(refreshInterval);
|
|
233
|
-
clearInterval(pnlInterval);
|
|
234
|
-
await marketFeed.disconnect();
|
|
235
|
-
if (cleanupKeys) cleanupKeys();
|
|
236
|
-
ui.cleanup();
|
|
237
|
-
|
|
238
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
239
|
-
process.stdin.resume();
|
|
240
|
-
|
|
241
|
-
// Duration
|
|
242
|
-
const durationMs = Date.now() - stats.startTime;
|
|
243
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
244
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
245
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
246
|
-
stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
247
|
-
|
|
248
|
-
renderSessionSummary(stats, stopReason);
|
|
249
|
-
|
|
250
|
-
console.log('\n Returning to menu in 3 seconds...');
|
|
251
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Place orders on all follower accounts
|
|
256
|
-
*/
|
|
257
|
-
const placeFollowerOrders = async (followers, contractId, orderSide, direction, ui) => {
|
|
258
|
-
for (let i = 0; i < followers.length; i++) {
|
|
259
|
-
const f = followers[i];
|
|
260
|
-
const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
const fResult = await fService.placeOrder({
|
|
264
|
-
accountId: f.account.accountId,
|
|
265
|
-
contractId,
|
|
266
|
-
type: 2,
|
|
267
|
-
side: orderSide,
|
|
268
|
-
size: f.contracts
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
if (fResult.success) {
|
|
272
|
-
ui.addLog('trade', `F${i + 1}: ${direction.toUpperCase()} ${f.contracts}x`);
|
|
273
|
-
} else {
|
|
274
|
-
ui.addLog('error', `F${i + 1}: Failed`);
|
|
275
|
-
}
|
|
276
|
-
} catch (e) {
|
|
277
|
-
ui.addLog('error', `F${i + 1}: ${e.message}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Place bracket orders (stop loss and take profit)
|
|
284
|
-
*/
|
|
285
|
-
const placeBracketOrders = async (service, account, contractId, direction, size, stopLoss, takeProfit) => {
|
|
286
|
-
const exitSide = direction === 'long' ? 1 : 0;
|
|
287
|
-
|
|
288
|
-
await service.placeOrder({
|
|
289
|
-
accountId: account.accountId,
|
|
290
|
-
contractId,
|
|
291
|
-
type: 4,
|
|
292
|
-
side: exitSide,
|
|
293
|
-
size,
|
|
294
|
-
stopPrice: stopLoss
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
await service.placeOrder({
|
|
298
|
-
accountId: account.accountId,
|
|
299
|
-
contractId,
|
|
300
|
-
type: 1,
|
|
301
|
-
side: exitSide,
|
|
302
|
-
size,
|
|
303
|
-
limitPrice: takeProfit
|
|
304
|
-
});
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Setup keyboard handler for stopping
|
|
309
|
-
*/
|
|
310
|
-
const setupKeyHandler = (onStop) => {
|
|
311
|
-
if (!process.stdin.isTTY) return null;
|
|
312
|
-
|
|
313
|
-
readline.emitKeypressEvents(process.stdin);
|
|
314
|
-
process.stdin.setRawMode(true);
|
|
315
|
-
process.stdin.resume();
|
|
316
|
-
|
|
317
|
-
const onKey = (str, key) => {
|
|
318
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
319
|
-
onStop();
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
process.stdin.on('keypress', onKey);
|
|
324
|
-
|
|
325
|
-
return () => {
|
|
326
|
-
process.stdin.removeListener('keypress', onKey);
|
|
327
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
328
|
-
};
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
module.exports = { launchCopyTrading };
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Strategy - AI-powered strategy builder
|
|
3
|
-
* Config flow + AI chat to create strategy, then execute with AI supervision
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const chalk = require('chalk');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const ora = require('ora');
|
|
11
|
-
|
|
12
|
-
const { getLogoWidth, centerText, displayBanner , clearScreen } = require('../../ui');
|
|
13
|
-
const { prompts } = require('../../utils');
|
|
14
|
-
const { connections } = require('../../services');
|
|
15
|
-
const { getActiveProvider, getActiveAgents } = require('../ai-agents');
|
|
16
|
-
const cliproxy = require('../../services/cliproxy');
|
|
17
|
-
const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
|
|
18
|
-
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
19
|
-
const { executeAlgo } = require('./algo-executor');
|
|
20
|
-
|
|
21
|
-
const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
|
|
22
|
-
|
|
23
|
-
const ensureStrategiesDir = () => {
|
|
24
|
-
if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** Custom Strategy Menu */
|
|
28
|
-
const customStrategyMenu = async (service) => {
|
|
29
|
-
const aiProvider = getActiveProvider();
|
|
30
|
-
if (!aiProvider) {
|
|
31
|
-
console.log(chalk.red('\n No AI Agent connected. Go to AI Agents menu first.'));
|
|
32
|
-
await prompts.waitForEnter();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const market = checkMarketHours();
|
|
37
|
-
if (!market.isOpen && !market.message.includes('early')) {
|
|
38
|
-
console.log(chalk.red(`\n ${market.message}`));
|
|
39
|
-
console.log(chalk.gray(' Custom strategy requires market to be open\n'));
|
|
40
|
-
await prompts.waitForEnter();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
45
|
-
const allAccounts = await connections.getAllAccounts();
|
|
46
|
-
|
|
47
|
-
if (!allAccounts?.length) { spinner.fail('No accounts found'); await prompts.waitForEnter(); return; }
|
|
48
|
-
|
|
49
|
-
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
50
|
-
if (!activeAccounts.length) { spinner.fail('No active accounts'); await prompts.waitForEnter(); return; }
|
|
51
|
-
|
|
52
|
-
spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
|
|
53
|
-
|
|
54
|
-
// Step 1: Select account
|
|
55
|
-
console.log(chalk.cyan.bold('\n STEP 1: SELECT ACCOUNT'));
|
|
56
|
-
const accountOptions = activeAccounts.map(acc => {
|
|
57
|
-
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
58
|
-
const balance = acc.balance !== null && acc.balance !== undefined ? ` - $${acc.balance.toLocaleString()}` : '';
|
|
59
|
-
return { label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`, value: acc };
|
|
60
|
-
});
|
|
61
|
-
accountOptions.push({ label: '< Back', value: 'back' });
|
|
62
|
-
|
|
63
|
-
const selectedAccount = await prompts.selectOption('Select Account:', accountOptions);
|
|
64
|
-
if (!selectedAccount || selectedAccount === 'back') return;
|
|
65
|
-
|
|
66
|
-
const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
|
|
67
|
-
|
|
68
|
-
// Step 2: Select symbol
|
|
69
|
-
console.log(chalk.cyan.bold('\n STEP 2: SELECT SYMBOL'));
|
|
70
|
-
const contract = await selectSymbol(accountService);
|
|
71
|
-
if (!contract) return;
|
|
72
|
-
|
|
73
|
-
// Step 3: Configure parameters
|
|
74
|
-
console.log(chalk.cyan.bold('\n STEP 3: CONFIGURE PARAMETERS\n'));
|
|
75
|
-
|
|
76
|
-
const contracts = await prompts.numberInput('Number of contracts:', 1, 1, 10);
|
|
77
|
-
if (contracts === null) return;
|
|
78
|
-
|
|
79
|
-
const dailyTarget = await prompts.numberInput('Daily target ($):', 200, 1, 10000);
|
|
80
|
-
if (dailyTarget === null) return;
|
|
81
|
-
|
|
82
|
-
const maxRisk = await prompts.numberInput('Max risk ($):', 100, 1, 5000);
|
|
83
|
-
if (maxRisk === null) return;
|
|
84
|
-
|
|
85
|
-
const showName = await prompts.confirmPrompt('Show account name?', false);
|
|
86
|
-
if (showName === null) return;
|
|
87
|
-
|
|
88
|
-
// Step 4: AI Supervision with Pre-flight Check
|
|
89
|
-
console.log(chalk.cyan.bold('\n STEP 4: AI SUPERVISION'));
|
|
90
|
-
const aiSupervision = await prompts.confirmPrompt('Enable AI supervision during execution?', true);
|
|
91
|
-
if (aiSupervision === null) return;
|
|
92
|
-
|
|
93
|
-
if (aiSupervision) {
|
|
94
|
-
// Run pre-flight check - agent must pass
|
|
95
|
-
console.log();
|
|
96
|
-
console.log(chalk.yellow(' Running AI pre-flight check...'));
|
|
97
|
-
console.log();
|
|
98
|
-
|
|
99
|
-
const agents = getActiveAgents();
|
|
100
|
-
const preflightResults = await runPreflightCheck(agents);
|
|
101
|
-
|
|
102
|
-
const lines = formatPreflightResults(preflightResults, 60);
|
|
103
|
-
for (const line of lines) console.log(line);
|
|
104
|
-
|
|
105
|
-
const summary = getPreflightSummary(preflightResults);
|
|
106
|
-
console.log();
|
|
107
|
-
console.log(` ${summary.text}`);
|
|
108
|
-
console.log();
|
|
109
|
-
|
|
110
|
-
if (!preflightResults.success) {
|
|
111
|
-
console.log(chalk.red(' Cannot start - fix agent connection first.'));
|
|
112
|
-
await prompts.waitForEnter();
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const config = { account: selectedAccount, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider };
|
|
118
|
-
|
|
119
|
-
// Step 5: AI Chat
|
|
120
|
-
console.log(chalk.cyan.bold('\n STEP 5: CREATE YOUR STRATEGY WITH AI'));
|
|
121
|
-
console.log(chalk.gray(' Describe your trading strategy. AI will help build it.\n'));
|
|
122
|
-
|
|
123
|
-
await strategyChat(config, accountService);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
/** Select symbol */
|
|
127
|
-
const selectSymbol = async (service) => {
|
|
128
|
-
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
129
|
-
|
|
130
|
-
const result = await service.getContracts();
|
|
131
|
-
if (!result.success || !result.contracts?.length) { spinner.fail('Failed to load contracts'); return null; }
|
|
132
|
-
|
|
133
|
-
const popular = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
134
|
-
result.contracts.sort((a, b) => {
|
|
135
|
-
const baseA = a.baseSymbol || a.symbol || '', baseB = b.baseSymbol || b.symbol || '';
|
|
136
|
-
const idxA = popular.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
137
|
-
const idxB = popular.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
138
|
-
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
139
|
-
if (idxA !== -1) return -1;
|
|
140
|
-
if (idxB !== -1) return 1;
|
|
141
|
-
return baseA.localeCompare(baseB);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
spinner.succeed(`Found ${result.contracts.length} contracts`);
|
|
145
|
-
|
|
146
|
-
const options = result.contracts.map(c => ({ label: `${c.symbol} - ${c.name} (${c.exchange})`, value: c }));
|
|
147
|
-
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
148
|
-
|
|
149
|
-
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
150
|
-
return selected === 'back' || selected === null ? null : selected;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
/** AI Chat for strategy creation */
|
|
154
|
-
const strategyChat = async (config, service) => {
|
|
155
|
-
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
156
|
-
const accountName = showName ? (account.accountName || account.rithmicAccountId || account.accountId) : 'HQX *****';
|
|
157
|
-
|
|
158
|
-
clearScreen();
|
|
159
|
-
displayBanner();
|
|
160
|
-
|
|
161
|
-
const W = getLogoWidth() - 2;
|
|
162
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
163
|
-
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY - AI CHAT', W)) + chalk.cyan('║'));
|
|
164
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
165
|
-
console.log(chalk.cyan('║') + centerText(`Account: ${accountName} | Symbol: ${contract.name} | Qty: ${contracts}`, W) + chalk.cyan('║'));
|
|
166
|
-
console.log(chalk.cyan('║') + centerText(`Target: $${dailyTarget} | Risk: $${maxRisk} | AI: ${aiSupervision ? 'ON' : 'OFF'}`, W) + chalk.cyan('║'));
|
|
167
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText(`Provider: ${aiProvider.name}`, W)) + chalk.cyan('║'));
|
|
168
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
169
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText('"run" to execute, "save" to save, "cancel" to abort', W)) + chalk.cyan('║'));
|
|
170
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝\n'));
|
|
171
|
-
|
|
172
|
-
const systemPrompt = `You are an expert algorithmic trading assistant.
|
|
173
|
-
Setup: ${accountName} | ${contract.name} | ${contracts} contracts | Target: $${dailyTarget} | Risk: $${maxRisk}
|
|
174
|
-
Help create a trading strategy. Be concise (2-3 sentences). When ready, say [STRATEGY_READY] with JSON config.`;
|
|
175
|
-
|
|
176
|
-
const messages = [{ role: 'system', content: systemPrompt }];
|
|
177
|
-
let strategyReady = false, strategyConfig = null;
|
|
178
|
-
|
|
179
|
-
console.log(chalk.green(' AI: ') + `I'll help you create a custom strategy for ${contract.name}. What kind of strategy?`);
|
|
180
|
-
console.log();
|
|
181
|
-
|
|
182
|
-
while (true) {
|
|
183
|
-
const userInput = await prompts.textInput(chalk.yellow(' You: '));
|
|
184
|
-
if (!userInput) continue;
|
|
185
|
-
|
|
186
|
-
const cmd = userInput.toLowerCase().trim();
|
|
187
|
-
|
|
188
|
-
if (cmd === 'cancel' || cmd === 'exit' || cmd === 'quit') {
|
|
189
|
-
console.log(chalk.gray('\n Cancelled.')); await prompts.waitForEnter(); return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (cmd === 'save') {
|
|
193
|
-
if (strategyConfig) await saveStrategy(strategyConfig, config);
|
|
194
|
-
else console.log(chalk.yellow('\n No strategy to save yet.'));
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (cmd === 'run') {
|
|
199
|
-
if (strategyReady && strategyConfig) {
|
|
200
|
-
console.log(chalk.green('\n Launching strategy...'));
|
|
201
|
-
await launchCustomStrategy(config, strategyConfig, service);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
console.log(chalk.yellow('\n Strategy not ready. Describe your entry/exit conditions first.'));
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
messages.push({ role: 'user', content: userInput });
|
|
209
|
-
const spinner = ora({ text: 'AI thinking...', color: 'yellow' }).start();
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
213
|
-
const result = await cliproxy.chatCompletion(modelId, messages);
|
|
214
|
-
|
|
215
|
-
if (!result.success) { spinner.fail(`AI Error: ${result.error}`); messages.pop(); continue; }
|
|
216
|
-
|
|
217
|
-
const response = result.response?.choices?.[0]?.message?.content || '';
|
|
218
|
-
messages.push({ role: 'assistant', content: response });
|
|
219
|
-
spinner.stop();
|
|
220
|
-
|
|
221
|
-
console.log('\n' + chalk.green(' AI: ') + formatResponse(response) + '\n');
|
|
222
|
-
|
|
223
|
-
if (response.includes('[STRATEGY_READY]') || response.toLowerCase().includes('strategy is ready')) {
|
|
224
|
-
strategyReady = true;
|
|
225
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
226
|
-
if (jsonMatch) try { strategyConfig = JSON.parse(jsonMatch[0]); } catch (e) {}
|
|
227
|
-
if (!strategyConfig) strategyConfig = { description: userInput, messages: messages.slice(1) };
|
|
228
|
-
console.log(chalk.cyan(' [Strategy ready! "run" to execute or "save" to save]\n'));
|
|
229
|
-
}
|
|
230
|
-
} catch (e) { spinner.fail(`Error: ${e.message}`); messages.pop(); }
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const getDefaultModel = (id) => ({ anthropic: 'claude-sonnet-4-20250514', google: 'gemini-2.5-pro', openai: 'gpt-4o' }[id] || 'claude-sonnet-4-20250514');
|
|
235
|
-
|
|
236
|
-
const formatResponse = (text) => {
|
|
237
|
-
const lines = text.replace(/\[STRATEGY_READY\]/g, '').trim().split('\n');
|
|
238
|
-
return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
/** Save strategy */
|
|
242
|
-
const saveStrategy = async (strategyConfig, config) => {
|
|
243
|
-
ensureStrategiesDir();
|
|
244
|
-
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
245
|
-
if (!name?.trim()) { console.log(chalk.gray(' Save cancelled.')); return; }
|
|
246
|
-
|
|
247
|
-
const folderName = name.trim().toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
248
|
-
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
249
|
-
if (!fs.existsSync(strategyPath)) fs.mkdirSync(strategyPath, { recursive: true });
|
|
250
|
-
|
|
251
|
-
const configFile = {
|
|
252
|
-
name: name.trim(), symbol: config.contract.name, contracts: config.contracts,
|
|
253
|
-
dailyTarget: config.dailyTarget, maxRisk: config.maxRisk, aiSupervision: config.aiSupervision,
|
|
254
|
-
strategy: strategyConfig, createdAt: new Date().toISOString()
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
fs.writeFileSync(path.join(strategyPath, 'config.json'), JSON.stringify(configFile, null, 2));
|
|
258
|
-
console.log(chalk.green(`\n ✓ Saved: ${strategyPath}`));
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
/** Launch custom strategy with AI supervision */
|
|
262
|
-
const launchCustomStrategy = async (config, strategyConfig, service) => {
|
|
263
|
-
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
264
|
-
|
|
265
|
-
// AI supervision function
|
|
266
|
-
const askAI = async (aiContext, signal, ctx) => {
|
|
267
|
-
if (!aiSupervision) return { approve: true };
|
|
268
|
-
|
|
269
|
-
const prompt = `Trading supervisor check:
|
|
270
|
-
Symbol: ${ctx.symbolName} | Position: ${ctx.currentPosition === 0 ? 'FLAT' : (ctx.currentPosition > 0 ? 'LONG' : 'SHORT')}
|
|
271
|
-
P&L: $${ctx.stats.pnl.toFixed(2)} | Trades: ${ctx.stats.trades} (W:${ctx.stats.wins} L:${ctx.stats.losses})
|
|
272
|
-
Strategy: ${JSON.stringify(strategyConfig.description || strategyConfig).substring(0, 200)}
|
|
273
|
-
Signal: ${signal.direction.toUpperCase()} @ ${signal.entry.toFixed(2)} (${(signal.confidence * 100).toFixed(0)}%)
|
|
274
|
-
Recent prices: ${aiContext.recentTicks.slice(-5).map(t => t.price?.toFixed(2)).join(', ') || 'N/A'}
|
|
275
|
-
Reply JSON: {"approve": true/false, "reason": "brief"}`;
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
279
|
-
const result = await cliproxy.chatCompletion(modelId, [
|
|
280
|
-
{ role: 'system', content: 'Trading supervisor. JSON only.' },
|
|
281
|
-
{ role: 'user', content: prompt }
|
|
282
|
-
]);
|
|
283
|
-
|
|
284
|
-
if (result.success) {
|
|
285
|
-
const content = result.response?.choices?.[0]?.message?.content || '';
|
|
286
|
-
const match = content.match(/\{[\s\S]*\}/);
|
|
287
|
-
if (match) return JSON.parse(match[0]);
|
|
288
|
-
}
|
|
289
|
-
} catch (e) { /* fallback */ }
|
|
290
|
-
return { approve: true, reason: 'AI unavailable' };
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
await executeAlgo({
|
|
294
|
-
service, account, contract,
|
|
295
|
-
config: { contracts, dailyTarget, maxRisk, showName },
|
|
296
|
-
options: { aiSupervision, aiProvider, askAI, subtitle: 'CUSTOM STRATEGY + AI' }
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
/** Load saved strategies */
|
|
301
|
-
const loadStrategies = () => {
|
|
302
|
-
ensureStrategiesDir();
|
|
303
|
-
try {
|
|
304
|
-
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
305
|
-
return items.filter(i => i.isDirectory()).map(dir => {
|
|
306
|
-
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
307
|
-
if (fs.existsSync(configPath)) return { folder: dir.name, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
308
|
-
return { folder: dir.name, name: dir.name };
|
|
309
|
-
});
|
|
310
|
-
} catch (e) { return []; }
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
module.exports = { customStrategyMenu, loadStrategies };
|