hedgequantx 2.7.88 → 2.7.90
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/pages/algo/algo-executor.js +265 -0
- package/src/pages/algo/custom-strategy.js +211 -317
- package/src/pages/algo/index.js +41 -16
- package/src/pages/algo/one-account.js +7 -289
package/package.json
CHANGED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algo Executor - Shared execution engine for all algo modes
|
|
3
|
+
* Handles market data, signals, orders, and P&L tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
8
|
+
const { M1 } = require('../../lib/m/s1');
|
|
9
|
+
const { MarketDataFeed } = require('../../lib/data');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute algo strategy with market data
|
|
13
|
+
* @param {Object} params - Execution parameters
|
|
14
|
+
* @param {Object} params.service - Trading service (Rithmic/ProjectX)
|
|
15
|
+
* @param {Object} params.account - Account object
|
|
16
|
+
* @param {Object} params.contract - Contract object
|
|
17
|
+
* @param {Object} params.config - Algo config (contracts, target, risk, showName)
|
|
18
|
+
* @param {Object} params.options - Optional: aiSupervision, aiProvider, askAI function
|
|
19
|
+
*/
|
|
20
|
+
const executeAlgo = async ({ service, account, contract, config, options = {} }) => {
|
|
21
|
+
const { contracts, dailyTarget, maxRisk, showName } = config;
|
|
22
|
+
const { aiSupervision, aiProvider, askAI, subtitle } = options;
|
|
23
|
+
|
|
24
|
+
const accountName = showName
|
|
25
|
+
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
26
|
+
: 'HQX *****';
|
|
27
|
+
const symbolName = contract.name;
|
|
28
|
+
const contractId = contract.id;
|
|
29
|
+
const tickSize = contract.tickSize || 0.25;
|
|
30
|
+
|
|
31
|
+
const ui = new AlgoUI({
|
|
32
|
+
subtitle: subtitle || (aiSupervision ? 'CUSTOM STRATEGY + AI' : 'HQX Ultra Scalping'),
|
|
33
|
+
mode: 'one-account'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const stats = {
|
|
37
|
+
accountName,
|
|
38
|
+
symbol: symbolName,
|
|
39
|
+
qty: contracts,
|
|
40
|
+
target: dailyTarget,
|
|
41
|
+
risk: maxRisk,
|
|
42
|
+
propfirm: account.propfirm || 'Unknown',
|
|
43
|
+
platform: account.platform || 'Rithmic',
|
|
44
|
+
pnl: 0,
|
|
45
|
+
trades: 0,
|
|
46
|
+
wins: 0,
|
|
47
|
+
losses: 0,
|
|
48
|
+
latency: 0,
|
|
49
|
+
connected: false,
|
|
50
|
+
startTime: Date.now()
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let running = true;
|
|
54
|
+
let stopReason = null;
|
|
55
|
+
let startingPnL = null;
|
|
56
|
+
let currentPosition = 0;
|
|
57
|
+
let pendingOrder = false;
|
|
58
|
+
let tickCount = 0;
|
|
59
|
+
|
|
60
|
+
// AI context for supervision
|
|
61
|
+
const aiContext = { recentTicks: [], recentSignals: [], maxTicks: 100 };
|
|
62
|
+
|
|
63
|
+
// Initialize Strategy
|
|
64
|
+
const strategy = new M1({ tickSize });
|
|
65
|
+
strategy.initialize(contractId, tickSize);
|
|
66
|
+
|
|
67
|
+
// Initialize Market Data Feed
|
|
68
|
+
const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
|
|
69
|
+
|
|
70
|
+
// Log startup
|
|
71
|
+
ui.addLog('info', `Strategy: ${aiSupervision ? 'CUSTOM + AI' : 'HQX Ultra Scalping'}`);
|
|
72
|
+
ui.addLog('info', `Account: ${accountName}`);
|
|
73
|
+
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
|
|
74
|
+
ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
75
|
+
if (aiSupervision && aiProvider) ui.addLog('info', `AI: ${aiProvider.name} supervision`);
|
|
76
|
+
ui.addLog('info', 'Connecting to market data...');
|
|
77
|
+
|
|
78
|
+
// Handle strategy signals
|
|
79
|
+
strategy.on('signal', async (signal) => {
|
|
80
|
+
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
81
|
+
|
|
82
|
+
const { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
83
|
+
|
|
84
|
+
aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
|
|
85
|
+
if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
|
|
86
|
+
|
|
87
|
+
ui.addLog('info', `Signal: ${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
88
|
+
|
|
89
|
+
// AI Supervision check
|
|
90
|
+
if (aiSupervision && askAI) {
|
|
91
|
+
const aiDecision = await askAI(aiContext, signal, { symbolName, currentPosition, stats, dailyTarget, maxRisk });
|
|
92
|
+
if (!aiDecision.approve) {
|
|
93
|
+
ui.addLog('info', `AI rejected: ${aiDecision.reason || 'No reason'}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
ui.addLog('info', `AI approved: ${aiDecision.reason || 'OK'}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Place order
|
|
100
|
+
pendingOrder = true;
|
|
101
|
+
try {
|
|
102
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
103
|
+
const orderResult = await service.placeOrder({
|
|
104
|
+
accountId: account.accountId,
|
|
105
|
+
contractId: contractId,
|
|
106
|
+
type: 2,
|
|
107
|
+
side: orderSide,
|
|
108
|
+
size: contracts
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (orderResult.success) {
|
|
112
|
+
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
113
|
+
stats.trades++;
|
|
114
|
+
ui.addLog('fill_' + (direction === 'long' ? 'buy' : 'sell'),
|
|
115
|
+
`OPENED ${direction.toUpperCase()} ${contracts}x @ market`);
|
|
116
|
+
|
|
117
|
+
// Bracket orders
|
|
118
|
+
if (stopLoss && takeProfit) {
|
|
119
|
+
await service.placeOrder({
|
|
120
|
+
accountId: account.accountId, contractId, type: 4,
|
|
121
|
+
side: direction === 'long' ? 1 : 0, size: contracts, stopPrice: stopLoss
|
|
122
|
+
});
|
|
123
|
+
await service.placeOrder({
|
|
124
|
+
accountId: account.accountId, contractId, type: 1,
|
|
125
|
+
side: direction === 'long' ? 1 : 0, size: contracts, limitPrice: takeProfit
|
|
126
|
+
});
|
|
127
|
+
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
ui.addLog('error', `Order failed: ${orderResult.error}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
ui.addLog('error', `Order error: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
pendingOrder = false;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle market data ticks
|
|
139
|
+
marketFeed.on('tick', (tick) => {
|
|
140
|
+
tickCount++;
|
|
141
|
+
const latencyStart = Date.now();
|
|
142
|
+
|
|
143
|
+
aiContext.recentTicks.push(tick);
|
|
144
|
+
if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
|
|
145
|
+
|
|
146
|
+
strategy.processTick({
|
|
147
|
+
contractId: tick.contractId || contractId,
|
|
148
|
+
price: tick.price, bid: tick.bid, ask: tick.ask,
|
|
149
|
+
volume: tick.volume || 1, side: tick.lastTradeSide || 'unknown',
|
|
150
|
+
timestamp: tick.timestamp || Date.now()
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
stats.latency = Date.now() - latencyStart;
|
|
154
|
+
if (tickCount % 100 === 0) ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
marketFeed.on('connected', () => { stats.connected = true; ui.addLog('connected', 'Market data connected!'); });
|
|
158
|
+
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
159
|
+
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
|
|
160
|
+
|
|
161
|
+
// Connect to market data
|
|
162
|
+
try {
|
|
163
|
+
const token = service.token || service.getToken?.();
|
|
164
|
+
const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
165
|
+
await marketFeed.connect(token, propfirmKey, contractId);
|
|
166
|
+
await marketFeed.subscribe(symbolName, contractId);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Poll P&L
|
|
172
|
+
const pollPnL = async () => {
|
|
173
|
+
try {
|
|
174
|
+
const accountResult = await service.getTradingAccounts();
|
|
175
|
+
if (accountResult.success && accountResult.accounts) {
|
|
176
|
+
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
177
|
+
if (acc && acc.profitAndLoss !== undefined) {
|
|
178
|
+
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
179
|
+
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
180
|
+
if (stats.pnl !== 0) strategy.recordTradeResult(stats.pnl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const posResult = await service.getPositions(account.accountId);
|
|
185
|
+
if (posResult.success && posResult.positions) {
|
|
186
|
+
const pos = posResult.positions.find(p => {
|
|
187
|
+
const sym = p.contractId || p.symbol || '';
|
|
188
|
+
return sym.includes(contract.name) || sym.includes(contractId);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (pos && pos.quantity !== 0) {
|
|
192
|
+
currentPosition = pos.quantity;
|
|
193
|
+
const pnl = pos.profitAndLoss || 0;
|
|
194
|
+
if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
|
|
195
|
+
else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
|
|
196
|
+
} else {
|
|
197
|
+
currentPosition = 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (stats.pnl >= dailyTarget) {
|
|
202
|
+
stopReason = 'target'; running = false;
|
|
203
|
+
ui.addLog('fill_win', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
204
|
+
} else if (stats.pnl <= -maxRisk) {
|
|
205
|
+
stopReason = 'risk'; running = false;
|
|
206
|
+
ui.addLog('fill_loss', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (e) { /* silent */ }
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Start loops
|
|
212
|
+
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
213
|
+
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
214
|
+
pollPnL();
|
|
215
|
+
|
|
216
|
+
// Keyboard handler
|
|
217
|
+
const setupKeyHandler = () => {
|
|
218
|
+
if (!process.stdin.isTTY) return;
|
|
219
|
+
readline.emitKeypressEvents(process.stdin);
|
|
220
|
+
process.stdin.setRawMode(true);
|
|
221
|
+
process.stdin.resume();
|
|
222
|
+
|
|
223
|
+
const onKey = (str, key) => {
|
|
224
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
225
|
+
running = false; stopReason = 'manual';
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
process.stdin.on('keypress', onKey);
|
|
229
|
+
return () => {
|
|
230
|
+
process.stdin.removeListener('keypress', onKey);
|
|
231
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const cleanupKeys = setupKeyHandler();
|
|
236
|
+
|
|
237
|
+
// Wait for stop
|
|
238
|
+
await new Promise(resolve => {
|
|
239
|
+
const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Cleanup
|
|
243
|
+
clearInterval(refreshInterval);
|
|
244
|
+
clearInterval(pnlInterval);
|
|
245
|
+
await marketFeed.disconnect();
|
|
246
|
+
if (cleanupKeys) cleanupKeys();
|
|
247
|
+
ui.cleanup();
|
|
248
|
+
|
|
249
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
250
|
+
process.stdin.resume();
|
|
251
|
+
|
|
252
|
+
// Duration
|
|
253
|
+
const durationMs = Date.now() - stats.startTime;
|
|
254
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
255
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
256
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
257
|
+
stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
258
|
+
|
|
259
|
+
renderSessionSummary(stats, stopReason);
|
|
260
|
+
|
|
261
|
+
console.log('\n Returning to menu in 3 seconds...');
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
module.exports = { executeAlgo };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom Strategy - AI-powered
|
|
3
|
-
*
|
|
2
|
+
* Custom Strategy - AI-powered strategy builder
|
|
3
|
+
* Config flow + AI chat to create strategy, then execute with AI supervision
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const chalk = require('chalk');
|
|
@@ -11,384 +11,278 @@ const ora = require('ora');
|
|
|
11
11
|
|
|
12
12
|
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
13
13
|
const { prompts } = require('../../utils');
|
|
14
|
+
const { connections } = require('../../services');
|
|
14
15
|
const { getActiveProvider } = require('../ai-agents');
|
|
15
16
|
const cliproxy = require('../../services/cliproxy');
|
|
17
|
+
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
18
|
+
const { executeAlgo } = require('./algo-executor');
|
|
16
19
|
|
|
17
|
-
// Base strategies directory
|
|
18
20
|
const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
|
|
19
21
|
|
|
20
|
-
/** Ensure strategies directory exists */
|
|
21
22
|
const ensureStrategiesDir = () => {
|
|
22
23
|
if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
/** Load all saved strategies (folders) */
|
|
26
|
-
const loadStrategies = () => {
|
|
27
|
-
ensureStrategiesDir();
|
|
28
|
-
try {
|
|
29
|
-
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
30
|
-
return items.filter(i => i.isDirectory()).map(dir => {
|
|
31
|
-
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
32
|
-
if (fs.existsSync(configPath)) {
|
|
33
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
34
|
-
return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), ...config };
|
|
35
|
-
}
|
|
36
|
-
return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), name: dir.name };
|
|
37
|
-
});
|
|
38
|
-
} catch (e) { return []; }
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/** Create strategy folder structure */
|
|
42
|
-
const createStrategyFolder = (name) => {
|
|
43
|
-
ensureStrategiesDir();
|
|
44
|
-
const folderName = name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
|
|
45
|
-
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
46
|
-
|
|
47
|
-
if (fs.existsSync(strategyPath)) {
|
|
48
|
-
return { success: false, error: 'Strategy folder already exists', path: null };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
fs.mkdirSync(strategyPath, { recursive: true });
|
|
52
|
-
return { success: true, path: strategyPath, folder: folderName };
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** Save strategy module */
|
|
56
|
-
const saveModule = (strategyPath, moduleName, content) => {
|
|
57
|
-
const filePath = path.join(strategyPath, moduleName);
|
|
58
|
-
fs.writeFileSync(filePath, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
59
|
-
return filePath;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Delete strategy folder */
|
|
63
|
-
const deleteStrategy = (strategyPath) => {
|
|
64
|
-
if (fs.existsSync(strategyPath)) {
|
|
65
|
-
fs.rmSync(strategyPath, { recursive: true, force: true });
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
26
|
/** Custom Strategy Menu */
|
|
72
27
|
const customStrategyMenu = async (service) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const boxWidth = getLogoWidth();
|
|
78
|
-
const W = boxWidth - 2;
|
|
79
|
-
const aiProvider = getActiveProvider();
|
|
80
|
-
const strategies = loadStrategies();
|
|
81
|
-
|
|
82
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
83
|
-
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY BUILDER', W)) + chalk.cyan('║'));
|
|
84
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
85
|
-
|
|
86
|
-
// AI Status
|
|
87
|
-
if (aiProvider) {
|
|
88
|
-
const status = `AI: ${aiProvider.name} (${aiProvider.modelName || 'default'})`;
|
|
89
|
-
console.log(chalk.cyan('║') + chalk.green(centerText('● ' + status, W)) + chalk.cyan('║'));
|
|
90
|
-
} else {
|
|
91
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('○ NO AI AGENT CONNECTED', W)) + chalk.cyan('║'));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
95
|
-
|
|
96
|
-
// Options
|
|
97
|
-
const col1 = '[1] CREATE NEW';
|
|
98
|
-
const col2 = `[2] MY STRATEGIES (${strategies.length})`;
|
|
99
|
-
const colWidth = Math.floor(W / 2);
|
|
100
|
-
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
101
|
-
const pad2 = Math.floor((W - colWidth - col2.length) / 2);
|
|
102
|
-
console.log(chalk.cyan('║') +
|
|
103
|
-
' '.repeat(pad1) + chalk.yellow(col1) + ' '.repeat(colWidth - col1.length - pad1) +
|
|
104
|
-
' '.repeat(pad2) + chalk.cyan(col2) + ' '.repeat(W - colWidth - col2.length - pad2) +
|
|
105
|
-
chalk.cyan('║'));
|
|
106
|
-
|
|
107
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
108
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
109
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
110
|
-
|
|
111
|
-
const input = await prompts.textInput(chalk.cyan('SELECT (1/2/B): '));
|
|
112
|
-
const choice = (input || '').toLowerCase().trim();
|
|
113
|
-
|
|
114
|
-
if (choice === 'b' || choice === '') return;
|
|
115
|
-
|
|
116
|
-
if (choice === '1') {
|
|
117
|
-
if (!aiProvider) {
|
|
118
|
-
console.log(chalk.red('\n Connect an AI Agent first (AI Agents menu)'));
|
|
119
|
-
await prompts.waitForEnter();
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
await createStrategyWizard(aiProvider);
|
|
123
|
-
} else if (choice === '2') {
|
|
124
|
-
await myStrategiesMenu(strategies, service);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** AI Wizard to create modular strategy */
|
|
130
|
-
const createStrategyWizard = async (aiProvider) => {
|
|
131
|
-
console.clear();
|
|
132
|
-
displayBanner();
|
|
133
|
-
|
|
134
|
-
const boxWidth = getLogoWidth();
|
|
135
|
-
const W = boxWidth - 2;
|
|
136
|
-
|
|
137
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
138
|
-
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CREATE STRATEGY WITH AI', W)) + chalk.cyan('║'));
|
|
139
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
140
|
-
console.log();
|
|
141
|
-
|
|
142
|
-
// Step 1: Strategy name
|
|
143
|
-
console.log(chalk.yellow(' STEP 1: Name your strategy'));
|
|
144
|
-
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
145
|
-
if (!name || !name.trim()) {
|
|
146
|
-
console.log(chalk.red(' Cancelled'));
|
|
28
|
+
const aiProvider = getActiveProvider();
|
|
29
|
+
if (!aiProvider) {
|
|
30
|
+
console.log(chalk.red('\n No AI Agent connected. Go to AI Agents menu first.'));
|
|
147
31
|
await prompts.waitForEnter();
|
|
148
32
|
return;
|
|
149
33
|
}
|
|
150
34
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log(chalk.
|
|
35
|
+
const market = checkMarketHours();
|
|
36
|
+
if (!market.isOpen && !market.message.includes('early')) {
|
|
37
|
+
console.log(chalk.red(`\n ${market.message}`));
|
|
38
|
+
console.log(chalk.gray(' Custom strategy requires market to be open\n'));
|
|
155
39
|
await prompts.waitForEnter();
|
|
156
40
|
return;
|
|
157
41
|
}
|
|
158
42
|
|
|
159
|
-
|
|
160
|
-
|
|
43
|
+
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
44
|
+
const allAccounts = await connections.getAllAccounts();
|
|
161
45
|
|
|
162
|
-
|
|
163
|
-
console.log(chalk.yellow(' STEP 2: Describe your strategy to the AI'));
|
|
164
|
-
console.log(chalk.gray(' Type your strategy idea in plain English.'));
|
|
165
|
-
console.log(chalk.gray(' The AI will help you build each module.'));
|
|
166
|
-
console.log(chalk.gray(' Type "done" when finished, "cancel" to abort.'));
|
|
167
|
-
console.log();
|
|
46
|
+
if (!allAccounts?.length) { spinner.fail('No accounts found'); await prompts.waitForEnter(); return; }
|
|
168
47
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
48
|
+
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
49
|
+
if (!activeAccounts.length) { spinner.fail('No active accounts'); await prompts.waitForEnter(); return; }
|
|
50
|
+
|
|
51
|
+
spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
|
|
52
|
+
|
|
53
|
+
// Step 1: Select account
|
|
54
|
+
console.log(chalk.cyan.bold('\n STEP 1: SELECT ACCOUNT'));
|
|
55
|
+
const accountOptions = activeAccounts.map(acc => {
|
|
56
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
57
|
+
const balance = acc.balance !== null && acc.balance !== undefined ? ` - $${acc.balance.toLocaleString()}` : '';
|
|
58
|
+
return { label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`, value: acc };
|
|
59
|
+
});
|
|
60
|
+
accountOptions.push({ label: '< Back', value: 'back' });
|
|
61
|
+
|
|
62
|
+
const selectedAccount = await prompts.selectOption('Select Account:', accountOptions);
|
|
63
|
+
if (!selectedAccount || selectedAccount === 'back') return;
|
|
64
|
+
|
|
65
|
+
const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
|
|
66
|
+
|
|
67
|
+
// Step 2: Select symbol
|
|
68
|
+
console.log(chalk.cyan.bold('\n STEP 2: SELECT SYMBOL'));
|
|
69
|
+
const contract = await selectSymbol(accountService);
|
|
70
|
+
if (!contract) return;
|
|
71
|
+
|
|
72
|
+
// Step 3: Configure parameters
|
|
73
|
+
console.log(chalk.cyan.bold('\n STEP 3: CONFIGURE PARAMETERS\n'));
|
|
74
|
+
|
|
75
|
+
const contracts = await prompts.numberInput('Number of contracts:', 1, 1, 10);
|
|
76
|
+
if (contracts === null) return;
|
|
77
|
+
|
|
78
|
+
const dailyTarget = await prompts.numberInput('Daily target ($):', 200, 1, 10000);
|
|
79
|
+
if (dailyTarget === null) return;
|
|
80
|
+
|
|
81
|
+
const maxRisk = await prompts.numberInput('Max risk ($):', 100, 1, 5000);
|
|
82
|
+
if (maxRisk === null) return;
|
|
83
|
+
|
|
84
|
+
const showName = await prompts.confirmPrompt('Show account name?', false);
|
|
85
|
+
if (showName === null) return;
|
|
86
|
+
|
|
87
|
+
// Step 4: AI Supervision
|
|
88
|
+
console.log(chalk.cyan.bold('\n STEP 4: AI SUPERVISION'));
|
|
89
|
+
const aiSupervision = await prompts.confirmPrompt('Enable AI supervision during execution?', true);
|
|
90
|
+
if (aiSupervision === null) return;
|
|
91
|
+
|
|
92
|
+
const config = { account: selectedAccount, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider };
|
|
93
|
+
|
|
94
|
+
// Step 5: AI Chat
|
|
95
|
+
console.log(chalk.cyan.bold('\n STEP 5: CREATE YOUR STRATEGY WITH AI'));
|
|
96
|
+
console.log(chalk.gray(' Describe your trading strategy. AI will help build it.\n'));
|
|
97
|
+
|
|
98
|
+
await strategyChat(config, accountService);
|
|
99
|
+
};
|
|
178
100
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
101
|
+
/** Select symbol */
|
|
102
|
+
const selectSymbol = async (service) => {
|
|
103
|
+
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
104
|
+
|
|
105
|
+
const result = await service.getContracts();
|
|
106
|
+
if (!result.success || !result.contracts?.length) { spinner.fail('Failed to load contracts'); return null; }
|
|
107
|
+
|
|
108
|
+
const popular = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
109
|
+
result.contracts.sort((a, b) => {
|
|
110
|
+
const baseA = a.baseSymbol || a.symbol || '', baseB = b.baseSymbol || b.symbol || '';
|
|
111
|
+
const idxA = popular.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
112
|
+
const idxB = popular.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
113
|
+
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
114
|
+
if (idxA !== -1) return -1;
|
|
115
|
+
if (idxB !== -1) return 1;
|
|
116
|
+
return baseA.localeCompare(baseB);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
spinner.succeed(`Found ${result.contracts.length} contracts`);
|
|
120
|
+
|
|
121
|
+
const options = result.contracts.map(c => ({ label: `${c.symbol} - ${c.name} (${c.exchange})`, value: c }));
|
|
122
|
+
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
123
|
+
|
|
124
|
+
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
125
|
+
return selected === 'back' || selected === null ? null : selected;
|
|
184
126
|
};
|
|
185
|
-
\`\`\`
|
|
186
127
|
|
|
187
|
-
|
|
128
|
+
/** AI Chat for strategy creation */
|
|
129
|
+
const strategyChat = async (config, service) => {
|
|
130
|
+
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
131
|
+
const accountName = showName ? (account.accountName || account.rithmicAccountId || account.accountId) : 'HQX *****';
|
|
132
|
+
|
|
133
|
+
console.clear();
|
|
134
|
+
displayBanner();
|
|
135
|
+
|
|
136
|
+
const W = getLogoWidth() - 2;
|
|
137
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
138
|
+
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY - AI CHAT', W)) + chalk.cyan('║'));
|
|
139
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
140
|
+
console.log(chalk.cyan('║') + centerText(`Account: ${accountName} | Symbol: ${contract.name} | Qty: ${contracts}`, W) + chalk.cyan('║'));
|
|
141
|
+
console.log(chalk.cyan('║') + centerText(`Target: $${dailyTarget} | Risk: $${maxRisk} | AI: ${aiSupervision ? 'ON' : 'OFF'}`, W) + chalk.cyan('║'));
|
|
142
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText(`Provider: ${aiProvider.name}`, W)) + chalk.cyan('║'));
|
|
143
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
144
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('"run" to execute, "save" to save, "cancel" to abort', W)) + chalk.cyan('║'));
|
|
145
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝\n'));
|
|
146
|
+
|
|
147
|
+
const systemPrompt = `You are an expert algorithmic trading assistant.
|
|
148
|
+
Setup: ${accountName} | ${contract.name} | ${contracts} contracts | Target: $${dailyTarget} | Risk: $${maxRisk}
|
|
149
|
+
Help create a trading strategy. Be concise (2-3 sentences). When ready, say [STRATEGY_READY] with JSON config.`;
|
|
188
150
|
|
|
189
151
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
190
|
-
|
|
152
|
+
let strategyReady = false, strategyConfig = null;
|
|
191
153
|
|
|
192
|
-
console.log(chalk.green(' AI: ') + '
|
|
193
|
-
console.log(chalk.gray(' Example: "A mean reversion strategy that buys when RSI < 30"'));
|
|
154
|
+
console.log(chalk.green(' AI: ') + `I'll help you create a custom strategy for ${contract.name}. What kind of strategy?`);
|
|
194
155
|
console.log();
|
|
195
156
|
|
|
196
157
|
while (true) {
|
|
197
158
|
const userInput = await prompts.textInput(chalk.yellow(' You: '));
|
|
198
|
-
|
|
199
159
|
if (!userInput) continue;
|
|
200
160
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await prompts.waitForEnter();
|
|
205
|
-
return;
|
|
161
|
+
const cmd = userInput.toLowerCase().trim();
|
|
162
|
+
|
|
163
|
+
if (cmd === 'cancel' || cmd === 'exit' || cmd === 'quit') {
|
|
164
|
+
console.log(chalk.gray('\n Cancelled.')); await prompts.waitForEnter(); return;
|
|
206
165
|
}
|
|
207
166
|
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
console.log(chalk.gray(' Modules created:'));
|
|
220
|
-
for (const m of Object.keys(modules)) {
|
|
221
|
-
if (m !== 'description') console.log(chalk.gray(` - ${m}`));
|
|
167
|
+
if (cmd === 'save') {
|
|
168
|
+
if (strategyConfig) await saveStrategy(strategyConfig, config);
|
|
169
|
+
else console.log(chalk.yellow('\n No strategy to save yet.'));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (cmd === 'run') {
|
|
174
|
+
if (strategyReady && strategyConfig) {
|
|
175
|
+
console.log(chalk.green('\n Launching strategy...'));
|
|
176
|
+
await launchCustomStrategy(config, strategyConfig, service);
|
|
177
|
+
return;
|
|
222
178
|
}
|
|
223
|
-
|
|
224
|
-
|
|
179
|
+
console.log(chalk.yellow('\n Strategy not ready. Describe your entry/exit conditions first.'));
|
|
180
|
+
continue;
|
|
225
181
|
}
|
|
226
182
|
|
|
227
183
|
messages.push({ role: 'user', content: userInput });
|
|
228
|
-
|
|
229
184
|
const spinner = ora({ text: 'AI thinking...', color: 'yellow' }).start();
|
|
230
185
|
|
|
231
186
|
try {
|
|
232
187
|
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
233
188
|
const result = await cliproxy.chatCompletion(modelId, messages);
|
|
234
189
|
|
|
235
|
-
if (!result.success) {
|
|
236
|
-
spinner.fail(`AI Error: ${result.error}`);
|
|
237
|
-
messages.pop();
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
190
|
+
if (!result.success) { spinner.fail(`AI Error: ${result.error}`); messages.pop(); continue; }
|
|
240
191
|
|
|
241
192
|
const response = result.response?.choices?.[0]?.message?.content || '';
|
|
242
193
|
messages.push({ role: 'assistant', content: response });
|
|
243
|
-
|
|
244
194
|
spinner.stop();
|
|
245
|
-
console.log();
|
|
246
|
-
|
|
247
|
-
// Extract and save code modules
|
|
248
|
-
const codeBlocks = response.matchAll(/```javascript:(\w+\.js)\n([\s\S]*?)```/g);
|
|
249
|
-
for (const match of codeBlocks) {
|
|
250
|
-
const [, filename, code] = match;
|
|
251
|
-
saveModule(folder.path, filename, code.trim());
|
|
252
|
-
modules[filename] = true;
|
|
253
|
-
console.log(chalk.green(` ✓ Saved module: ${filename}`));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Extract description if present
|
|
257
|
-
const descMatch = response.match(/description[:\s]*["']?([^"'\n]+)/i);
|
|
258
|
-
if (descMatch) modules.description = descMatch[1];
|
|
259
195
|
|
|
260
|
-
|
|
261
|
-
const cleanResponse = response.replace(/```[\s\S]*?```/g, '[code saved]');
|
|
262
|
-
console.log(chalk.green(' AI: ') + formatResponse(cleanResponse));
|
|
263
|
-
console.log();
|
|
196
|
+
console.log('\n' + chalk.green(' AI: ') + formatResponse(response) + '\n');
|
|
264
197
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
198
|
+
if (response.includes('[STRATEGY_READY]') || response.toLowerCase().includes('strategy is ready')) {
|
|
199
|
+
strategyReady = true;
|
|
200
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
201
|
+
if (jsonMatch) try { strategyConfig = JSON.parse(jsonMatch[0]); } catch (e) {}
|
|
202
|
+
if (!strategyConfig) strategyConfig = { description: userInput, messages: messages.slice(1) };
|
|
203
|
+
console.log(chalk.cyan(' [Strategy ready! "run" to execute or "save" to save]\n'));
|
|
204
|
+
}
|
|
205
|
+
} catch (e) { spinner.fail(`Error: ${e.message}`); messages.pop(); }
|
|
269
206
|
}
|
|
270
207
|
};
|
|
271
208
|
|
|
272
|
-
|
|
273
|
-
const getDefaultModel = (providerId) => {
|
|
274
|
-
const defaults = {
|
|
275
|
-
anthropic: 'claude-sonnet-4-20250514',
|
|
276
|
-
google: 'gemini-2.5-pro',
|
|
277
|
-
openai: 'gpt-4o'
|
|
278
|
-
};
|
|
279
|
-
return defaults[providerId] || 'claude-sonnet-4-20250514';
|
|
280
|
-
};
|
|
209
|
+
const getDefaultModel = (id) => ({ anthropic: 'claude-sonnet-4-20250514', google: 'gemini-2.5-pro', openai: 'gpt-4o' }[id] || 'claude-sonnet-4-20250514');
|
|
281
210
|
|
|
282
|
-
/** Format response for terminal */
|
|
283
211
|
const formatResponse = (text) => {
|
|
284
|
-
const lines = text.split('\n');
|
|
212
|
+
const lines = text.replace(/\[STRATEGY_READY\]/g, '').trim().split('\n');
|
|
285
213
|
return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
|
|
286
214
|
};
|
|
287
215
|
|
|
288
|
-
/**
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const boxWidth = getLogoWidth();
|
|
295
|
-
const W = boxWidth - 2;
|
|
296
|
-
|
|
297
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
298
|
-
console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('MY STRATEGIES', W)) + chalk.cyan('║'));
|
|
299
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
300
|
-
|
|
301
|
-
if (strategies.length === 0) {
|
|
302
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText('No strategies yet', W)) + chalk.cyan('║'));
|
|
303
|
-
} else {
|
|
304
|
-
for (let i = 0; i < strategies.length; i++) {
|
|
305
|
-
const s = strategies[i];
|
|
306
|
-
const num = `[${i + 1}]`.padEnd(4);
|
|
307
|
-
const sname = (s.name || s.folder).substring(0, 30).padEnd(32);
|
|
308
|
-
const modules = s.modules ? `${s.modules.length} modules` : '';
|
|
309
|
-
const line = `${num} ${sname} ${chalk.gray(modules)}`;
|
|
310
|
-
console.log(chalk.cyan('║') + ' ' + chalk.white(num) + chalk.cyan(sname) + chalk.gray(modules.padEnd(W - 38)) + chalk.cyan('║'));
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
315
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
316
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
317
|
-
|
|
318
|
-
if (strategies.length === 0) {
|
|
319
|
-
await prompts.waitForEnter();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const input = await prompts.textInput(chalk.cyan(`SELECT (1-${strategies.length}/B): `));
|
|
324
|
-
const choice = (input || '').toLowerCase().trim();
|
|
325
|
-
|
|
326
|
-
if (choice === 'b' || choice === '') return;
|
|
327
|
-
|
|
328
|
-
const num = parseInt(choice);
|
|
329
|
-
if (!isNaN(num) && num >= 1 && num <= strategies.length) {
|
|
330
|
-
await strategyDetailMenu(strategies[num - 1], service);
|
|
331
|
-
strategies.length = 0;
|
|
332
|
-
strategies.push(...loadStrategies());
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/** Strategy Detail Menu */
|
|
338
|
-
const strategyDetailMenu = async (strategy, service) => {
|
|
339
|
-
console.clear();
|
|
340
|
-
displayBanner();
|
|
341
|
-
|
|
342
|
-
const boxWidth = getLogoWidth();
|
|
343
|
-
const W = boxWidth - 2;
|
|
344
|
-
|
|
345
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
346
|
-
console.log(chalk.cyan('║') + chalk.green.bold(centerText((strategy.name || strategy.folder).toUpperCase(), W)) + chalk.cyan('║'));
|
|
347
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
348
|
-
|
|
349
|
-
// Show modules
|
|
350
|
-
const files = fs.readdirSync(strategy.path);
|
|
351
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText(`Path: ${strategy.path}`, W)) + chalk.cyan('║'));
|
|
352
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
353
|
-
console.log(chalk.cyan('║') + chalk.white(centerText('MODULES:', W)) + chalk.cyan('║'));
|
|
354
|
-
|
|
355
|
-
for (const f of files) {
|
|
356
|
-
const icon = f.endsWith('.js') ? '📄' : f.endsWith('.json') ? '⚙️' : '📁';
|
|
357
|
-
console.log(chalk.cyan('║') + centerText(`${icon} ${f}`, W) + chalk.cyan('║'));
|
|
358
|
-
}
|
|
216
|
+
/** Save strategy */
|
|
217
|
+
const saveStrategy = async (strategyConfig, config) => {
|
|
218
|
+
ensureStrategiesDir();
|
|
219
|
+
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
220
|
+
if (!name?.trim()) { console.log(chalk.gray(' Save cancelled.')); return; }
|
|
359
221
|
|
|
360
|
-
|
|
222
|
+
const folderName = name.trim().toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
223
|
+
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
224
|
+
if (!fs.existsSync(strategyPath)) fs.mkdirSync(strategyPath, { recursive: true });
|
|
361
225
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
) + chalk.cyan('║'));
|
|
226
|
+
const configFile = {
|
|
227
|
+
name: name.trim(), symbol: config.contract.name, contracts: config.contracts,
|
|
228
|
+
dailyTarget: config.dailyTarget, maxRisk: config.maxRisk, aiSupervision: config.aiSupervision,
|
|
229
|
+
strategy: strategyConfig, createdAt: new Date().toISOString()
|
|
230
|
+
};
|
|
368
231
|
|
|
369
|
-
|
|
370
|
-
console.log(chalk.
|
|
371
|
-
|
|
232
|
+
fs.writeFileSync(path.join(strategyPath, 'config.json'), JSON.stringify(configFile, null, 2));
|
|
233
|
+
console.log(chalk.green(`\n ✓ Saved: ${strategyPath}`));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Launch custom strategy with AI supervision */
|
|
237
|
+
const launchCustomStrategy = async (config, strategyConfig, service) => {
|
|
238
|
+
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
372
239
|
|
|
373
|
-
|
|
374
|
-
const
|
|
240
|
+
// AI supervision function
|
|
241
|
+
const askAI = async (aiContext, signal, ctx) => {
|
|
242
|
+
if (!aiSupervision) return { approve: true };
|
|
243
|
+
|
|
244
|
+
const prompt = `Trading supervisor check:
|
|
245
|
+
Symbol: ${ctx.symbolName} | Position: ${ctx.currentPosition === 0 ? 'FLAT' : (ctx.currentPosition > 0 ? 'LONG' : 'SHORT')}
|
|
246
|
+
P&L: $${ctx.stats.pnl.toFixed(2)} | Trades: ${ctx.stats.trades} (W:${ctx.stats.wins} L:${ctx.stats.losses})
|
|
247
|
+
Strategy: ${JSON.stringify(strategyConfig.description || strategyConfig).substring(0, 200)}
|
|
248
|
+
Signal: ${signal.direction.toUpperCase()} @ ${signal.entry.toFixed(2)} (${(signal.confidence * 100).toFixed(0)}%)
|
|
249
|
+
Recent prices: ${aiContext.recentTicks.slice(-5).map(t => t.price?.toFixed(2)).join(', ') || 'N/A'}
|
|
250
|
+
Reply JSON: {"approve": true/false, "reason": "brief"}`;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
254
|
+
const result = await cliproxy.chatCompletion(modelId, [
|
|
255
|
+
{ role: 'system', content: 'Trading supervisor. JSON only.' },
|
|
256
|
+
{ role: 'user', content: prompt }
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
if (result.success) {
|
|
260
|
+
const content = result.response?.choices?.[0]?.message?.content || '';
|
|
261
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
262
|
+
if (match) return JSON.parse(match[0]);
|
|
263
|
+
}
|
|
264
|
+
} catch (e) { /* fallback */ }
|
|
265
|
+
return { approve: true, reason: 'AI unavailable' };
|
|
266
|
+
};
|
|
375
267
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
268
|
+
await executeAlgo({
|
|
269
|
+
service, account, contract,
|
|
270
|
+
config: { contracts, dailyTarget, maxRisk, showName },
|
|
271
|
+
options: { aiSupervision, aiProvider, askAI, subtitle: 'CUSTOM STRATEGY + AI' }
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** Load saved strategies */
|
|
276
|
+
const loadStrategies = () => {
|
|
277
|
+
ensureStrategiesDir();
|
|
278
|
+
try {
|
|
279
|
+
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
280
|
+
return items.filter(i => i.isDirectory()).map(dir => {
|
|
281
|
+
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
282
|
+
if (fs.existsSync(configPath)) return { folder: dir.name, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
283
|
+
return { folder: dir.name, name: dir.name };
|
|
284
|
+
});
|
|
285
|
+
} catch (e) { return []; }
|
|
392
286
|
};
|
|
393
287
|
|
|
394
|
-
module.exports = { customStrategyMenu, loadStrategies
|
|
288
|
+
module.exports = { customStrategyMenu, loadStrategies };
|
package/src/pages/algo/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
7
7
|
const { logger, prompts } = require('../../utils');
|
|
8
|
+
const { getActiveProvider } = require('../ai-agents');
|
|
8
9
|
|
|
9
10
|
const log = logger.scope('AlgoMenu');
|
|
10
11
|
|
|
@@ -26,33 +27,52 @@ const algoTradingMenu = async (service) => {
|
|
|
26
27
|
const boxWidth = getLogoWidth();
|
|
27
28
|
const W = boxWidth - 2;
|
|
28
29
|
|
|
30
|
+
// Check if AI agent is connected
|
|
31
|
+
const aiProvider = getActiveProvider();
|
|
32
|
+
const hasAI = !!aiProvider;
|
|
33
|
+
|
|
29
34
|
// Draw menu rectangle
|
|
30
35
|
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
31
36
|
console.log(chalk.cyan('║') + chalk.magenta.bold(centerText('ALGO-TRADING', W)) + chalk.cyan('║'));
|
|
32
37
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
33
38
|
|
|
34
|
-
// 3 columns layout
|
|
39
|
+
// 2 or 3 columns layout based on AI availability
|
|
35
40
|
const col1 = '[1] ONE ACCOUNT';
|
|
36
41
|
const col2 = '[2] COPY TRADING';
|
|
37
|
-
const col3 = '[3] CUSTOM STRATEGY';
|
|
38
|
-
const colWidth = Math.floor(W / 3);
|
|
39
|
-
const lastColWidth = W - 2 * colWidth;
|
|
40
|
-
|
|
41
|
-
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
42
|
-
const pad2 = Math.floor((colWidth - col2.length) / 2);
|
|
43
|
-
const pad3 = Math.floor((lastColWidth - col3.length) / 2);
|
|
42
|
+
const col3 = hasAI ? '[3] CUSTOM STRATEGY' : '';
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if (hasAI) {
|
|
45
|
+
// 3 columns
|
|
46
|
+
const colWidth = Math.floor(W / 3);
|
|
47
|
+
const lastColWidth = W - 2 * colWidth;
|
|
48
|
+
|
|
49
|
+
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
50
|
+
const pad2 = Math.floor((colWidth - col2.length) / 2);
|
|
51
|
+
const pad3 = Math.floor((lastColWidth - col3.length) / 2);
|
|
52
|
+
|
|
53
|
+
const col1Str = ' '.repeat(pad1) + chalk.cyan(col1) + ' '.repeat(colWidth - col1.length - pad1);
|
|
54
|
+
const col2Str = ' '.repeat(pad2) + chalk.yellow(col2) + ' '.repeat(colWidth - col2.length - pad2);
|
|
55
|
+
const col3Str = ' '.repeat(pad3) + chalk.green(col3) + ' '.repeat(lastColWidth - col3.length - pad3);
|
|
56
|
+
|
|
57
|
+
console.log(chalk.cyan('║') + col1Str + col2Str + col3Str + chalk.cyan('║'));
|
|
58
|
+
} else {
|
|
59
|
+
// 2 columns only (no AI connected)
|
|
60
|
+
const colWidth = Math.floor(W / 2);
|
|
61
|
+
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
62
|
+
const pad2 = Math.floor((W - colWidth - col2.length) / 2);
|
|
63
|
+
|
|
64
|
+
const col1Str = ' '.repeat(pad1) + chalk.cyan(col1) + ' '.repeat(colWidth - col1.length - pad1);
|
|
65
|
+
const col2Str = ' '.repeat(pad2) + chalk.yellow(col2) + ' '.repeat(W - colWidth - col2.length - pad2);
|
|
66
|
+
|
|
67
|
+
console.log(chalk.cyan('║') + col1Str + col2Str + chalk.cyan('║'));
|
|
68
|
+
}
|
|
50
69
|
|
|
51
70
|
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
52
71
|
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
53
72
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
54
73
|
|
|
55
|
-
const
|
|
74
|
+
const promptText = hasAI ? 'SELECT (1/2/3/B): ' : 'SELECT (1/2/B): ';
|
|
75
|
+
const input = await prompts.textInput(chalk.cyan(promptText));
|
|
56
76
|
const choice = (input || '').toLowerCase().trim();
|
|
57
77
|
|
|
58
78
|
log.debug('Algo mode selected', { choice });
|
|
@@ -71,8 +91,13 @@ const algoTradingMenu = async (service) => {
|
|
|
71
91
|
await copyTradingMenu();
|
|
72
92
|
break;
|
|
73
93
|
case '3':
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
if (hasAI) {
|
|
95
|
+
log.info('Starting Custom Strategy mode');
|
|
96
|
+
await customStrategyMenu(service);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.red(' INVALID OPTION'));
|
|
99
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
100
|
+
}
|
|
76
101
|
break;
|
|
77
102
|
default:
|
|
78
103
|
console.log(chalk.red(' INVALID OPTION'));
|
|
@@ -4,16 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const ora = require('ora');
|
|
7
|
-
const readline = require('readline');
|
|
8
7
|
|
|
9
8
|
const { connections } = require('../../services');
|
|
10
|
-
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
11
9
|
const { prompts } = require('../../utils');
|
|
12
10
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
13
|
-
|
|
14
|
-
// Strategy & Market Data
|
|
15
|
-
const { M1 } = require('../../lib/m/s1');
|
|
16
|
-
const { MarketDataFeed } = require('../../lib/data');
|
|
11
|
+
const { executeAlgo } = require('./algo-executor');
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
|
|
@@ -80,7 +75,12 @@ const oneAccountMenu = async (service) => {
|
|
|
80
75
|
const config = await configureAlgo(selectedAccount, contract);
|
|
81
76
|
if (!config) return;
|
|
82
77
|
|
|
83
|
-
await
|
|
78
|
+
await executeAlgo({
|
|
79
|
+
service: accountService,
|
|
80
|
+
account: selectedAccount,
|
|
81
|
+
contract,
|
|
82
|
+
config
|
|
83
|
+
});
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -158,286 +158,4 @@ const configureAlgo = async (account, contract) => {
|
|
|
158
158
|
return { contracts, dailyTarget, maxRisk, showName };
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
* Launch algo trading - HQX Ultra Scalping Strategy
|
|
163
|
-
* Real-time market data + Strategy signals + Auto order execution
|
|
164
|
-
*/
|
|
165
|
-
const launchAlgo = async (service, account, contract, config) => {
|
|
166
|
-
const { contracts, dailyTarget, maxRisk, showName } = config;
|
|
167
|
-
|
|
168
|
-
// Use RAW API fields
|
|
169
|
-
const accountName = showName
|
|
170
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
171
|
-
: 'HQX *****';
|
|
172
|
-
const symbolName = contract.name;
|
|
173
|
-
const contractId = contract.id;
|
|
174
|
-
const connectionType = account.platform || 'Rithmic';
|
|
175
|
-
const tickSize = contract.tickSize || 0.25;
|
|
176
|
-
|
|
177
|
-
const ui = new AlgoUI({ subtitle: 'HQX Ultra Scalping', mode: 'one-account' });
|
|
178
|
-
|
|
179
|
-
const stats = {
|
|
180
|
-
accountName,
|
|
181
|
-
symbol: symbolName,
|
|
182
|
-
qty: contracts,
|
|
183
|
-
target: dailyTarget,
|
|
184
|
-
risk: maxRisk,
|
|
185
|
-
propfirm: account.propfirm || 'Unknown',
|
|
186
|
-
platform: connectionType,
|
|
187
|
-
pnl: 0,
|
|
188
|
-
trades: 0,
|
|
189
|
-
wins: 0,
|
|
190
|
-
losses: 0,
|
|
191
|
-
latency: 0,
|
|
192
|
-
connected: false,
|
|
193
|
-
startTime: Date.now()
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
let running = true;
|
|
197
|
-
let stopReason = null;
|
|
198
|
-
let startingPnL = null;
|
|
199
|
-
let currentPosition = 0; // Current position qty (+ long, - short)
|
|
200
|
-
let pendingOrder = false; // Prevent duplicate orders
|
|
201
|
-
let tickCount = 0;
|
|
202
|
-
|
|
203
|
-
// Initialize Strategy
|
|
204
|
-
const strategy = new M1({ tickSize });
|
|
205
|
-
strategy.initialize(contractId, tickSize);
|
|
206
|
-
|
|
207
|
-
// Initialize Market Data Feed
|
|
208
|
-
const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
|
|
209
|
-
|
|
210
|
-
// Log startup
|
|
211
|
-
ui.addLog('info', `Connection: ${connectionType}`);
|
|
212
|
-
ui.addLog('info', `Account: ${accountName}`);
|
|
213
|
-
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
|
|
214
|
-
ui.addLog('info', `Target: $${dailyTarget} | Max Risk: $${maxRisk}`);
|
|
215
|
-
ui.addLog('info', 'Connecting to market data...');
|
|
216
|
-
|
|
217
|
-
// Handle strategy signals
|
|
218
|
-
strategy.on('signal', async (signal) => {
|
|
219
|
-
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
220
|
-
|
|
221
|
-
const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
222
|
-
|
|
223
|
-
ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
224
|
-
|
|
225
|
-
// Place order via API
|
|
226
|
-
pendingOrder = true;
|
|
227
|
-
try {
|
|
228
|
-
const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
|
|
229
|
-
const orderResult = await service.placeOrder({
|
|
230
|
-
accountId: account.accountId,
|
|
231
|
-
contractId: contractId,
|
|
232
|
-
type: 2, // Market order
|
|
233
|
-
side: orderSide,
|
|
234
|
-
size: contracts
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
if (orderResult.success) {
|
|
238
|
-
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
239
|
-
stats.trades++;
|
|
240
|
-
ui.addLog('trade', `OPENED ${direction.toUpperCase()} ${contracts}x @ market`);
|
|
241
|
-
|
|
242
|
-
// Place bracket orders (SL/TP)
|
|
243
|
-
if (stopLoss && takeProfit) {
|
|
244
|
-
// Stop Loss
|
|
245
|
-
await service.placeOrder({
|
|
246
|
-
accountId: account.accountId,
|
|
247
|
-
contractId: contractId,
|
|
248
|
-
type: 4, // Stop order
|
|
249
|
-
side: direction === 'long' ? 1 : 0, // Opposite side
|
|
250
|
-
size: contracts,
|
|
251
|
-
stopPrice: stopLoss
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Take Profit
|
|
255
|
-
await service.placeOrder({
|
|
256
|
-
accountId: account.accountId,
|
|
257
|
-
contractId: contractId,
|
|
258
|
-
type: 1, // Limit order
|
|
259
|
-
side: direction === 'long' ? 1 : 0,
|
|
260
|
-
size: contracts,
|
|
261
|
-
limitPrice: takeProfit
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
ui.addLog('error', `Order failed: ${orderResult.error}`);
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
ui.addLog('error', `Order error: ${e.message}`);
|
|
271
|
-
}
|
|
272
|
-
pendingOrder = false;
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Handle market data ticks
|
|
276
|
-
marketFeed.on('tick', (tick) => {
|
|
277
|
-
tickCount++;
|
|
278
|
-
const latencyStart = Date.now();
|
|
279
|
-
|
|
280
|
-
// Feed tick to strategy
|
|
281
|
-
strategy.processTick({
|
|
282
|
-
contractId: tick.contractId || contractId,
|
|
283
|
-
price: tick.price,
|
|
284
|
-
bid: tick.bid,
|
|
285
|
-
ask: tick.ask,
|
|
286
|
-
volume: tick.volume || 1,
|
|
287
|
-
side: tick.lastTradeSide || 'unknown',
|
|
288
|
-
timestamp: tick.timestamp || Date.now()
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
stats.latency = Date.now() - latencyStart;
|
|
292
|
-
|
|
293
|
-
// Log every 100th tick to show activity
|
|
294
|
-
if (tickCount % 100 === 0) {
|
|
295
|
-
ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
marketFeed.on('connected', () => {
|
|
300
|
-
stats.connected = true;
|
|
301
|
-
ui.addLog('success', 'Market data connected!');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
marketFeed.on('error', (err) => {
|
|
305
|
-
ui.addLog('error', `Market: ${err.message}`);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
marketFeed.on('disconnected', () => {
|
|
309
|
-
stats.connected = false;
|
|
310
|
-
ui.addLog('error', 'Market data disconnected');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Connect to market data
|
|
314
|
-
try {
|
|
315
|
-
const token = service.token || service.getToken?.();
|
|
316
|
-
const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
317
|
-
await marketFeed.connect(token, propfirmKey, contractId);
|
|
318
|
-
await marketFeed.subscribe(symbolName, contractId);
|
|
319
|
-
} catch (e) {
|
|
320
|
-
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Poll account P&L from API
|
|
324
|
-
const pollPnL = async () => {
|
|
325
|
-
try {
|
|
326
|
-
const accountResult = await service.getTradingAccounts();
|
|
327
|
-
if (accountResult.success && accountResult.accounts) {
|
|
328
|
-
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
329
|
-
if (acc && acc.profitAndLoss !== undefined) {
|
|
330
|
-
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
331
|
-
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
332
|
-
|
|
333
|
-
// Record trade result in strategy
|
|
334
|
-
if (stats.pnl !== 0) {
|
|
335
|
-
strategy.recordTradeResult(stats.pnl);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Check positions
|
|
341
|
-
const posResult = await service.getPositions(account.accountId);
|
|
342
|
-
if (posResult.success && posResult.positions) {
|
|
343
|
-
const pos = posResult.positions.find(p => {
|
|
344
|
-
const sym = p.contractId || p.symbol || '';
|
|
345
|
-
return sym.includes(contract.name) || sym.includes(contractId);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (pos && pos.quantity !== 0) {
|
|
349
|
-
currentPosition = pos.quantity;
|
|
350
|
-
const side = pos.quantity > 0 ? 'LONG' : 'SHORT';
|
|
351
|
-
const pnl = pos.profitAndLoss || 0;
|
|
352
|
-
|
|
353
|
-
// Check if position closed (win/loss)
|
|
354
|
-
if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
|
|
355
|
-
else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
|
|
356
|
-
} else {
|
|
357
|
-
currentPosition = 0;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Check target/risk
|
|
362
|
-
if (stats.pnl >= dailyTarget) {
|
|
363
|
-
stopReason = 'target';
|
|
364
|
-
running = false;
|
|
365
|
-
ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
366
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
367
|
-
stopReason = 'risk';
|
|
368
|
-
running = false;
|
|
369
|
-
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
370
|
-
}
|
|
371
|
-
} catch (e) {
|
|
372
|
-
// Silently handle polling errors
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Start polling and UI refresh
|
|
377
|
-
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
378
|
-
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
379
|
-
pollPnL(); // Initial poll
|
|
380
|
-
|
|
381
|
-
// Keyboard handler
|
|
382
|
-
const setupKeyHandler = () => {
|
|
383
|
-
if (!process.stdin.isTTY) return;
|
|
384
|
-
readline.emitKeypressEvents(process.stdin);
|
|
385
|
-
process.stdin.setRawMode(true);
|
|
386
|
-
process.stdin.resume();
|
|
387
|
-
|
|
388
|
-
const onKey = (str, key) => {
|
|
389
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
390
|
-
running = false;
|
|
391
|
-
stopReason = 'manual';
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
process.stdin.on('keypress', onKey);
|
|
395
|
-
return () => {
|
|
396
|
-
process.stdin.removeListener('keypress', onKey);
|
|
397
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
398
|
-
};
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const cleanupKeys = setupKeyHandler();
|
|
402
|
-
|
|
403
|
-
// Wait for stop
|
|
404
|
-
await new Promise(resolve => {
|
|
405
|
-
const check = setInterval(() => {
|
|
406
|
-
if (!running) {
|
|
407
|
-
clearInterval(check);
|
|
408
|
-
resolve();
|
|
409
|
-
}
|
|
410
|
-
}, 100);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Cleanup
|
|
414
|
-
clearInterval(refreshInterval);
|
|
415
|
-
clearInterval(pnlInterval);
|
|
416
|
-
await marketFeed.disconnect();
|
|
417
|
-
if (cleanupKeys) cleanupKeys();
|
|
418
|
-
ui.cleanup();
|
|
419
|
-
|
|
420
|
-
if (process.stdin.isTTY) {
|
|
421
|
-
process.stdin.setRawMode(false);
|
|
422
|
-
}
|
|
423
|
-
process.stdin.resume();
|
|
424
|
-
|
|
425
|
-
// Duration
|
|
426
|
-
const durationMs = Date.now() - stats.startTime;
|
|
427
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
428
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
429
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
430
|
-
stats.duration = hours > 0
|
|
431
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
432
|
-
: minutes > 0
|
|
433
|
-
? `${minutes}m ${seconds}s`
|
|
434
|
-
: `${seconds}s`;
|
|
435
|
-
|
|
436
|
-
// Summary
|
|
437
|
-
renderSessionSummary(stats, stopReason);
|
|
438
|
-
|
|
439
|
-
console.log('\n Returning to menu in 3 seconds...');
|
|
440
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
441
|
-
};
|
|
442
|
-
|
|
443
161
|
module.exports = { oneAccountMenu };
|