hedgequantx 2.7.99 → 2.8.1

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.
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Copy Trading Executor - Execution engine for copy trading
3
+ * Handles signal processing, order placement, and AI supervision
4
+ */
5
+
6
+ const readline = require('readline');
7
+ const { connections } = require('../../services');
8
+ const { AlgoUI, renderSessionSummary } = require('./ui');
9
+ const { SupervisionEngine } = require('../../services/ai-supervision');
10
+ const { M1 } = require('../../lib/m/s1');
11
+ const { MarketDataFeed } = require('../../lib/data');
12
+
13
+ /**
14
+ * Launch Copy Trading execution
15
+ */
16
+ const launchCopyTrading = async (config) => {
17
+ const { lead, followers, contract, dailyTarget, maxRisk, showNames, supervisionConfig } = config;
18
+
19
+ // Initialize AI Supervision if configured
20
+ const supervisionEnabled = supervisionConfig?.supervisionEnabled && supervisionConfig?.agents?.length > 0;
21
+ const supervisionEngine = supervisionEnabled ? new SupervisionEngine(supervisionConfig) : null;
22
+ const aiContext = { recentTicks: [], recentSignals: [], maxTicks: 100 };
23
+
24
+ const leadAccount = lead.account;
25
+ const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
26
+ const leadName = showNames
27
+ ? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
28
+ : 'HQX Lead *****';
29
+ const symbolName = contract.name;
30
+ const contractId = contract.id;
31
+ const tickSize = contract.tickSize || 0.25;
32
+
33
+ const followerNames = followers.map((f, i) =>
34
+ showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
35
+ );
36
+
37
+ const ui = new AlgoUI({
38
+ subtitle: supervisionEnabled ? 'HQX Copy + AI' : 'HQX Copy Trading',
39
+ mode: 'copy-trading'
40
+ });
41
+
42
+ const stats = {
43
+ accountName: leadName,
44
+ followerNames,
45
+ symbol: symbolName,
46
+ qty: lead.contracts,
47
+ followerQty: followers[0]?.contracts || lead.contracts,
48
+ target: dailyTarget,
49
+ risk: maxRisk,
50
+ propfirm: leadAccount.propfirm || 'Unknown',
51
+ platform: leadAccount.platform || 'Rithmic',
52
+ pnl: 0,
53
+ followerPnl: 0,
54
+ trades: 0,
55
+ wins: 0,
56
+ losses: 0,
57
+ latency: 0,
58
+ connected: false,
59
+ startTime: Date.now(),
60
+ followersCount: followers.length
61
+ };
62
+
63
+ let running = true;
64
+ let stopReason = null;
65
+ let startingPnL = null;
66
+ let currentPosition = 0;
67
+ let pendingOrder = false;
68
+ let tickCount = 0;
69
+
70
+ // Initialize Strategy
71
+ const strategy = new M1({ tickSize });
72
+ strategy.initialize(contractId, tickSize);
73
+
74
+ // Initialize Market Data Feed
75
+ const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
76
+
77
+ // Log startup
78
+ ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
79
+ ui.addLog('info', `Symbol: ${symbolName} | Qty: ${lead.contracts}/${followers[0]?.contracts}`);
80
+ ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
81
+ if (supervisionEnabled) ui.addLog('info', `AI: ${supervisionEngine.getActiveCount()} agents`);
82
+ ui.addLog('info', 'Connecting...');
83
+
84
+ // Handle strategy signals
85
+ strategy.on('signal', async (signal) => {
86
+ if (!running || pendingOrder || currentPosition !== 0) return;
87
+
88
+ let { direction, entry, stopLoss, takeProfit, confidence } = signal;
89
+
90
+ aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
91
+ if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
92
+
93
+ ui.addLog('signal', `${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
94
+
95
+ // AI Supervision
96
+ if (supervisionEnabled && supervisionEngine) {
97
+ const result = await supervisionEngine.supervise({
98
+ symbolId: symbolName,
99
+ signal: { direction, entry, stopLoss, takeProfit, confidence },
100
+ recentTicks: aiContext.recentTicks,
101
+ recentSignals: aiContext.recentSignals,
102
+ stats,
103
+ config: { dailyTarget, maxRisk }
104
+ });
105
+
106
+ if (result.decision === 'reject') {
107
+ ui.addLog('info', `AI rejected: ${result.reason}`);
108
+ return;
109
+ }
110
+
111
+ // Apply optimizations
112
+ if (result.optimizedSignal?.aiOptimized) {
113
+ const opt = result.optimizedSignal;
114
+ if (opt.entry) entry = opt.entry;
115
+ if (opt.stopLoss) stopLoss = opt.stopLoss;
116
+ if (opt.takeProfit) takeProfit = opt.takeProfit;
117
+ }
118
+ ui.addLog('info', `AI ${result.decision} (${result.confidence}%)`);
119
+ }
120
+
121
+ // Execute orders
122
+ pendingOrder = true;
123
+ try {
124
+ const orderSide = direction === 'long' ? 0 : 1;
125
+
126
+ // Place order on LEAD
127
+ const leadResult = await leadService.placeOrder({
128
+ accountId: leadAccount.accountId,
129
+ contractId,
130
+ type: 2,
131
+ side: orderSide,
132
+ size: lead.contracts
133
+ });
134
+
135
+ if (leadResult.success) {
136
+ currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
137
+ stats.trades++;
138
+ ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x`);
139
+
140
+ // Place orders on FOLLOWERS
141
+ await placeFollowerOrders(followers, contractId, orderSide, direction, ui);
142
+
143
+ // Bracket orders on lead
144
+ if (stopLoss && takeProfit) {
145
+ await placeBracketOrders(leadService, leadAccount, contractId, direction, lead.contracts, stopLoss, takeProfit);
146
+ ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
147
+ }
148
+ } else {
149
+ ui.addLog('error', `Lead failed: ${leadResult.error}`);
150
+ }
151
+ } catch (e) {
152
+ ui.addLog('error', `Order error: ${e.message}`);
153
+ }
154
+ pendingOrder = false;
155
+ });
156
+
157
+ // Handle market data ticks
158
+ marketFeed.on('tick', (tick) => {
159
+ tickCount++;
160
+ const latencyStart = Date.now();
161
+
162
+ aiContext.recentTicks.push(tick);
163
+ if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
164
+
165
+ strategy.processTick({
166
+ contractId: tick.contractId || contractId,
167
+ price: tick.price,
168
+ bid: tick.bid,
169
+ ask: tick.ask,
170
+ volume: tick.volume || 1,
171
+ side: tick.lastTradeSide || 'unknown',
172
+ timestamp: tick.timestamp || Date.now()
173
+ });
174
+
175
+ stats.latency = Date.now() - latencyStart;
176
+ if (tickCount % 100 === 0) ui.addLog('info', `#${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
177
+ });
178
+
179
+ marketFeed.on('connected', () => { stats.connected = true; ui.addLog('success', 'Connected!'); });
180
+ marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
181
+ marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Disconnected'); });
182
+
183
+ // Connect to market data
184
+ try {
185
+ const token = leadService.token || leadService.getToken?.();
186
+ const pk = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
187
+ await marketFeed.connect(token, pk, contractId);
188
+ await marketFeed.subscribe(symbolName, contractId);
189
+ } catch (e) {
190
+ ui.addLog('error', `Connect failed: ${e.message}`);
191
+ }
192
+
193
+ // Poll P&L
194
+ const pollPnL = async () => {
195
+ try {
196
+ const res = await leadService.getTradingAccounts();
197
+ if (res.success && res.accounts) {
198
+ const acc = res.accounts.find(a => a.accountId === leadAccount.accountId);
199
+ if (acc?.profitAndLoss !== undefined) {
200
+ if (startingPnL === null) startingPnL = acc.profitAndLoss;
201
+ stats.pnl = acc.profitAndLoss - startingPnL;
202
+ }
203
+ }
204
+ if (stats.pnl >= dailyTarget) {
205
+ stopReason = 'target';
206
+ running = false;
207
+ ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
208
+ } else if (stats.pnl <= -maxRisk) {
209
+ stopReason = 'risk';
210
+ running = false;
211
+ ui.addLog('error', `RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
212
+ }
213
+ } catch (e) { /* silent */ }
214
+ };
215
+
216
+ // Start intervals
217
+ const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
218
+ const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
219
+ pollPnL();
220
+
221
+ // Keyboard handler
222
+ const cleanupKeys = setupKeyHandler(() => { running = false; stopReason = 'manual'; });
223
+
224
+ // Wait for stop
225
+ await new Promise(resolve => {
226
+ const check = setInterval(() => {
227
+ if (!running) { clearInterval(check); resolve(); }
228
+ }, 100);
229
+ });
230
+
231
+ // Cleanup
232
+ clearInterval(refreshInterval);
233
+ clearInterval(pnlInterval);
234
+ await marketFeed.disconnect();
235
+ if (cleanupKeys) cleanupKeys();
236
+ ui.cleanup();
237
+
238
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
239
+ process.stdin.resume();
240
+
241
+ // Duration
242
+ const durationMs = Date.now() - stats.startTime;
243
+ const hours = Math.floor(durationMs / 3600000);
244
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
245
+ const seconds = Math.floor((durationMs % 60000) / 1000);
246
+ stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
247
+
248
+ renderSessionSummary(stats, stopReason);
249
+
250
+ console.log('\n Returning to menu in 3 seconds...');
251
+ await new Promise(resolve => setTimeout(resolve, 3000));
252
+ };
253
+
254
+ /**
255
+ * Place orders on all follower accounts
256
+ */
257
+ const placeFollowerOrders = async (followers, contractId, orderSide, direction, ui) => {
258
+ for (let i = 0; i < followers.length; i++) {
259
+ const f = followers[i];
260
+ const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
261
+
262
+ try {
263
+ const fResult = await fService.placeOrder({
264
+ accountId: f.account.accountId,
265
+ contractId,
266
+ type: 2,
267
+ side: orderSide,
268
+ size: f.contracts
269
+ });
270
+
271
+ if (fResult.success) {
272
+ ui.addLog('trade', `F${i + 1}: ${direction.toUpperCase()} ${f.contracts}x`);
273
+ } else {
274
+ ui.addLog('error', `F${i + 1}: Failed`);
275
+ }
276
+ } catch (e) {
277
+ ui.addLog('error', `F${i + 1}: ${e.message}`);
278
+ }
279
+ }
280
+ };
281
+
282
+ /**
283
+ * Place bracket orders (stop loss and take profit)
284
+ */
285
+ const placeBracketOrders = async (service, account, contractId, direction, size, stopLoss, takeProfit) => {
286
+ const exitSide = direction === 'long' ? 1 : 0;
287
+
288
+ await service.placeOrder({
289
+ accountId: account.accountId,
290
+ contractId,
291
+ type: 4,
292
+ side: exitSide,
293
+ size,
294
+ stopPrice: stopLoss
295
+ });
296
+
297
+ await service.placeOrder({
298
+ accountId: account.accountId,
299
+ contractId,
300
+ type: 1,
301
+ side: exitSide,
302
+ size,
303
+ limitPrice: takeProfit
304
+ });
305
+ };
306
+
307
+ /**
308
+ * Setup keyboard handler for stopping
309
+ */
310
+ const setupKeyHandler = (onStop) => {
311
+ if (!process.stdin.isTTY) return null;
312
+
313
+ readline.emitKeypressEvents(process.stdin);
314
+ process.stdin.setRawMode(true);
315
+ process.stdin.resume();
316
+
317
+ const onKey = (str, key) => {
318
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
319
+ onStop();
320
+ }
321
+ };
322
+
323
+ process.stdin.on('keypress', onKey);
324
+
325
+ return () => {
326
+ process.stdin.removeListener('keypress', onKey);
327
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
328
+ };
329
+ };
330
+
331
+ module.exports = { launchCopyTrading };
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Copy Trading Mode - HQX Ultra Scalping
3
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
11
  const { prompts } = require('../../utils');
