hedgequantx 2.9.18 → 2.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +42 -64
  3. package/src/lib/m/hqx-2b.js +7 -0
  4. package/src/lib/m/index.js +138 -0
  5. package/src/lib/m/ultra-scalping.js +7 -0
  6. package/src/menus/connect.js +14 -17
  7. package/src/menus/dashboard.js +58 -76
  8. package/src/pages/accounts.js +38 -49
  9. package/src/pages/algo/copy-trading.js +546 -178
  10. package/src/pages/algo/index.js +18 -75
  11. package/src/pages/algo/one-account.js +322 -57
  12. package/src/pages/algo/ui.js +15 -15
  13. package/src/pages/orders.js +19 -22
  14. package/src/pages/positions.js +19 -22
  15. package/src/pages/stats/index.js +15 -16
  16. package/src/pages/user.js +7 -11
  17. package/src/services/ai-supervision/health.js +35 -47
  18. package/src/services/index.js +1 -9
  19. package/src/services/rithmic/accounts.js +8 -6
  20. package/src/ui/box.js +9 -5
  21. package/src/ui/index.js +5 -18
  22. package/src/ui/menu.js +4 -4
  23. package/src/pages/ai-agents-ui.js +0 -388
  24. package/src/pages/ai-agents.js +0 -494
  25. package/src/pages/ai-models.js +0 -389
  26. package/src/pages/algo/algo-executor.js +0 -307
  27. package/src/pages/algo/copy-executor.js +0 -331
  28. package/src/pages/algo/custom-strategy.js +0 -313
  29. package/src/services/ai-supervision/consensus.js +0 -284
  30. package/src/services/ai-supervision/context.js +0 -275
  31. package/src/services/ai-supervision/directive.js +0 -167
  32. package/src/services/ai-supervision/index.js +0 -309
  33. package/src/services/ai-supervision/parser.js +0 -278
  34. package/src/services/ai-supervision/symbols.js +0 -259
  35. package/src/services/cliproxy/index.js +0 -256
  36. package/src/services/cliproxy/installer.js +0 -111
  37. package/src/services/cliproxy/manager.js +0 -387
  38. package/src/services/llmproxy/index.js +0 -166
  39. package/src/services/llmproxy/manager.js +0 -411
@@ -1,24 +1,49 @@
1
1
  /**
2
- * Copy Trading Mode - HQX Ultra Scalping
3
- * Same as One Account but copies trades to multiple followers
4
- * Supports multi-agent AI supervision
2
+ * @fileoverview Copy Trading Mode with Strategy Selection
3
+ * @module pages/algo/copy-trading
5
4
  */
6
5
 
7
6
  const chalk = require('chalk');
8
7
  const ora = require('ora');
8
+ const readline = require('readline');
9
9
 
10
10
  const { connections } = require('../../services');
11
- const { prompts } = require('../../utils');
11
+ const { AlgoUI, renderSessionSummary } = require('./ui');
12
+ const { logger, prompts } = require('../../utils');
12
13
  const { checkMarketHours } = require('../../services/rithmic/market');
13
- const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
14
- const { launchCopyTrading } = require('./copy-executor');
15
- const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
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
+
16
39
 
17
40
  /**
18
41
  * Copy Trading Menu
19
42
  */
20
43
  const copyTradingMenu = async () => {
21
- // Check if market is open
44
+ log.info('Copy Trading menu opened');
45
+
46
+ // Check market hours
22
47
  const market = checkMarketHours();
23
48
  if (!market.isOpen && !market.message.includes('early')) {
24
49
  console.log();
@@ -28,222 +53,565 @@ const copyTradingMenu = async () => {
28
53
  await prompts.waitForEnter();
29
54
  return;
30
55
  }
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})`);
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})`));
46
62
  console.log(chalk.gray(' Connect to another PropFirm first'));
63
+ console.log();
47
64
  await prompts.waitForEnter();
48
65
  return;
49
66
  }
50
-
51
- spinner.succeed(`Found ${activeAccounts.length} active accounts`);
52
-
53
- // Step 1: Select LEAD Account
67
+
54
68
  console.log();
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)
69
+ console.log(chalk.yellow.bold(' Copy Trading Setup'));
72
70
  console.log();
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'));
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');
114
78
  await prompts.waitForEnter();
115
79
  return;
116
80
  }
117
-
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
+
118
97
  // Step 3: Select Symbol
119
98
  console.log();
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
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
126
104
  console.log();
127
- console.log(chalk.cyan.bold(' STEP 4: CONFIGURE PARAMETERS'));
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
128
110
  console.log();
129
-
111
+ console.log(chalk.cyan(' Step 5: Configure Parameters'));
112
+
130
113
  const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
131
114
  if (leadContracts === null) return;
132
-
133
- const followerContracts = await prompts.numberInput('Follower contracts (each):', leadContracts, 1, 10);
115
+
116
+ const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
134
117
  if (followerContracts === null) return;
135
-
118
+
136
119
  const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
137
120
  if (dailyTarget === null) return;
138
-
121
+
139
122
  const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
140
123
  if (maxRisk === null) return;
141
-
142
- const showNames = await prompts.confirmPrompt('Show account names?', false);
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
+ ]);
143
130
  if (showNames === null) return;
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
131
+
132
+ // Confirm
133
+ const strategyInfo = getStrategy(strategyId);
183
134
  console.log();
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
- }
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}`));
191
140
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
192
- if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
193
141
  console.log();
194
-
142
+
195
143
  const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
196
144
  if (!confirm) return;
197
-
145
+
146
+ // Launch
198
147
  await launchCopyTrading({
199
- lead: { account: leadAccount, contracts: leadContracts },
200
- followers: followers.map(f => ({ account: f, contracts: followerContracts })),
201
- contract,
148
+ lead: { ...lead, symbol, contracts: leadContracts },
149
+ follower: { ...follower, symbol, contracts: followerContracts },
150
+ strategyId,
202
151
  dailyTarget,
203
152
  maxRisk,
204
153
  showNames,
205
- supervisionConfig
206
154
  });
207
155
  };
208
156
 
209
157
  /**
210
- * Symbol selection - sorted with popular indices first
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>}
211
215
  */
212
216
  const selectSymbol = async (service) => {
213
217
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
214
-
215
- const contractsResult = await service.getContracts();
216
- if (!contractsResult.success || !contractsResult.contracts?.length) {
217
- spinner.fail('Failed to load contracts');
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();
218
266
  return null;
219
267
  }
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
+ });
220
452
 
221
- let contracts = contractsResult.contracts;
453
+ marketFeed.on('connected', () => {
454
+ stats.connected = true;
455
+ ui.addLog('success', 'Market data connected!');
456
+ });
222
457
 
223
- // Sort: Popular indices first
224
- const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
458
+ marketFeed.on('error', (err) => {
459
+ ui.addLog('error', `Market: ${err.message}`);
460
+ });
225
461
 
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);
462
+ marketFeed.on('disconnected', () => {
463
+ stats.connected = false;
464
+ ui.addLog('error', 'Market data disconnected');
235
465
  });
236
466
 
237
- spinner.succeed(`Found ${contracts.length} contracts`);
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
+ }
238
476
 
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' });
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);
539
+
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();
244
567
 
245
- const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
246
- return selected === 'back' || selected === null ? null : selected;
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
+ };
247
615
  };
248
616
 
249
617
  module.exports = { copyTradingMenu };