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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.7.88",
3
+ "version": "2.7.90",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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 modular strategy builder
3
- * Each strategy is a folder with modular components
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
- while (true) {
74
- console.clear();
75
- displayBanner();
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
- // Create folder
152
- const folder = createStrategyFolder(name.trim());
153
- if (!folder.success) {
154
- console.log(chalk.red(` Error: ${folder.error}`));
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
- console.log(chalk.green(` ✓ Created: ${folder.path}`));
160
- console.log();
43
+ const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
44
+ const allAccounts = await connections.getAllAccounts();
161
45
 
162
- // Step 2: Chat with AI to build strategy
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 systemPrompt = `You are an expert algo trading strategy builder for futures (ES, NQ, MES, MNQ, etc).
170
- Help the user create a modular trading strategy. Build these components:
171
-
172
- 1. ENTRY CONDITIONS - When to open a position (long/short signals)
173
- 2. EXIT CONDITIONS - Take profit, stop loss, trailing stops
174
- 3. RISK MANAGEMENT - Position sizing, max loss, max positions
175
- 4. FILTERS - Market conditions when NOT to trade
176
-
177
- Ask clarifying questions. Be concise. When ready, output each module.
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
- For each module, output JavaScript code in this format:
180
- \`\`\`javascript:entry.js
181
- module.exports = {
182
- checkLongEntry: (data) => { /* return true/false */ },
183
- checkShortEntry: (data) => { /* return true/false */ }
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
- The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, macd, vwap, high, low, open, close }`;
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
- const modules = {};
152
+ let strategyReady = false, strategyConfig = null;
191
153
 
192
- console.log(chalk.green(' AI: ') + 'What kind of trading strategy do you want to create?');
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
- if (userInput.toLowerCase() === 'cancel') {
202
- deleteStrategy(folder.path);
203
- console.log(chalk.gray('\n Strategy cancelled and folder deleted.'));
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 (userInput.toLowerCase() === 'done') {
209
- // Save config
210
- saveModule(folder.path, 'config.json', {
211
- name: name.trim(),
212
- description: modules.description || '',
213
- createdAt: new Date().toISOString(),
214
- modules: Object.keys(modules).filter(k => k !== 'description')
215
- });
216
-
217
- console.log(chalk.green('\n ✓ Strategy saved!'));
218
- console.log(chalk.cyan(` Location: ${folder.path}`));
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
- await prompts.waitForEnter();
224
- return;
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
- // Print AI response (without code blocks for cleaner output)
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
- } catch (e) {
266
- spinner.fail(`Error: ${e.message}`);
267
- messages.pop();
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
- /** Get default model */
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
- /** My Strategies Menu */
289
- const myStrategiesMenu = async (strategies, service) => {
290
- while (true) {
291
- console.clear();
292
- displayBanner();
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
- console.log(chalk.cyan('╠' + '═'.repeat(W) + ''));
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
- // Options: Run, Edit with AI, Delete
363
- const opts = ['[1] RUN', '[2] EDIT WITH AI', '[3] DELETE'];
364
- const optLine = opts.join(' ');
365
- console.log(chalk.cyan('║') + centerText(
366
- chalk.green(opts[0]) + ' ' + chalk.yellow(opts[1]) + ' ' + chalk.red(opts[2]), W
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
- console.log(chalk.cyan('' + '─'.repeat(W) + '╣'));
370
- console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
371
- console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
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
- const input = await prompts.textInput(chalk.cyan('SELECT (1/2/3/B): '));
374
- const choice = (input || '').toLowerCase().trim();
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
- if (choice === '1') {
377
- console.log(chalk.yellow('\n Running custom strategy...'));
378
- console.log(chalk.gray(' This will use your connected accounts and market data.'));
379
- console.log(chalk.gray(' (Full execution coming soon)'));
380
- await prompts.waitForEnter();
381
- } else if (choice === '2') {
382
- console.log(chalk.yellow('\n Edit with AI coming soon...'));
383
- await prompts.waitForEnter();
384
- } else if (choice === '3') {
385
- const confirm = await prompts.confirmPrompt(`Delete "${strategy.name || strategy.folder}"?`, false);
386
- if (confirm) {
387
- deleteStrategy(strategy.path);
388
- console.log(chalk.green('\n ✓ Strategy deleted'));
389
- await prompts.waitForEnter();
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, createStrategyFolder, saveModule };
288
+ module.exports = { customStrategyMenu, loadStrategies };
@@ -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
- const col1Str = ' '.repeat(pad1) + chalk.cyan(col1) + ' '.repeat(colWidth - col1.length - pad1);
46
- const col2Str = ' '.repeat(pad2) + chalk.yellow(col2) + ' '.repeat(colWidth - col2.length - pad2);
47
- const col3Str = ' '.repeat(pad3) + chalk.green(col3) + ' '.repeat(lastColWidth - col3.length - pad3);
48
-
49
- console.log(chalk.cyan('║') + col1Str + col2Str + col3Str + chalk.cyan('║'));
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 input = await prompts.textInput(chalk.cyan('SELECT (1/2/3/B): '));
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
- log.info('Starting Custom Strategy mode');
75
- await customStrategyMenu(service);
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 launchAlgo(accountService, selectedAccount, contract, config);
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 };