hedgequantx 2.9.20 → 2.9.22

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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +64 -42
  3. package/src/menus/connect.js +17 -14
  4. package/src/menus/dashboard.js +76 -58
  5. package/src/pages/accounts.js +49 -38
  6. package/src/pages/ai-agents-ui.js +388 -0
  7. package/src/pages/ai-agents.js +494 -0
  8. package/src/pages/ai-models.js +389 -0
  9. package/src/pages/algo/algo-executor.js +307 -0
  10. package/src/pages/algo/copy-executor.js +331 -0
  11. package/src/pages/algo/copy-trading.js +178 -546
  12. package/src/pages/algo/custom-strategy.js +313 -0
  13. package/src/pages/algo/index.js +75 -18
  14. package/src/pages/algo/one-account.js +57 -322
  15. package/src/pages/algo/ui.js +15 -15
  16. package/src/pages/orders.js +22 -19
  17. package/src/pages/positions.js +22 -19
  18. package/src/pages/stats/index.js +16 -15
  19. package/src/pages/user.js +11 -7
  20. package/src/services/ai-supervision/consensus.js +284 -0
  21. package/src/services/ai-supervision/context.js +275 -0
  22. package/src/services/ai-supervision/directive.js +167 -0
  23. package/src/services/ai-supervision/health.js +47 -35
  24. package/src/services/ai-supervision/index.js +359 -0
  25. package/src/services/ai-supervision/parser.js +278 -0
  26. package/src/services/ai-supervision/symbols.js +259 -0
  27. package/src/services/cliproxy/index.js +256 -0
  28. package/src/services/cliproxy/installer.js +111 -0
  29. package/src/services/cliproxy/manager.js +387 -0
  30. package/src/services/index.js +9 -1
  31. package/src/services/llmproxy/index.js +166 -0
  32. package/src/services/llmproxy/manager.js +411 -0
  33. package/src/services/rithmic/accounts.js +6 -8
  34. package/src/ui/box.js +5 -9
  35. package/src/ui/index.js +18 -5
  36. package/src/ui/menu.js +4 -4
@@ -1,49 +1,24 @@
1
1
  /**
2
- * @fileoverview Copy Trading Mode with Strategy Selection
3
- * @module pages/algo/copy-trading
2
+ * Copy Trading Mode - HQX Ultra Scalping
3
+ * Same as One Account but copies trades to multiple followers
4
+ * Supports multi-agent AI supervision
4
5
  */
5
6
 
6
7
  const chalk = require('chalk');
7
8
  const ora = require('ora');
8
- const readline = require('readline');
9
9
 
10
10
  const { connections } = require('../../services');
11
- const { AlgoUI, renderSessionSummary } = require('./ui');
12
- const { logger, prompts } = require('../../utils');
11
+ const { prompts } = require('../../utils');
13
12
  const { checkMarketHours } = require('../../services/rithmic/market');
14
-
15
- // Strategy Registry & Market Data
16
- const { getAvailableStrategies, loadStrategy, getStrategy } = require('../../lib/m');
17
- const { MarketDataFeed } = require('../../lib/data');
18
-
19
- const log = logger.scope('CopyTrading');
20
-
21
-
22
- /**
23
- * Strategy Selection
24
- * @returns {Promise<string|null>} Selected strategy ID or null
25
- */
26
- const selectStrategy = async () => {
27
- const strategies = getAvailableStrategies();
28
-
29
- const options = strategies.map(s => ({
30
- label: s.id === 'ultra-scalping' ? 'HQX Scalping' : 'HQX Sweep',
31
- value: s.id
32
- }));
33
- options.push({ label: chalk.gray('< Back'), value: 'back' });
34
-
35
- const selected = await prompts.selectOption('Select Strategy:', options);
36
- return selected === 'back' ? null : selected;
37
- };
38
-
13
+ const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
14
+ const { launchCopyTrading } = require('./copy-executor');
15
+ const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
39
16
 