13
12
  const { checkMarketHours } = require('../../services/rithmic/market');
14
-
15
- // Strategy & Market Data
16
- const { M1 } = require('../../lib/m/s1');
17
- const { MarketDataFeed } = require('../../lib/data');
13
+ const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
14
+ const { launchCopyTrading } = require('./copy-executor');
15
+ const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
18
16
 
19
17
  /**
20
18
  * Copy Trading Menu
@@ -144,6 +142,43 @@ const copyTradingMenu = async () => {
144
142
  const showNames = await prompts.confirmPrompt('Show account names?', false);
145
143
  if (showNames === null) return;
146
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
+
147
182
  // Summary
148
183
  console.log();
149
184
  console.log(chalk.white.bold(' SUMMARY:'));
@@ -154,6 +189,7 @@ const copyTradingMenu = async () => {
154
189
  console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
155
190
  }
156
191
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
192
+ if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
157
193
  console.log();
158
194
 
159
195
  const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
@@ -165,7 +201,8 @@ const copyTradingMenu = async () => {
165
201
  contract,
166
202
  dailyTarget,
167
203
  maxRisk,
168
- showNames
204
+ showNames,
205
+ supervisionConfig
169
206
  });
170
207
  };
171
208
 
@@ -209,260 +246,4 @@ const selectSymbol = async (service) => {
209
246
  return selected === 'back' || selected === null ? null : selected;
210
247
  };
211
248
 
212
- /**
213
- * Launch Copy Trading - HQX Ultra Scalping with trade copying
214
- */
215
- const launchCopyTrading = async (config) => {
216
- const { lead, followers, contract, dailyTarget, maxRisk, showNames } = config;
217
-
218
- const leadAccount = lead.account;
219
- const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
220
- const leadName = showNames
221
- ? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
222
- : 'HQX Lead *****';
223
- const symbolName = contract.name;
224
- const contractId = contract.id;
225
- const tickSize = contract.tickSize || 0.25;
226
-
227
- const followerNames = followers.map((f, i) =>
228
- showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
229
- );
230
-
231
- const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
232
-
233
- const stats = {
234
- accountName: leadName,
235
- followerNames,
236
- symbol: symbolName,
237
- qty: lead.contracts,
238
- followerQty: followers[0]?.contracts || lead.contracts,
239
- target: dailyTarget,
240
- risk: maxRisk,
241
- propfirm: leadAccount.propfirm || 'Unknown',
242
- platform: leadAccount.platform || 'Rithmic',
243
- pnl: 0,
244
- followerPnl: 0,
245
- trades: 0,
246
- wins: 0,
247
- losses: 0,
248
- latency: 0,
249
- connected: false,
250
- startTime: Date.now(),
251
- followersCount: followers.length
252
- };
253
-
254
- let running = true;
255
- let stopReason = null;
256
- let startingPnL = null;
257
- let currentPosition = 0;
258
- let pendingOrder = false;
259
- let tickCount = 0;
260
-
261
- // Initialize Strategy
262
- const strategy = new M1({ tickSize });
263
- strategy.initialize(contractId, tickSize);
264
-
265
- // Initialize Market Data Feed
266
- const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
267
-
268
- // Log startup
269
- ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
270
- ui.addLog('info', `Symbol: ${symbolName} | Lead Qty: ${lead.contracts} | Follower Qty: ${followers[0]?.contracts}`);
271
- ui.addLog('info', `Target: $${dailyTarget} | Max Risk: $${maxRisk}`);
272
- ui.addLog('info', 'Connecting to market data...');
273
-
274
- // Handle strategy signals - place on lead AND all followers
275
- strategy.on('signal', async (signal) => {
276
- if (!running || pendingOrder || currentPosition !== 0) return;
277
-
278
- const { direction, entry, stopLoss, takeProfit, confidence } = signal;
279
-
280
- ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
281
-
282
- pendingOrder = true;
283
- try {
284
- const orderSide = direction === 'long' ? 0 : 1;
285
-
286
- // Place order on LEAD
287
- const leadResult = await leadService.placeOrder({
288
- accountId: leadAccount.accountId,
289
- contractId: contractId,
290
- type: 2,
291
- side: orderSide,
292
- size: lead.contracts
293
- });
294
-
295
- if (leadResult.success) {
296
- currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
297
- stats.trades++;
298
- ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x @ market`);
299
-
300
- // Place orders on ALL FOLLOWERS
301
- for (let i = 0; i < followers.length; i++) {
302
- const f = followers[i];
303
- const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
304
-
305
- try {
306
- const fResult = await fService.placeOrder({
307
- accountId: f.account.accountId,
308
- contractId: contractId,
309
- type: 2,
310
- side: orderSide,
311
- size: f.contracts
312
- });
313
-
314
- if (fResult.success) {
315
- ui.addLog('trade', `FOLLOWER ${i + 1}: ${direction.toUpperCase()} ${f.contracts}x @ market`);
316
- } else {
317
- ui.addLog('error', `FOLLOWER ${i + 1}: Order failed`);
318
- }
319
- } catch (e) {
320
- ui.addLog('error', `FOLLOWER ${i + 1}: ${e.message}`);
321
- }
322
- }
323
-
324
- // Place bracket orders on lead (SL/TP)
325
- if (stopLoss && takeProfit) {
326
- await leadService.placeOrder({
327
- accountId: leadAccount.accountId, contractId, type: 4,
328
- side: direction === 'long' ? 1 : 0, size: lead.contracts, stopPrice: stopLoss
329
- });
330
- await leadService.placeOrder({
331
- accountId: leadAccount.accountId, contractId, type: 1,
332
- side: direction === 'long' ? 1 : 0, size: lead.contracts, limitPrice: takeProfit
333
- });
334
- ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
335
- }
336
- } else {
337
- ui.addLog('error', `Lead order failed: ${leadResult.error}`);
338
- }
339
- } catch (e) {
340
- ui.addLog('error', `Order error: ${e.message}`);
341
- }
342
- pendingOrder = false;
343
- });
344
-
345
- // Handle market data ticks
346
- marketFeed.on('tick', (tick) => {
347
- tickCount++;
348
- const latencyStart = Date.now();
349
-
350
- strategy.processTick({
351
- contractId: tick.contractId || contractId,
352
- price: tick.price,
353
- bid: tick.bid,
354
- ask: tick.ask,
355
- volume: tick.volume || 1,
356
- side: tick.lastTradeSide || 'unknown',
357
- timestamp: tick.timestamp || Date.now()
358
- });
359
-
360
- stats.latency = Date.now() - latencyStart;
361
-
362
- if (tickCount % 100 === 0) {
363
- ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
364
- }
365
- });
366
-
367
- marketFeed.on('connected', () => {
368
- stats.connected = true;
369
- ui.addLog('success', 'Market data connected!');
370
- });
371
-
372
- marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
373
- marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market data disconnected'); });
374
-
375
- // Connect to market data
376
- try {
377
- const token = leadService.token || leadService.getToken?.();
378
- const propfirmKey = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
379
- await marketFeed.connect(token, propfirmKey, contractId);
380
- await marketFeed.subscribe(symbolName, contractId);
381
- } catch (e) {
382
- ui.addLog('error', `Failed to connect: ${e.message}`);
383
- }
384
-
385
- // Poll P&L from lead and followers
386
- const pollPnL = async () => {
387
- try {
388
- // Lead P&L
389
- const leadResult = await leadService.getTradingAccounts();
390
- if (leadResult.success && leadResult.accounts) {
391
- const acc = leadResult.accounts.find(a => a.accountId === leadAccount.accountId);
392
- if (acc && acc.profitAndLoss !== undefined) {
393
- if (startingPnL === null) startingPnL = acc.profitAndLoss;
394
- stats.pnl = acc.profitAndLoss - startingPnL;
395
- }
396
- }
397
-
398
- // Check target/risk
399
- if (stats.pnl >= dailyTarget) {
400
- stopReason = 'target';
401
- running = false;
402
- ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
403
- } else if (stats.pnl <= -maxRisk) {
404
- stopReason = 'risk';
405
- running = false;
406
- ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
407
- }
408
- } catch (e) { /* silent */ }
409
- };
410
-
411
- // Start intervals
412
- const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
413
- const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
414
- pollPnL();
415
-
416
- // Keyboard handler
417
- const setupKeyHandler = () => {
418
- if (!process.stdin.isTTY) return;
419
- readline.emitKeypressEvents(process.stdin);
420
- process.stdin.setRawMode(true);
421
- process.stdin.resume();
422
-
423
- const onKey = (str, key) => {
424
- if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
425
- running = false;
426
- stopReason = 'manual';
427
- }
428
- };
429
- process.stdin.on('keypress', onKey);
430
- return () => {
431
- process.stdin.removeListener('keypress', onKey);
432
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
433
- };
434
- };
435
-
436
- const cleanupKeys = setupKeyHandler();
437
-
438
- // Wait for stop
439
- await new Promise(resolve => {
440
- const check = setInterval(() => {
441
- if (!running) { clearInterval(check); resolve(); }
442
- }, 100);
443
- });
444
-
445
- // Cleanup
446
- clearInterval(refreshInterval);
447
- clearInterval(pnlInterval);
448
- await marketFeed.disconnect();
449
- if (cleanupKeys) cleanupKeys();
450
- ui.cleanup();
451
-
452
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
453
- process.stdin.resume();
454
-
455
- // Duration
456
- const durationMs = Date.now() - stats.startTime;
457
- const hours = Math.floor(durationMs / 3600000);
458
- const minutes = Math.floor((durationMs % 3600000) / 60000);
459
- const seconds = Math.floor((durationMs % 60000) / 1000);
460
- stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
461
-
462
- renderSessionSummary(stats, stopReason);
463
-
464
- console.log('\n Returning to menu in 3 seconds...');
465
- await new Promise(resolve => setTimeout(resolve, 3000));
466
- };
467
-
468
249
  module.exports = { copyTradingMenu };