40
17
  /**
41
18
  * Copy Trading Menu
42
19
  */
43
20
  const copyTradingMenu = async () => {
44
- log.info('Copy Trading menu opened');
45
-
46
- // Check market hours
21
+ // Check if market is open
47
22
  const market = checkMarketHours();
48
23
  if (!market.isOpen && !market.message.includes('early')) {
49
24
  console.log();
@@ -53,565 +28,222 @@ const copyTradingMenu = async () => {
53
28
  await prompts.waitForEnter();
54
29
  return;
55
30
  }
56
-
57
- const allConns = connections.getAll();
58
-
59
- if (allConns.length < 2) {
60
- console.log();
61
- console.log(chalk.yellow(` Copy Trading requires 2 connected accounts (found: ${allConns.length})`));
31
+
32
+ const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
33
+
34
+ const allAccounts = await connections.getAllAccounts();
35
+
36
+ if (!allAccounts?.length) {
37
+ spinner.fail('No accounts found');
38
+ await prompts.waitForEnter();
39
+ return;
40
+ }
41
+
42
+ const activeAccounts = allAccounts.filter(acc => acc.status === 0);
43
+
44
+ if (activeAccounts.length < 2) {
45
+ spinner.fail(`Need at least 2 active accounts (found: ${activeAccounts.length})`);
62
46
  console.log(chalk.gray(' Connect to another PropFirm first'));
63
- console.log();
64
47
  await prompts.waitForEnter();
65
48
  return;
66
49
  }
67
-
50
+
51
+ spinner.succeed(`Found ${activeAccounts.length} active accounts`);
52
+
53
+ // Step 1: Select LEAD Account
68
54
  console.log();
69
- console.log(chalk.yellow.bold(' Copy Trading Setup'));
55
+ console.log(chalk.cyan.bold(' STEP 1: SELECT LEAD ACCOUNT'));
56
+ const leadOptions = activeAccounts.map(acc => {
57
+ const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
58
+ const balance = acc.balance !== null && acc.balance !== undefined
59
+ ? ` - $${acc.balance.toLocaleString()}`
60
+ : '';
61
+ return {
62
+ label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
63
+ value: acc
64
+ };
65
+ });
66
+ leadOptions.push({ label: '< Back', value: 'back' });
67
+
68
+ const leadAccount = await prompts.selectOption('Lead Account:', leadOptions);
69
+ if (!leadAccount || leadAccount === 'back') return;
70
+
71
+ // Step 2: Select FOLLOWER Account(s)
70
72
  console.log();
71
-
72
- // Fetch all accounts
73
- const spinner = ora({ text: 'Fetching accounts...', color: 'yellow' }).start();
74
- const allAccounts = await fetchAllAccounts(allConns);
75
-
76
- if (allAccounts.length < 2) {
77
- spinner.fail('Need at least 2 active accounts');
73
+ console.log(chalk.yellow.bold(' STEP 2: SELECT FOLLOWER ACCOUNT(S)'));
74
+ console.log(chalk.gray(' (Select accounts to copy trades to)'));
75
+
76
+ const followers = [];
77
+ const availableFollowers = activeAccounts.filter(a => a.accountId !== leadAccount.accountId);
78
+
79
+ while (availableFollowers.length > 0) {
80
+ const remaining = availableFollowers.filter(a => !followers.find(f => f.accountId === a.accountId));
81
+ if (remaining.length === 0) break;
82
+
83
+ const followerOptions = remaining.map(acc => {
84
+ const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
85
+ const balance = acc.balance !== null && acc.balance !== undefined
86
+ ? ` - $${acc.balance.toLocaleString()}`
87
+ : '';
88
+ return {
89
+ label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
90
+ value: acc
91
+ };
92
+ });
93
+
94
+ if (followers.length > 0) {
95
+ followerOptions.push({ label: chalk.green('✓ Done selecting followers'), value: 'done' });
96
+ }
97
+ followerOptions.push({ label: '< Back', value: 'back' });
98
+
99
+ const msg = followers.length === 0 ? 'Select Follower:' : `Add another follower (${followers.length} selected):`;
100
+ const selected = await prompts.selectOption(msg, followerOptions);
101
+
102
+ if (!selected || selected === 'back') {
103
+ if (followers.length === 0) return;
104
+ break;
105
+ }
106
+ if (selected === 'done') break;
107
+
108
+ followers.push(selected);
109
+ console.log(chalk.green(` ✓ Added: ${selected.accountName || selected.accountId}`));
110
+ }
111
+
112
+ if (followers.length === 0) {
113
+ console.log(chalk.red(' No followers selected'));
78
114
  await prompts.waitForEnter();
79
115
  return;
80
116
  }
81
-
82
- spinner.succeed(`Found ${allAccounts.length} active accounts`);
83
-
84
- // Step 1: Select Lead Account
85
- console.log(chalk.cyan(' Step 1: Select LEAD Account'));
86
- const leadIdx = await selectAccount('Lead Account:', allAccounts, -1);
87
- if (leadIdx === null || leadIdx === -1) return;
88
- const lead = allAccounts[leadIdx];
89
-
90
- // Step 2: Select Follower Account
91
- console.log();
92
- console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
93
- const followerIdx = await selectAccount('Follower Account:', allAccounts, leadIdx);
94
- if (followerIdx === null || followerIdx === -1) return;
95
- const follower = allAccounts[followerIdx];
96
-
117
+
97
118
  // Step 3: Select Symbol
98
119
  console.log();
99
- console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
100
- const symbol = await selectSymbol(lead.service);
101
- if (!symbol) return;
102
-
103
- // Step 4: Select Strategy
120
+ console.log(chalk.magenta.bold(' STEP 3: SELECT SYMBOL'));
121
+ const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
122
+ const contract = await selectSymbol(leadService);
123
+ if (!contract) return;
124
+
125
+ // Step 4: Configure Parameters
104
126
  console.log();
105
- console.log(chalk.cyan(' Step 4: Select Trading Strategy'));
106
- const strategyId = await selectStrategy();
107
- if (!strategyId) return;
108
-
109
- // Step 5: Configure Parameters
127
+ console.log(chalk.cyan.bold(' STEP 4: CONFIGURE PARAMETERS'));
110
128
  console.log();
111
- console.log(chalk.cyan(' Step 5: Configure Parameters'));
112
-
129
+
113
130
  const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
114
131
  if (leadContracts === null) return;
115
-
116
- const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
132
+
133
+ const followerContracts = await prompts.numberInput('Follower contracts (each):', leadContracts, 1, 10);
117
134
  if (followerContracts === null) return;
118
-
135
+
119
136
  const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
120
137
  if (dailyTarget === null) return;
121
-
138
+
122
139
  const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
123
140
  if (maxRisk === null) return;
124
-
125
- // Step 6: Privacy
126
- const showNames = await prompts.selectOption('Account names:', [
127
- { label: 'Hide account names', value: false },
128
- { label: 'Show account names', value: true },
129
- ]);
141
+
142
+ const showNames = await prompts.confirmPrompt('Show account names?', false);
130
143
  if (showNames === null) return;
131
-
132
- // Confirm
133
- const strategyInfo = getStrategy(strategyId);
144
+
145
+ // Check for AI Supervision
146
+ const agentCount = getActiveAgentCount();
147
+ let supervisionConfig = null;
148
+
149
+ if (agentCount > 0) {
150
+ console.log();
151
+ console.log(chalk.cyan(` ${agentCount} AI Agent(s) available for supervision`));
152
+ const enableAI = await prompts.confirmPrompt('Enable AI Supervision?', true);
153
+
154
+ if (enableAI) {
155
+ // Run pre-flight check - ALL agents must pass
156
+ console.log();
157
+ console.log(chalk.yellow(' Running AI pre-flight check...'));
158
+ console.log();
159
+
160
+ const agents = getActiveAgents();
161
+ const preflightResults = await runPreflightCheck(agents);
162
+
163
+ // Display results
164
+ const lines = formatPreflightResults(preflightResults, 60);
165
+ for (const line of lines) console.log(line);
166
+
167
+ const summary = getPreflightSummary(preflightResults);
168
+ console.log();
169
+ console.log(` ${summary.text}`);
170
+ console.log();
171
+
172
+ if (!preflightResults.success) {
173
+ console.log(chalk.red(' Cannot start algo - fix agent connections first.'));
174
+ await prompts.waitForEnter();
175
+ return;
176
+ }
177
+
178
+ supervisionConfig = getSupervisionConfig();
179
+ }
180
+ }
181
+
182
+ // Summary
134
183
  console.log();
135
- console.log(chalk.white(' Summary:'));
136
- console.log(chalk.cyan(` Strategy: ${strategyInfo.name}`));
137
- console.log(chalk.cyan(` Symbol: ${symbol.name}`));
138
- console.log(chalk.cyan(` Lead: ${lead.propfirm} x${leadContracts}`));
139
- console.log(chalk.cyan(` Follower: ${follower.propfirm} x${followerContracts}`));
184
+ console.log(chalk.white.bold(' SUMMARY:'));
185
+ console.log(chalk.cyan(` Symbol: ${contract.name}`));
186
+ console.log(chalk.cyan(` Lead: ${leadAccount.propfirm} x${leadContracts}`));
187
+ console.log(chalk.yellow(` Followers (${followers.length}):`));
188
+ for (const f of followers) {
189
+ console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
190
+ }
140
191
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
192
+ if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
141
193
  console.log();
142
-
194
+
143
195
  const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
144
196
  if (!confirm) return;
145
-
146
- // Launch
197
+
147
198
  await launchCopyTrading({
148
- lead: { ...lead, symbol, contracts: leadContracts },
149
- follower: { ...follower, symbol, contracts: followerContracts },
150
- strategyId,
199
+ lead: { account: leadAccount, contracts: leadContracts },
200
+ followers: followers.map(f => ({ account: f, contracts: followerContracts })),
201
+ contract,
151
202
  dailyTarget,
152
203
  maxRisk,
153
204
  showNames,
205
+ supervisionConfig
154
206
  });
155
207
  };
156
208
 
157
209
  /**
158
- * Fetch all active accounts from connections
159
- * @param {Array} allConns - All connections
160
- * @returns {Promise<Array>}
161
- */
162
- const fetchAllAccounts = async (allConns) => {
163
- const allAccounts = [];
164
-
165
- for (const conn of allConns) {
166
- try {
167
- const result = await conn.service.getTradingAccounts();
168
- if (result.success && result.accounts) {
169
- const active = result.accounts.filter(a => a.status === 0);
170
- for (const acc of active) {
171
- allAccounts.push({
172
- account: acc,
173
- service: conn.service,
174
- propfirm: conn.propfirm,
175
- type: conn.type,
176
- });
177
- }
178
- }
179
- } catch (err) {
180
- log.warn('Failed to get accounts', { type: conn.type, error: err.message });
181
- }
182
- }
183
-
184
- return allAccounts;
185
- };
186
-
187
- /**
188
- * Select account from list
189
- * @param {string} message - Prompt message
190
- * @param {Array} accounts - Available accounts
191
- * @param {number} excludeIdx - Index to exclude
192
- * @returns {Promise<number|null>}
193
- */
194
- const selectAccount = async (message, accounts, excludeIdx) => {
195
- const options = accounts
196
- .map((a, i) => ({ a, i }))
197
- .filter(x => x.i !== excludeIdx)
198
- .map(x => {
199
- const acc = x.a.account;
200
- const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
201
- return {
202
- label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
203
- value: x.i,
204
- };
205
- });
206
-
207
- options.push({ label: '< Cancel', value: -1 });
208
- return prompts.selectOption(message, options);
209
- };
210
-
211
- /**
212
- * Select trading symbol
213
- * @param {Object} service - Service instance
214
- * @returns {Promise<Object|null>}
210
+ * Symbol selection - sorted with popular indices first
215
211
  */
216
212
  const selectSymbol = async (service) => {
217
213
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
218
-
219
- try {
220
- // Try Rithmic API first for consistency
221
- let contracts = await getContractsFromAPI();
222
-
223
- // Fallback to service
224
- if (!contracts && typeof service.getContracts === 'function') {
225
- const result = await service.getContracts();
226
- if (result.success && result.contracts?.length > 0) {
227
- contracts = result.contracts;
228
- }
229
- }
230
-
231
- if (!contracts || !contracts.length) {
232
- spinner.fail('No contracts available');
233
- await prompts.waitForEnter();
234
- return null;
235
- }
236
-
237
- spinner.succeed(`Found ${contracts.length} contracts`);
238
-
239
- // Build options from RAW API data - no static mapping
240
- const options = [];
241
- let currentGroup = null;
242
-
243
- for (const c of contracts) {
244
- // Use RAW API field: contractGroup
245
- if (c.contractGroup && c.contractGroup !== currentGroup) {
246
- currentGroup = c.contractGroup;
247
- options.push({
248
- label: chalk.cyan.bold(`-- ${currentGroup} --`),
249
- value: null,
250
- disabled: true,
251
- });
252
- }
253
-
254
- // Use RAW API fields: symbol (trading symbol), name (product name), exchange
255
- const label = ` ${c.symbol} - ${c.name} (${c.exchange})`;
256
- options.push({ label, value: c });
257
- }
258
-
259
- options.push({ label: '', value: null, disabled: true });
260
- options.push({ label: chalk.gray('< Cancel'), value: null });
261
-
262
- return prompts.selectOption('Trading Symbol:', options);
263
- } catch (err) {
264
- spinner.fail(`Error loading contracts: ${err.message}`);
265
- await prompts.waitForEnter();
214
+
215
+ const contractsResult = await service.getContracts();
216
+ if (!contractsResult.success || !contractsResult.contracts?.length) {
217
+ spinner.fail('Failed to load contracts');
266
218
  return null;
267
219
  }
268
- };
269
-
270
- /**
271
- * Get contracts from Rithmic API - RAW data only
272
- * @returns {Promise<Array|null>}
273
- */
274
- const getContractsFromAPI = async () => {
275
- const allConns = connections.getAll();
276
- const rithmicConn = allConns.find(c => c.type === 'rithmic');
277
-
278
- if (rithmicConn && typeof rithmicConn.service.getContracts === 'function') {
279
- const result = await rithmicConn.service.getContracts();
280
- if (result.success && result.contracts?.length > 0) {
281
- // Return RAW API data - no mapping
282
- return result.contracts;
283
- }
284
- }
285
-
286
- return null;
287
- };
288
-
289
- /**
290
- * Launch Copy Trading session with strategy
291
- * @param {Object} config - Session configuration
292
- */
293
- const launchCopyTrading = async (config) => {
294
- const { lead, follower, strategyId, dailyTarget, maxRisk, showNames } = config;
295
-
296
- // Load strategy dynamically
297
- const strategyInfo = getStrategy(strategyId);
298
- const strategyModule = loadStrategy(strategyId);
299
-
300
- // Account names (masked for privacy)
301
- const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
302
- const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
303
-
304
- const tickSize = lead.symbol.tickSize || 0.25;
305
- const contractId = lead.symbol.id;
306
-
307
- const ui = new AlgoUI({ subtitle: `${strategyInfo.name} - Copy Trading`, mode: 'copy-trading' });
308
-
309
- const stats = {
310
- leadName,
311
- followerName,
312
- leadSymbol: lead.symbol.name,
313
- followerSymbol: follower.symbol.name,
314
- leadQty: lead.contracts,
315
- followerQty: follower.contracts,
316
- target: dailyTarget,
317
- risk: maxRisk,
318
- pnl: 0,
319
- trades: 0,
320
- wins: 0,
321
- losses: 0,
322
- latency: 0,
323
- connected: false,
324
- platform: lead.account.platform || 'Rithmic',
325
- startTime: Date.now(),
326
- };
327
-
328
- let running = true;
329
- let stopReason = null;
330
- let currentPosition = 0;
331
- let pendingOrder = false;
332
- let tickCount = 0;
333
-
334
- // Initialize Strategy dynamically
335
- const strategy = new strategyModule.M1({ tickSize });
336
- strategy.initialize(contractId, tickSize);
337
-
338
- // Initialize Market Data Feed
339
- const marketFeed = new MarketDataFeed({ propfirm: lead.propfirm });
340
-
341
- // Measure API latency (CLI <-> API)
342
- const measureLatency = async () => {
343
- try {
344
- const start = Date.now();
345
- await lead.service.getPositions(lead.account.accountId);
346
- stats.latency = Date.now() - start;
347
- } catch (e) {
348
- stats.latency = 0;
349
- }
350
- };
351
-
352
- // Log startup
353
- ui.addLog('info', `Strategy: ${strategyInfo.name}`);
354
- ui.addLog('info', `Lead: ${stats.leadName} -> Follower: ${stats.followerName}`);
355
- ui.addLog('info', `Symbol: ${stats.leadSymbol} | Target: $${dailyTarget} | Risk: $${maxRisk}`);
356
- ui.addLog('info', `Params: ${strategyInfo.params.stopTicks}t stop, ${strategyInfo.params.targetTicks}t target (${strategyInfo.params.riskReward})`);
357
- ui.addLog('info', 'Connecting to market data...');
358
-
359
- // Handle strategy signals - execute on BOTH accounts
360
- strategy.on('signal', async (signal) => {
361
- if (!running || pendingOrder || currentPosition !== 0) return;
362
-
363
- const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
364
-
365
- ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
366
-
367
- pendingOrder = true;
368
- try {
369
- const orderSide = direction === 'long' ? 0 : 1;
370
-
371
- // Place on LEAD account
372
- const leadResult = await lead.service.placeOrder({
373
- accountId: lead.account.accountId,
374
- contractId: contractId,
375
- type: 2,
376
- side: orderSide,
377
- size: lead.contracts
378
- });
379
-
380
- if (leadResult.success) {
381
- ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x`);
382
-
383
- // Place on FOLLOWER account
384
- const followerResult = await follower.service.placeOrder({
385
- accountId: follower.account.accountId,
386
- contractId: contractId,
387
- type: 2,
388
- side: orderSide,
389
- size: follower.contracts
390
- });
391
-
392
- if (followerResult.success) {
393
- ui.addLog('trade', `FOLLOWER: ${direction.toUpperCase()} ${follower.contracts}x`);
394
- currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
395
- stats.trades++;
396
-
397
- // Place bracket orders on both accounts
398
- if (stopLoss && takeProfit) {
399
- const exitSide = direction === 'long' ? 1 : 0;
400
-
401
- // Lead SL/TP
402
- await lead.service.placeOrder({
403
- accountId: lead.account.accountId, contractId, type: 4, side: exitSide, size: lead.contracts, stopPrice: stopLoss
404
- });
405
- await lead.service.placeOrder({
406
- accountId: lead.account.accountId, contractId, type: 1, side: exitSide, size: lead.contracts, limitPrice: takeProfit
407
- });
408
-
409
- // Follower SL/TP
410
- await follower.service.placeOrder({
411
- accountId: follower.account.accountId, contractId, type: 4, side: exitSide, size: follower.contracts, stopPrice: stopLoss
412
- });
413
- await follower.service.placeOrder({
414
- accountId: follower.account.accountId, contractId, type: 1, side: exitSide, size: follower.contracts, limitPrice: takeProfit
415
- });
416
-
417
- ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
418
- }
419
- } else {
420
- ui.addLog('error', `Follower order failed: ${followerResult.error}`);
421
- }
422
- } else {
423
- ui.addLog('error', `Lead order failed: ${leadResult.error}`);
424
- }
425
- } catch (e) {
426
- ui.addLog('error', `Order error: ${e.message}`);
427
- }
428
- pendingOrder = false;
429
- });
430
-
431
- // Handle market data ticks
432
- marketFeed.on('tick', (tick) => {
433
- tickCount++;
434
- const latencyStart = Date.now();
435
-
436
- strategy.processTick({
437
- contractId: tick.contractId || contractId,
438
- price: tick.price,
439
- bid: tick.bid,
440
- ask: tick.ask,
441
- volume: tick.volume || 1,
442
- side: tick.lastTradeSide || 'unknown',
443
- timestamp: tick.timestamp || Date.now()
444
- });
445
-
446
- stats.latency = Date.now() - latencyStart;
447
-
448
- if (tickCount % 100 === 0) {
449
- ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
450
- }
451
- });
452
220
 
453
- marketFeed.on('connected', () => {
454
- stats.connected = true;
455
- ui.addLog('success', 'Market data connected!');
456
- });
221
+ let contracts = contractsResult.contracts;
457
222
 
458
- marketFeed.on('error', (err) => {
459
- ui.addLog('error', `Market: ${err.message}`);
460
- });
223
+ // Sort: Popular indices first
224
+ const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
461
225
 
462
- marketFeed.on('disconnected', () => {
463
- stats.connected = false;
464
- ui.addLog('error', 'Market data disconnected');
226
+ contracts.sort((a, b) => {
227
+ const baseA = a.baseSymbol || a.symbol || '';
228
+ const baseB = b.baseSymbol || b.symbol || '';
229
+ const idxA = popularPrefixes.findIndex(p => baseA === p || baseA.startsWith(p));
230
+ const idxB = popularPrefixes.findIndex(p => baseB === p || baseB.startsWith(p));
231
+ if (idxA !== -1 && idxB !== -1) return idxA - idxB;
232
+ if (idxA !== -1) return -1;
233
+ if (idxB !== -1) return 1;
234
+ return baseA.localeCompare(baseB);
465
235
  });
466
236
 
467
- // Connect to market data
468
- try {
469
- const token = lead.service.token || lead.service.getToken?.();
470
- const propfirmKey = (lead.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
471
- await marketFeed.connect(token, propfirmKey, contractId);
472
- await marketFeed.subscribe(lead.symbol.name, contractId);
473
- } catch (e) {
474
- ui.addLog('error', `Failed to connect: ${e.message}`);
475
- }
476
-
477
- // Poll combined P&L from both accounts
478
- const pollPnL = async () => {
479
- try {
480
- let combinedPnL = 0;
481
-
482
- // Lead P&L
483
- const leadResult = await lead.service.getPositions(lead.account.accountId);
484
- if (leadResult.success && leadResult.positions) {
485
- const pos = leadResult.positions.find(p => {
486
- const sym = p.contractId || p.symbol || '';
487
- return sym.includes(lead.symbol.name) || sym.includes(contractId);
488
- });
489
- if (pos) combinedPnL += pos.profitAndLoss || 0;
490
- }
491
-
492
- // Follower P&L
493
- const followerResult = await follower.service.getPositions(follower.account.accountId);
494
- if (followerResult.success && followerResult.positions) {
495
- const pos = followerResult.positions.find(p => {
496
- const sym = p.contractId || p.symbol || '';
497
- return sym.includes(follower.symbol.name) || sym.includes(contractId);
498
- });
499
- if (pos) combinedPnL += pos.profitAndLoss || 0;
500
- }
501
-
502
- // Update stats
503
- if (combinedPnL !== stats.pnl) {
504
- const diff = combinedPnL - stats.pnl;
505
- if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
506
- if (diff >= 0) stats.wins++;
507
- else stats.losses++;
508
- }
509
- stats.pnl = combinedPnL;
510
-
511
- if (stats.pnl !== 0) {
512
- strategy.recordTradeResult(stats.pnl);
513
- }
514
- }
515
-
516
- // Check target/risk limits
517
- if (stats.pnl >= dailyTarget) {
518
- stopReason = 'target';
519
- running = false;
520
- ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
521
- } else if (stats.pnl <= -maxRisk) {
522
- stopReason = 'risk';
523
- running = false;
524
- ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
525
- }
526
- } catch (e) {
527
- // Silent fail - will retry
528
- }
529
- };
530
-
531
- // UI refresh loop
532
- const refreshInterval = setInterval(() => {
533
- if (running) ui.render(stats);
534
- }, 250);
535
-
536
- // Measure API latency every 5 seconds
537
- measureLatency();
538
- const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
237
+ spinner.succeed(`Found ${contracts.length} contracts`);
539
238
 
540
- // Poll P&L every 2 seconds
541
- pollPnL();
542
- const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
543
-
544
- // Keyboard handling
545
- const cleanupKeys = setupKeyboardHandler(() => {
546
- running = false;
547
- stopReason = 'manual';
548
- });
549
-
550
- // Wait for stop
551
- await new Promise((resolve) => {
552
- const check = setInterval(() => {
553
- if (!running) {
554
- clearInterval(check);
555
- resolve();
556
- }
557
- }, 100);
558
- });
559
-
560
- // Cleanup
561
- clearInterval(refreshInterval);
562
- clearInterval(latencyInterval);
563
- clearInterval(pnlInterval);
564
- await marketFeed.disconnect();
565
- if (cleanupKeys) cleanupKeys();
566
- ui.cleanup();
239
+ const options = contracts.map(c => ({
240
+ label: `${c.symbol} - ${c.name} (${c.exchange})`,
241
+ value: c
242
+ }));
243
+ options.push({ label: chalk.gray('< Back'), value: 'back' });
567
244
 
568
- if (process.stdin.isTTY) {
569
- process.stdin.setRawMode(false);
570
- }
571
- process.stdin.resume();
572
-
573
- // Duration
574
- const durationMs = Date.now() - stats.startTime;
575
- const hours = Math.floor(durationMs / 3600000);
576
- const minutes = Math.floor((durationMs % 3600000) / 60000);
577
- const seconds = Math.floor((durationMs % 60000) / 1000);
578
- stats.duration = hours > 0
579
- ? `${hours}h ${minutes}m ${seconds}s`
580
- : minutes > 0
581
- ? `${minutes}m ${seconds}s`
582
- : `${seconds}s`;
583
-
584
- // Show summary
585
- renderSessionSummary(stats, stopReason);
586
- await prompts.waitForEnter();
587
- };
588
-
589
- /**
590
- * Setup keyboard handler
591
- * @param {Function} onStop - Stop callback
592
- * @returns {Function|null} Cleanup function
593
- */
594
- const setupKeyboardHandler = (onStop) => {
595
- if (!process.stdin.isTTY) return null;
596
-
597
- readline.emitKeypressEvents(process.stdin);
598
- process.stdin.setRawMode(true);
599
- process.stdin.resume();
600
-
601
- const handler = (str, key) => {
602
- if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
603
- onStop();
604
- }
605
- };
606
-
607
- process.stdin.on('keypress', handler);
608
-
609
- return () => {
610
- process.stdin.removeListener('keypress', handler);
611
- if (process.stdin.isTTY) {
612
- process.stdin.setRawMode(false);
613
- }
614
- };
245
+ const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
246
+ return selected === 'back' || selected === null ? null : selected;
615
247
  };
616
248
 
617
249
  module.exports = { copyTradingMenu };