hedgequantx 2.5.43 → 2.5.44

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.5.43",
3
+ "version": "2.5.44",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -1,6 +1,15 @@
1
1
  /**
2
- * @fileoverview Copy Trading Mode
2
+ * @fileoverview Professional Copy Trading System
3
3
  * @module pages/algo/copy-trading
4
+ *
5
+ * Ultra-low latency copy trading with:
6
+ * - Fast polling (250ms adaptive)
7
+ * - Multi-follower support
8
+ * - Parallel order execution
9
+ * - Automatic retry with exponential backoff
10
+ * - Position reconciliation
11
+ * - Slippage protection
12
+ * - Cross-platform support (ProjectX <-> Rithmic)
4
13
  */
5
14
 
6
15
  const chalk = require('chalk');
@@ -13,8 +22,512 @@ const { logger, prompts } = require('../../utils');
13
22
  const { checkMarketHours } = require('../../services/projectx/market');
14
23
  const { algoLogger } = require('./logger');
15
24
 
25
+ // AI Strategy Supervisor
26
+ const aiService = require('../../services/ai');
27
+ const StrategySupervisor = require('../../services/ai/strategy-supervisor');
28
+
16
29
  const log = logger.scope('CopyTrading');
17
30
 
31
+ // ============================================================================
32
+ // COPY ENGINE - Professional Order Execution
33
+ // ============================================================================
34
+
35
+ /**
36
+ * CopyEngine - Handles all copy trading logic with professional execution
37
+ */
38
+ class CopyEngine {
39
+ constructor(config) {
40
+ this.lead = config.lead;
41
+ this.followers = config.followers; // Array of followers
42
+ this.symbol = config.symbol;
43
+ this.dailyTarget = config.dailyTarget;
44
+ this.maxRisk = config.maxRisk;
45
+ this.ui = config.ui;
46
+ this.stats = config.stats;
47
+
48
+ // Engine state
49
+ this.running = false;
50
+ this.stopReason = null;
51
+
52
+ // Position tracking
53
+ this.leadPositions = new Map(); // key: positionKey, value: position
54
+ this.followerPositions = new Map(); // key: `${followerIdx}:${posKey}`, value: position
55
+ this.pendingOrders = new Map(); // key: orderId, value: orderInfo
56
+
57
+ // Order queue for sequential execution per follower
58
+ this.orderQueues = new Map(); // key: followerIdx, value: queue[]
59
+ this.processingQueue = new Map(); // key: followerIdx, value: boolean
60
+
61
+ // Timing
62
+ this.pollInterval = 250; // Start at 250ms, adaptive
63
+ this.lastPollTime = 0;
64
+ this.pollCount = 0;
65
+ this.orderCount = 0;
66
+ this.failedOrders = 0;
67
+
68
+ // Retry configuration
69
+ this.maxRetries = 3;
70
+ this.retryDelayBase = 100; // ms
71
+
72
+ // Slippage protection (ticks)
73
+ this.maxSlippageTicks = 4;
74
+ }
75
+
76
+ /**
77
+ * Get unique position key (cross-platform compatible)
78
+ */
79
+ getPositionKey(position) {
80
+ return position.contractId || position.symbol || position.id;
81
+ }
82
+
83
+ /**
84
+ * Resolve symbol for target platform
85
+ */
86
+ resolveSymbol(position, targetAccount) {
87
+ const targetType = targetAccount.type;
88
+
89
+ if (targetType === 'rithmic') {
90
+ return {
91
+ symbol: position.symbol || this.symbol.name,
92
+ exchange: position.exchange || this.symbol.exchange || 'CME',
93
+ contractId: null
94
+ };
95
+ } else {
96
+ return {
97
+ contractId: position.contractId || this.symbol.id || this.symbol.contractId,
98
+ symbol: null,
99
+ exchange: null
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Build order data for specific platform
106
+ */
107
+ buildOrderData(params, platformType) {
108
+ const { accountId, contractId, symbol, exchange, side, size, type, price } = params;
109
+
110
+ if (platformType === 'rithmic') {
111
+ return {
112
+ accountId,
113
+ symbol,
114
+ exchange: exchange || 'CME',
115
+ size,
116
+ side,
117
+ type,
118
+ price: price || 0
119
+ };
120
+ } else {
121
+ return {
122
+ accountId,
123
+ contractId,
124
+ type,
125
+ side,
126
+ size
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Execute order with retry logic
133
+ */
134
+ async executeOrderWithRetry(follower, orderData, retryCount = 0) {
135
+ try {
136
+ const startTime = Date.now();
137
+ const result = await follower.service.placeOrder(orderData);
138
+ const latency = Date.now() - startTime;
139
+
140
+ if (result.success) {
141
+ this.orderCount++;
142
+ this.stats.latency = Math.round((this.stats.latency + latency) / 2);
143
+ return { success: true, latency };
144
+ }
145
+
146
+ // Retry on failure
147
+ if (retryCount < this.maxRetries) {
148
+ const delay = this.retryDelayBase * Math.pow(2, retryCount);
149
+ await this.sleep(delay);
150
+ return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
151
+ }
152
+
153
+ this.failedOrders++;
154
+ return { success: false, error: result.error || 'Max retries exceeded' };
155
+ } catch (err) {
156
+ if (retryCount < this.maxRetries) {
157
+ const delay = this.retryDelayBase * Math.pow(2, retryCount);
158
+ await this.sleep(delay);
159
+ return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
160
+ }
161
+
162
+ this.failedOrders++;
163
+ return { success: false, error: err.message };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Queue order for a follower (ensures sequential execution per follower)
169
+ */
170
+ async queueOrder(followerIdx, orderFn) {
171
+ if (!this.orderQueues.has(followerIdx)) {
172
+ this.orderQueues.set(followerIdx, []);
173
+ }
174
+
175
+ return new Promise((resolve) => {
176
+ this.orderQueues.get(followerIdx).push({ fn: orderFn, resolve });
177
+ this.processQueue(followerIdx);
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Process order queue for a follower
183
+ */
184
+ async processQueue(followerIdx) {
185
+ if (this.processingQueue.get(followerIdx)) return;
186
+
187
+ const queue = this.orderQueues.get(followerIdx);
188
+ if (!queue || queue.length === 0) return;
189
+
190
+ this.processingQueue.set(followerIdx, true);
191
+
192
+ while (queue.length > 0 && this.running) {
193
+ const { fn, resolve } = queue.shift();
194
+ try {
195
+ const result = await fn();
196
+ resolve(result);
197
+ } catch (err) {
198
+ resolve({ success: false, error: err.message });
199
+ }
200
+ }
201
+
202
+ this.processingQueue.set(followerIdx, false);
203
+ }
204
+
205
+ /**
206
+ * Copy position open to all followers (parallel execution)
207
+ */
208
+ async copyPositionOpen(position) {
209
+ const side = position.quantity > 0 ? 'LONG' : 'SHORT';
210
+ const orderSide = position.quantity > 0 ? 0 : 1;
211
+ const displaySymbol = position.symbol || this.symbol.name;
212
+ const size = Math.abs(position.quantity);
213
+ const entry = position.averagePrice || 0;
214
+
215
+ algoLogger.positionOpened(this.ui, displaySymbol, side, size, entry);
216
+
217
+ // Feed to AI supervisor
218
+ if (this.stats.aiSupervision) {
219
+ StrategySupervisor.feedSignal({
220
+ direction: side.toLowerCase(),
221
+ entry,
222
+ stopLoss: null,
223
+ takeProfit: null,
224
+ confidence: 0.5
225
+ });
226
+ }
227
+
228
+ // Execute on all followers in parallel
229
+ const promises = this.followers.map((follower, idx) => {
230
+ return this.queueOrder(idx, async () => {
231
+ const resolved = this.resolveSymbol(position, follower);
232
+ const orderData = this.buildOrderData({
233
+ accountId: follower.account.accountId,
234
+ contractId: resolved.contractId,
235
+ symbol: resolved.symbol,
236
+ exchange: resolved.exchange,
237
+ side: orderSide,
238
+ size: follower.contracts,
239
+ type: 2 // Market
240
+ }, follower.type);
241
+
242
+ algoLogger.info(this.ui, 'COPY ORDER', `${side} ${follower.contracts}x -> ${follower.propfirm}`);
243
+
244
+ const result = await this.executeOrderWithRetry(follower, orderData);
245
+
246
+ if (result.success) {
247
+ algoLogger.orderFilled(this.ui, displaySymbol, side, follower.contracts, entry);
248
+
249
+ // Track follower position
250
+ const posKey = this.getPositionKey(position);
251
+ this.followerPositions.set(`${idx}:${posKey}`, {
252
+ ...position,
253
+ followerIdx: idx,
254
+ openTime: Date.now()
255
+ });
256
+ } else {
257
+ algoLogger.orderRejected(this.ui, displaySymbol, result.error);
258
+ }
259
+
260
+ return result;
261
+ });
262
+ });
263
+
264
+ const results = await Promise.all(promises);
265
+ const successCount = results.filter(r => r.success).length;
266
+
267
+ if (successCount === this.followers.length) {
268
+ algoLogger.info(this.ui, 'ALL COPIED', `${successCount}/${this.followers.length} followers`);
269
+ } else if (successCount > 0) {
270
+ algoLogger.info(this.ui, 'PARTIAL COPY', `${successCount}/${this.followers.length} followers`);
271
+ }
272
+
273
+ return results;
274
+ }
275
+
276
+ /**
277
+ * Copy position close to all followers (parallel execution)
278
+ */
279
+ async copyPositionClose(position, exitPrice, pnl) {
280
+ const side = position.quantity > 0 ? 'LONG' : 'SHORT';
281
+ const closeSide = position.quantity > 0 ? 1 : 0;
282
+ const displaySymbol = position.symbol || this.symbol.name;
283
+ const size = Math.abs(position.quantity);
284
+
285
+ algoLogger.positionClosed(this.ui, displaySymbol, side, size, exitPrice, pnl);
286
+
287
+ // Feed to AI supervisor
288
+ if (this.stats.aiSupervision) {
289
+ StrategySupervisor.feedTradeResult({
290
+ side,
291
+ qty: size,
292
+ price: exitPrice,
293
+ pnl,
294
+ symbol: displaySymbol,
295
+ direction: side
296
+ });
297
+
298
+ const aiStatus = StrategySupervisor.getStatus();
299
+ if (aiStatus.patternsLearned.winning + aiStatus.patternsLearned.losing > 0) {
300
+ algoLogger.info(this.ui, 'AI LEARNING',
301
+ `${aiStatus.patternsLearned.winning}W/${aiStatus.patternsLearned.losing}L patterns`);
302
+ }
303
+ }
304
+
305
+ // Close on all followers in parallel
306
+ const posKey = this.getPositionKey(position);
307
+
308
+ const promises = this.followers.map((follower, idx) => {
309
+ return this.queueOrder(idx, async () => {
310
+ const resolved = this.resolveSymbol(position, follower);
311
+ const posIdentifier = follower.type === 'rithmic'
312
+ ? (position.symbol || this.symbol.name)
313
+ : (position.contractId || this.symbol.id);
314
+
315
+ algoLogger.info(this.ui, 'CLOSE ORDER', `${displaySymbol} -> ${follower.propfirm}`);
316
+
317
+ // Try closePosition first
318
+ let result = await follower.service.closePosition(
319
+ follower.account.accountId,
320
+ posIdentifier
321
+ );
322
+
323
+ if (!result.success) {
324
+ // Fallback: market order
325
+ const orderData = this.buildOrderData({
326
+ accountId: follower.account.accountId,
327
+ contractId: resolved.contractId,
328
+ symbol: resolved.symbol,
329
+ exchange: resolved.exchange,
330
+ side: closeSide,
331
+ size: follower.contracts,
332
+ type: 2
333
+ }, follower.type);
334
+
335
+ result = await this.executeOrderWithRetry(follower, orderData);
336
+ }
337
+
338
+ if (result.success) {
339
+ algoLogger.info(this.ui, 'CLOSED', `${displaySymbol} on ${follower.propfirm}`);
340
+ this.followerPositions.delete(`${idx}:${posKey}`);
341
+ } else {
342
+ algoLogger.error(this.ui, 'CLOSE FAILED', `${follower.propfirm}: ${result.error}`);
343
+ }
344
+
345
+ return result;
346
+ });
347
+ });
348
+
349
+ const results = await Promise.all(promises);
350
+ const successCount = results.filter(r => r.success).length;
351
+
352
+ if (successCount === this.followers.length) {
353
+ this.stats.trades++;
354
+ if (pnl >= 0) this.stats.wins++;
355
+ else this.stats.losses++;
356
+ }
357
+
358
+ return results;
359
+ }
360
+
361
+ /**
362
+ * Poll lead positions and detect changes
363
+ */
364
+ async pollLeadPositions() {
365
+ if (!this.running) return;
366
+
367
+ const startTime = Date.now();
368
+
369
+ try {
370
+ const result = await this.lead.service.getPositions(this.lead.account.accountId);
371
+ if (!result.success) return;
372
+
373
+ const currentPositions = result.positions || [];
374
+ const currentMap = new Map();
375
+
376
+ // Build current positions map
377
+ for (const pos of currentPositions) {
378
+ if (pos.quantity === 0) continue;
379
+ const key = this.getPositionKey(pos);
380
+ currentMap.set(key, pos);
381
+ }
382
+
383
+ // Detect new positions (opened)
384
+ for (const [key, pos] of currentMap) {
385
+ if (!this.leadPositions.has(key)) {
386
+ // New position - copy to followers
387
+ await this.copyPositionOpen(pos);
388
+ this.leadPositions.set(key, pos);
389
+ } else {
390
+ // Position exists - check for size change (scaling)
391
+ const oldPos = this.leadPositions.get(key);
392
+ if (Math.abs(pos.quantity) !== Math.abs(oldPos.quantity)) {
393
+ // Size changed - update tracked position (scaling in/out)
394
+ this.leadPositions.set(key, pos);
395
+ }
396
+ }
397
+ }
398
+
399
+ // Detect closed positions
400
+ for (const [key, oldPos] of this.leadPositions) {
401
+ if (!currentMap.has(key)) {
402
+ // Position closed - close on followers
403
+ const exitPrice = oldPos.averagePrice || 0;
404
+ const pnl = oldPos.profitAndLoss || 0;
405
+ await this.copyPositionClose(oldPos, exitPrice, pnl);
406
+ this.leadPositions.delete(key);
407
+ }
408
+ }
409
+
410
+ // Update P&L from current positions
411
+ const totalPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
412
+ this.stats.pnl = totalPnL;
413
+
414
+ // Check limits
415
+ if (totalPnL >= this.dailyTarget) {
416
+ this.stop('target');
417
+ algoLogger.info(this.ui, 'TARGET REACHED', `+$${totalPnL.toFixed(2)}`);
418
+ } else if (totalPnL <= -this.maxRisk) {
419
+ this.stop('risk');
420
+ algoLogger.error(this.ui, 'MAX RISK HIT', `-$${Math.abs(totalPnL).toFixed(2)}`);
421
+ }
422
+
423
+ // Adaptive polling - faster when positions are open
424
+ const pollTime = Date.now() - startTime;
425
+ this.stats.latency = pollTime;
426
+
427
+ if (this.leadPositions.size > 0) {
428
+ this.pollInterval = Math.max(100, Math.min(250, pollTime * 2));
429
+ } else {
430
+ this.pollInterval = Math.max(250, Math.min(500, pollTime * 3));
431
+ }
432
+
433
+ this.pollCount++;
434
+
435
+ } catch (err) {
436
+ log.warn('Poll error', { error: err.message });
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Reconcile follower positions with lead
442
+ */
443
+ async reconcilePositions() {
444
+ // Get all follower positions and compare with lead
445
+ for (let idx = 0; idx < this.followers.length; idx++) {
446
+ const follower = this.followers[idx];
447
+
448
+ try {
449
+ const result = await follower.service.getPositions(follower.account.accountId);
450
+ if (!result.success) continue;
451
+
452
+ const followerPositions = result.positions || [];
453
+
454
+ // Check each lead position has corresponding follower position
455
+ for (const [key, leadPos] of this.leadPositions) {
456
+ const hasFollowerPos = followerPositions.some(fp => {
457
+ const fpKey = this.getPositionKey(fp);
458
+ return fpKey === key && fp.quantity !== 0;
459
+ });
460
+
461
+ if (!hasFollowerPos) {
462
+ // Missing position on follower - need to open
463
+ algoLogger.info(this.ui, 'RECONCILE', `Missing ${key} on ${follower.propfirm}`);
464
+ await this.copyPositionOpen(leadPos);
465
+ }
466
+ }
467
+
468
+ // Check for orphaned follower positions (position on follower but not on lead)
469
+ for (const fp of followerPositions) {
470
+ if (fp.quantity === 0) continue;
471
+ const fpKey = this.getPositionKey(fp);
472
+
473
+ if (!this.leadPositions.has(fpKey)) {
474
+ // Orphaned position - close it
475
+ algoLogger.info(this.ui, 'RECONCILE', `Orphaned ${fpKey} on ${follower.propfirm}`);
476
+
477
+ const posIdentifier = follower.type === 'rithmic' ? fp.symbol : fp.contractId;
478
+ await follower.service.closePosition(follower.account.accountId, posIdentifier);
479
+ }
480
+ }
481
+
482
+ } catch (err) {
483
+ log.warn('Reconcile error', { follower: follower.propfirm, error: err.message });
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Start the copy engine
490
+ */
491
+ async start() {
492
+ this.running = true;
493
+ this.stats.connected = true;
494
+
495
+ algoLogger.info(this.ui, 'ENGINE STARTED', `Polling every ${this.pollInterval}ms`);
496
+ algoLogger.info(this.ui, 'FOLLOWERS', `${this.followers.length} account(s)`);
497
+
498
+ // Initial reconciliation
499
+ await this.reconcilePositions();
500
+
501
+ // Main polling loop
502
+ while (this.running) {
503
+ await this.pollLeadPositions();
504
+ await this.sleep(this.pollInterval);
505
+ }
506
+
507
+ return this.stopReason;
508
+ }
509
+
510
+ /**
511
+ * Stop the copy engine
512
+ */
513
+ stop(reason = 'manual') {
514
+ this.running = false;
515
+ this.stopReason = reason;
516
+ this.stats.connected = false;
517
+ }
518
+
519
+ /**
520
+ * Sleep utility
521
+ */
522
+ sleep(ms) {
523
+ return new Promise(resolve => setTimeout(resolve, ms));
524
+ }
525
+ }
526
+
527
+ // ============================================================================
528
+ // COPY TRADING MENU
529
+ // ============================================================================
530
+
18
531
  /**
19
532
  * Copy Trading Menu
20
533
  */
@@ -34,17 +547,17 @@ const copyTradingMenu = async () => {
34
547
 
35
548
  const allConns = connections.getAll();
36
549
 
37
- if (allConns.length < 2) {
550
+ if (allConns.length === 0) {
38
551
  console.log();
39
- console.log(chalk.yellow(` Copy Trading requires 2 connected accounts (found: ${allConns.length})`));
40
- console.log(chalk.gray(' Connect to another PropFirm first'));
552
+ console.log(chalk.yellow(' No connections found'));
553
+ console.log(chalk.gray(' Connect to a PropFirm first'));
41
554
  console.log();
42
555
  await prompts.waitForEnter();
43
556
  return;
44
557
  }
45
558
 
46
559
  console.log();
47
- console.log(chalk.yellow.bold(' Copy Trading Setup'));
560
+ console.log(chalk.yellow.bold(' COPY TRADING - Professional Mode'));
48
561
  console.log();
49
562
 
50
563
  // Fetch all accounts
@@ -53,61 +566,105 @@ const copyTradingMenu = async () => {
53
566
 
54
567
  if (allAccounts.length < 2) {
55
568
  spinner.fail('NEED AT LEAST 2 ACTIVE ACCOUNTS');
569
+ console.log(chalk.gray(' Copy Trading requires a lead + at least one follower'));
56
570
  await prompts.waitForEnter();
57
571
  return;
58
572
  }
59
573
 
60
- spinner.succeed(`Found ${allAccounts.length} active accounts`);
574
+ spinner.succeed(`Found ${allAccounts.length} accounts across ${allConns.length} connection(s)`);
61
575
 
62
576
  // Step 1: Select Lead Account
63
- console.log(chalk.cyan(' Step 1: Select LEAD Account'));
64
- const leadIdx = await selectAccount('LEAD ACCOUNT:', allAccounts, -1);
577
+ console.log();
578
+ console.log(chalk.cyan.bold(' Step 1: Select LEAD Account (source)'));
579
+ const leadIdx = await selectAccount('LEAD ACCOUNT:', allAccounts, []);
65
580
  if (leadIdx === null || leadIdx === -1) return;
66
581
  const lead = allAccounts[leadIdx];
67
582
 
68
- // Step 2: Select Follower Account
583
+ // Step 2: Select Follower Accounts (multiple)
69
584
  console.log();
70
- console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
71
- const followerIdx = await selectAccount('FOLLOWER ACCOUNT:', allAccounts, leadIdx);
72
- if (followerIdx === null || followerIdx === -1) return;
73
- const follower = allAccounts[followerIdx];
585
+ console.log(chalk.cyan.bold(' Step 2: Select FOLLOWER Account(s)'));
586
+ console.log(chalk.gray(' Select accounts to copy trades to'));
587
+
588
+ const followers = [];
589
+ let selectingFollowers = true;
590
+ const excludeIndices = [leadIdx];
591
+
592
+ while (selectingFollowers && excludeIndices.length < allAccounts.length) {
593
+ const followerIdx = await selectAccount(
594
+ followers.length === 0 ? 'FOLLOWER ACCOUNT:' : 'ADD ANOTHER FOLLOWER:',
595
+ allAccounts,
596
+ excludeIndices,
597
+ followers.length > 0 // Allow skip if at least one follower
598
+ );
599
+
600
+ if (followerIdx === null || followerIdx === -1) {
601
+ if (followers.length === 0) return; // Cancel
602
+ selectingFollowers = false;
603
+ } else if (followerIdx === -2) {
604
+ selectingFollowers = false; // Done adding
605
+ } else {
606
+ followers.push(allAccounts[followerIdx]);
607
+ excludeIndices.push(followerIdx);
608
+ console.log(chalk.green(` Added: ${allAccounts[followerIdx].propfirm}`));
609
+ }
610
+ }
611
+
612
+ if (followers.length === 0) {
613
+ console.log(chalk.red(' No followers selected'));
614
+ await prompts.waitForEnter();
615
+ return;
616
+ }
74
617
 
75
618
  // Step 3: Select Symbol
76
619
  console.log();
77
- console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
620
+ console.log(chalk.cyan.bold(' Step 3: Select Trading Symbol'));
78
621
  const symbol = await selectSymbol(lead.service);
79
622
  if (!symbol) return;
80
623
 
81
- // Step 4: Configure Parameters
624
+ // Step 4: Configure contracts for each account
82
625
  console.log();
83
- console.log(chalk.cyan(' Step 4: Configure Parameters'));
84
-
85
- const leadContracts = await prompts.numberInput('LEAD CONTRACTS:', 1, 1, 10);
626
+ console.log(chalk.cyan.bold(' Step 4: Configure Contract Sizes'));
627
+
628
+ const leadContracts = await prompts.numberInput(`${lead.propfirm} (LEAD) contracts:`, 1, 1, 10);
86
629
  if (leadContracts === null) return;
630
+ lead.contracts = leadContracts;
631
+
632
+ for (const follower of followers) {
633
+ const contracts = await prompts.numberInput(`${follower.propfirm} contracts:`, leadContracts, 1, 10);
634
+ if (contracts === null) return;
635
+ follower.contracts = contracts;
636
+ }
87
637
 
88
- const followerContracts = await prompts.numberInput('FOLLOWER CONTRACTS:', leadContracts, 1, 10);
89
- if (followerContracts === null) return;
90
-
91
- const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
638
+ // Step 5: Risk parameters
639
+ console.log();
640
+ console.log(chalk.cyan.bold(' Step 5: Risk Parameters'));
641
+
642
+ const dailyTarget = await prompts.numberInput('Daily target ($):', 500, 1, 10000);
92
643
  if (dailyTarget === null) return;
93
644
 
94
- const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
645
+ const maxRisk = await prompts.numberInput('Max risk ($):', 250, 1, 5000);
95
646
  if (maxRisk === null) return;
96
647
 
97
- // Step 5: Privacy
648
+ // Step 6: Privacy
98
649
  const showNames = await prompts.selectOption('ACCOUNT NAMES:', [
99
650
  { label: 'HIDE ACCOUNT NAMES', value: false },
100
651
  { label: 'SHOW ACCOUNT NAMES', value: true },
101
652
  ]);
102
653
  if (showNames === null) return;
103
654
 
104
- // Confirm
655
+ // Summary
105
656
  console.log();
106
- console.log(chalk.white(' Summary:'));
657
+ console.log(chalk.white.bold(' ═══════════════════════════════════════'));
658
+ console.log(chalk.white.bold(' COPY TRADING CONFIGURATION'));
659
+ console.log(chalk.white.bold(' ═══════════════════════════════════════'));
107
660
  console.log(chalk.cyan(` Symbol: ${symbol.name}`));
108
- console.log(chalk.cyan(` Lead: ${lead.propfirm} x${leadContracts}`));
109
- console.log(chalk.cyan(` Follower: ${follower.propfirm} x${followerContracts}`));
661
+ console.log(chalk.cyan(` Lead: ${lead.propfirm} (${leadContracts} contracts)`));
662
+ console.log(chalk.cyan(` Followers: ${followers.length}`));
663
+ followers.forEach(f => {
664
+ console.log(chalk.gray(` → ${f.propfirm} (${f.contracts} contracts)`));
665
+ });
110
666
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
667
+ console.log(chalk.white.bold(' ═══════════════════════════════════════'));
111
668
  console.log();
112
669
 
113
670
  const confirm = await prompts.confirmPrompt('START COPY TRADING?', true);
@@ -116,7 +673,7 @@ const copyTradingMenu = async () => {
116
673
  // Launch
117
674
  await launchCopyTrading({
118
675
  lead: { ...lead, symbol, contracts: leadContracts },
119
- follower: { ...follower, symbol, contracts: followerContracts },
676
+ followers: followers.map(f => ({ ...f, symbol })),
120
677
  dailyTarget,
121
678
  maxRisk,
122
679
  showNames,
@@ -125,71 +682,72 @@ const copyTradingMenu = async () => {
125
682
 
126
683
  /**
127
684
  * Fetch all active accounts from connections
128
- * @param {Array} allConns - All connections
129
- * @returns {Promise<Array>}
130
685
  */
131
686
  const fetchAllAccounts = async (allConns) => {
132
687
  const allAccounts = [];
133
688
 
134
- for (const conn of allConns) {
689
+ // Fetch in parallel
690
+ const promises = allConns.map(async (conn) => {
135
691
  try {
136
692
  const result = await conn.service.getTradingAccounts();
137
693
  if (result.success && result.accounts) {
138
- const active = result.accounts.filter(a => a.status === 0);
139
- for (const acc of active) {
140
- allAccounts.push({
694
+ return result.accounts
695
+ .filter(a => a.status === 0)
696
+ .map(acc => ({
141
697
  account: acc,
142
698
  service: conn.service,
143
699
  propfirm: conn.propfirm,
144
700
  type: conn.type,
145
- });
146
- }
701
+ }));
147
702
  }
148
703
  } catch (err) {
149
704
  log.warn('Failed to get accounts', { type: conn.type, error: err.message });
150
705
  }
151
- }
706
+ return [];
707
+ });
708
+
709
+ const results = await Promise.all(promises);
710
+ results.forEach(accounts => allAccounts.push(...accounts));
152
711
 
153
712
  return allAccounts;
154
713
  };
155
714
 
156
715
  /**
157
716
  * Select account from list
158
- * @param {string} message - Prompt message
159
- * @param {Array} accounts - Available accounts
160
- * @param {number} excludeIdx - Index to exclude
161
- * @returns {Promise<number|null>}
162
717
  */
163
- const selectAccount = async (message, accounts, excludeIdx) => {
718
+ const selectAccount = async (message, accounts, excludeIndices = [], allowDone = false) => {
164
719
  const options = accounts
165
720
  .map((a, i) => ({ a, i }))
166
- .filter(x => x.i !== excludeIdx)
721
+ .filter(x => !excludeIndices.includes(x.i))
167
722
  .map(x => {
168
723
  const acc = x.a.account;
169
- const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
724
+ const balance = acc.balance !== null && acc.balance !== undefined
725
+ ? ` ($${acc.balance.toLocaleString()})`
726
+ : '';
727
+ const platform = x.a.type === 'rithmic' ? ' [Rithmic]' : '';
170
728
  return {
171
- label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
729
+ label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.accountId}${balance}${platform}`,
172
730
  value: x.i,
173
731
  };
174
732
  });
175
733
 
176
- options.push({ label: '< CANCEL', value: -1 });
734
+ if (allowDone) {
735
+ options.push({ label: chalk.green('✓ DONE ADDING FOLLOWERS'), value: -2 });
736
+ }
737
+ options.push({ label: chalk.gray('< CANCEL'), value: -1 });
738
+
177
739
  return prompts.selectOption(message, options);
178
740
  };
179
741
 
180
742
  /**
181
743
  * Select trading symbol
182
- * @param {Object} service - Service instance
183
- * @returns {Promise<Object|null>}
184
744
  */
185
745
  const selectSymbol = async (service) => {
186
746
  const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
187
747
 
188
748
  try {
189
- // Try ProjectX API first for consistency
190
749
  let contracts = await getContractsFromAPI();
191
750
 
192
- // Fallback to service
193
751
  if (!contracts && typeof service.getContracts === 'function') {
194
752
  const result = await service.getContracts();
195
753
  if (result.success && result.contracts?.length > 0) {
@@ -205,27 +763,22 @@ const selectSymbol = async (service) => {
205
763
 
206
764
  spinner.succeed(`Found ${contracts.length} contracts`);
207
765
 
208
- // Build options from RAW API data - no static mapping
209
- const options = [];
210
- let currentGroup = null;
211
-
212
- for (const c of contracts) {
213
- // Use RAW API field: contractGroup
214
- if (c.contractGroup && c.contractGroup !== currentGroup) {
215
- currentGroup = c.contractGroup;
216
- options.push({
217
- label: chalk.cyan.bold(`── ${currentGroup} ──`),
218
- value: null,
219
- disabled: true,
220
- });
221
- }
766
+ // Sort by popular symbols first
767
+ const popular = ['ES', 'NQ', 'MES', 'MNQ', 'RTY', 'YM', 'CL', 'GC'];
768
+ contracts.sort((a, b) => {
769
+ const aIdx = popular.findIndex(p => (a.name || '').startsWith(p));
770
+ const bIdx = popular.findIndex(p => (b.name || '').startsWith(p));
771
+ if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
772
+ if (aIdx !== -1) return -1;
773
+ if (bIdx !== -1) return 1;
774
+ return (a.name || '').localeCompare(b.name || '');
775
+ });
222
776
 
223
- // Use RAW API fields: name (symbol), description (full name), exchange
224
- const label = ` ${c.name} - ${c.description} (${c.exchange})`;
225
- options.push({ label, value: c });
226
- }
777
+ const options = contracts.slice(0, 30).map(c => ({
778
+ label: `${c.name} - ${c.description || ''} (${c.exchange || 'CME'})`,
779
+ value: c
780
+ }));
227
781
 
228
- options.push({ label: '', value: null, disabled: true });
229
782
  options.push({ label: chalk.gray('< CANCEL'), value: null });
230
783
 
231
784
  return prompts.selectOption('TRADING SYMBOL:', options);
@@ -237,8 +790,7 @@ const selectSymbol = async (service) => {
237
790
  };
238
791
 
239
792
  /**
240
- * Get contracts from ProjectX API - RAW data only
241
- * @returns {Promise<Array|null>}
793
+ * Get contracts from ProjectX API
242
794
  */
243
795
  const getContractsFromAPI = async () => {
244
796
  const allConns = connections.getAll();
@@ -247,7 +799,6 @@ const getContractsFromAPI = async () => {
247
799
  if (projectxConn && typeof projectxConn.service.getContracts === 'function') {
248
800
  const result = await projectxConn.service.getContracts();
249
801
  if (result.success && result.contracts?.length > 0) {
250
- // Return RAW API data - no mapping
251
802
  return result.contracts;
252
803
  }
253
804
  }
@@ -257,24 +808,21 @@ const getContractsFromAPI = async () => {
257
808
 
258
809
  /**
259
810
  * Launch Copy Trading session
260
- * @param {Object} config - Session configuration
261
811
  */
262
812
  const launchCopyTrading = async (config) => {
263
- const { lead, follower, dailyTarget, maxRisk, showNames } = config;
264
-
265
- // Account names (masked for privacy)
266
- const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
267
- const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
813
+ const { lead, followers, dailyTarget, maxRisk, showNames } = config;
268
814
 
269
- const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
815
+ const leadName = showNames
816
+ ? (lead.account.accountName || lead.account.accountId)
817
+ : 'Lead *****';
818
+
819
+ const ui = new AlgoUI({ subtitle: 'COPY TRADING PRO', mode: 'copy-trading' });
270
820
 
271
821
  const stats = {
272
822
  leadName,
273
- followerName,
823
+ followerCount: followers.length,
274
824
  leadSymbol: lead.symbol.name,
275
- followerSymbol: follower.symbol.name,
276
825
  leadQty: lead.contracts,
277
- followerQty: follower.contracts,
278
826
  target: dailyTarget,
279
827
  risk: maxRisk,
280
828
  pnl: 0,
@@ -283,205 +831,96 @@ const launchCopyTrading = async (config) => {
283
831
  losses: 0,
284
832
  latency: 0,
285
833
  connected: false,
286
- platform: lead.account.platform || 'ProjectX',
834
+ platform: lead.type === 'rithmic' ? 'Rithmic' : 'ProjectX',
835
+ startTime: Date.now(),
836
+ aiSupervision: false,
837
+ aiMode: null
287
838
  };
288
839
 
289
- let running = true;
290
- let stopReason = null;
291
-
292
- // Measure API latency (CLI <-> API)
293
- const measureLatency = async () => {
294
- try {
295
- const start = Date.now();
296
- await lead.service.getPositions(lead.account.accountId);
297
- stats.latency = Date.now() - start;
298
- } catch (e) {
299
- stats.latency = 0;
300
- }
301
- };
840
+ // Initialize AI Supervisor
841
+ const aiAgents = aiService.getAgents();
842
+ if (aiAgents.length > 0) {
843
+ const supervisorResult = StrategySupervisor.initialize(null, aiAgents, lead.service, lead.account.accountId);
844
+ stats.aiSupervision = supervisorResult.success;
845
+ stats.aiMode = supervisorResult.mode;
846
+ }
302
847
 
303
- // Smart startup logs (same as HQX-TG)
848
+ // Startup logs
304
849
  const market = checkMarketHours();
305
850
  const sessionName = market.session || 'AMERICAN';
306
- const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
307
-
851
+ const etTime = new Date().toLocaleTimeString('en-US', {
852
+ hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York'
853
+ });
854
+
308
855
  algoLogger.connectingToEngine(ui, lead.account.accountId);
309
856
  algoLogger.engineStarting(ui, stats.platform, dailyTarget, maxRisk);
310
857
  algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
311
- algoLogger.info(ui, 'COPY MODE', `Lead: ${lead.propfirm} -> Follower: ${follower.propfirm}`);
312
- algoLogger.dataConnected(ui, 'API');
313
- algoLogger.algoOperational(ui, stats.platform);
314
- stats.connected = true;
315
-
316
- // Track lead positions and copy to follower
317
- let lastLeadPositions = [];
318
-
319
- const pollAndCopy = async () => {
320
- try {
321
- // Get lead positions
322
- const leadResult = await lead.service.getPositions(lead.account.accountId);
323
- if (!leadResult.success) return;
324
-
325
- const currentPositions = leadResult.positions || [];
326
-
327
- // Detect new positions on lead
328
- for (const pos of currentPositions) {
329
- const existing = lastLeadPositions.find(p => p.contractId === pos.contractId);
330
- if (!existing && pos.quantity !== 0) {
331
- // New position opened - copy to follower
332
- const side = pos.quantity > 0 ? 'LONG' : 'SHORT';
333
- const orderSide = pos.quantity > 0 ? 0 : 1; // 0=Buy, 1=Sell
334
- const symbol = pos.symbol || pos.contractId;
335
- const size = Math.abs(pos.quantity);
336
- const entry = pos.averagePrice || 0;
337
-
338
- algoLogger.positionOpened(ui, symbol, side, size, entry);
339
- algoLogger.info(ui, 'COPYING TO FOLLOWER', `${side} ${size}x ${symbol}`);
340
-
341
- // Place order on follower account
342
- try {
343
- const orderResult = await follower.service.placeOrder({
344
- accountId: follower.account.accountId,
345
- contractId: pos.contractId,
346
- type: 2, // Market order
347
- side: orderSide,
348
- size: follower.contracts
349
- });
350
-
351
- if (orderResult.success) {
352
- algoLogger.orderFilled(ui, symbol, side, follower.contracts, entry);
353
- algoLogger.info(ui, 'FOLLOWER ORDER', `${side} ${follower.contracts}x filled`);
354
- } else {
355
- algoLogger.orderRejected(ui, symbol, orderResult.error || 'Order failed');
356
- }
357
- } catch (err) {
358
- algoLogger.error(ui, 'FOLLOWER ORDER FAILED', err.message);
359
- }
360
- }
361
- }
362
-
363
- // Detect closed positions
364
- for (const oldPos of lastLeadPositions) {
365
- const stillOpen = currentPositions.find(p => p.contractId === oldPos.contractId);
366
- if (!stillOpen || stillOpen.quantity === 0) {
367
- const side = oldPos.quantity > 0 ? 'LONG' : 'SHORT';
368
- const closeSide = oldPos.quantity > 0 ? 1 : 0; // Opposite side to close
369
- const symbol = oldPos.symbol || oldPos.contractId;
370
- const size = Math.abs(oldPos.quantity);
371
- const exit = stillOpen?.averagePrice || oldPos.averagePrice || 0;
372
- const pnl = oldPos.profitAndLoss || 0;
373
-
374
- algoLogger.positionClosed(ui, symbol, side, size, exit, pnl);
375
- algoLogger.info(ui, 'CLOSING ON FOLLOWER', symbol);
376
-
377
- // Close position on follower account
378
- try {
379
- // First try closePosition API
380
- const closeResult = await follower.service.closePosition(
381
- follower.account.accountId,
382
- oldPos.contractId
383
- );
384
-
385
- if (closeResult.success) {
386
- algoLogger.info(ui, 'FOLLOWER CLOSED', `${symbol} position closed`);
387
- } else {
388
- // Fallback: place market order to close
389
- const orderResult = await follower.service.placeOrder({
390
- accountId: follower.account.accountId,
391
- contractId: oldPos.contractId,
392
- type: 2, // Market order
393
- side: closeSide,
394
- size: follower.contracts
395
- });
396
-
397
- if (orderResult.success) {
398
- algoLogger.info(ui, 'FOLLOWER CLOSED', `${symbol} via market order`);
399
- } else {
400
- algoLogger.error(ui, 'FOLLOWER CLOSE FAILED', orderResult.error || 'Close failed');
401
- }
402
- }
403
- } catch (err) {
404
- algoLogger.error(ui, 'FOLLOWER CLOSE ERROR', err.message);
405
- }
406
- }
407
- }
408
-
409
- lastLeadPositions = currentPositions;
410
-
411
- // Update P&L from lead
412
- const leadPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
413
- if (leadPnL !== stats.pnl) {
414
- const diff = leadPnL - stats.pnl;
415
- if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
416
- stats.trades++;
417
- if (diff >= 0) stats.wins++;
418
- else stats.losses++;
419
- }
420
- stats.pnl = leadPnL;
421
- }
422
-
423
- // Check target/risk limits
424
- if (stats.pnl >= dailyTarget) {
425
- stopReason = 'target';
426
- running = false;
427
- algoLogger.targetHit(ui, lead.symbol.name, 0, stats.pnl);
428
- algoLogger.info(ui, 'DAILY TARGET REACHED', `+$${stats.pnl.toFixed(2)} - Stopping algo`);
429
- } else if (stats.pnl <= -maxRisk) {
430
- stopReason = 'risk';
431
- running = false;
432
- algoLogger.dailyLimitWarning(ui, stats.pnl, -maxRisk);
433
- algoLogger.error(ui, 'MAX RISK HIT', `-$${Math.abs(stats.pnl).toFixed(2)} - Stopping algo`);
434
- }
435
- } catch (e) {
436
- // Silent fail - will retry
437
- }
438
- };
858
+ algoLogger.info(ui, 'COPY MODE', `Lead: ${lead.propfirm} -> ${followers.length} follower(s)`);
859
+
860
+ if (stats.aiSupervision) {
861
+ algoLogger.info(ui, 'AI SUPERVISION', `${aiAgents.length} agent(s) - LEARNING ACTIVE`);
862
+ }
863
+
864
+ // Create copy engine
865
+ const engine = new CopyEngine({
866
+ lead,
867
+ followers,
868
+ symbol: lead.symbol,
869
+ dailyTarget,
870
+ maxRisk,
871
+ ui,
872
+ stats
873
+ });
439
874
 
440
875
  // UI refresh loop
441
876
  const refreshInterval = setInterval(() => {
442
- if (running) ui.render(stats);
877
+ if (engine.running) ui.render(stats);
443
878
  }, 250);
444
-
445
- // Measure API latency every 5 seconds
446
- measureLatency(); // Initial measurement
447
- const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
448
-
449
- // Poll and copy every 2 seconds
450
- pollAndCopy(); // Initial poll
451
- const copyInterval = setInterval(() => { if (running) pollAndCopy(); }, 2000);
452
879
 
453
880
  // Keyboard handling
454
881
  const cleanupKeys = setupKeyboardHandler(() => {
455
- running = false;
456
- stopReason = 'manual';
882
+ engine.stop('manual');
457
883
  });
458
884
 
459
- // Wait for stop
460
- await new Promise((resolve) => {
461
- const check = setInterval(() => {
462
- if (!running) {
463
- clearInterval(check);
464
- resolve();
465
- }
466
- }, 100);
467
- });
885
+ // Start engine
886
+ algoLogger.dataConnected(ui, 'API');
887
+ algoLogger.algoOperational(ui, stats.platform);
888
+
889
+ const stopReason = await engine.start();
468
890
 
469
891
  // Cleanup
470
892
  clearInterval(refreshInterval);
471
- clearInterval(latencyInterval);
472
- clearInterval(copyInterval);
473
893
  if (cleanupKeys) cleanupKeys();
894
+
895
+ // Stop AI Supervisor
896
+ if (stats.aiSupervision) {
897
+ const aiSummary = StrategySupervisor.stop();
898
+ stats.aiLearning = {
899
+ optimizations: aiSummary.optimizationsApplied || 0,
900
+ patternsLearned: (aiSummary.winningPatterns || 0) + (aiSummary.losingPatterns || 0)
901
+ };
902
+ }
903
+
474
904
  ui.cleanup();
475
905
 
476
- // Show summary
906
+ // Duration
907
+ const durationMs = Date.now() - stats.startTime;
908
+ const hours = Math.floor(durationMs / 3600000);
909
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
910
+ const seconds = Math.floor((durationMs % 60000) / 1000);
911
+ stats.duration = hours > 0
912
+ ? `${hours}h ${minutes}m ${seconds}s`
913
+ : minutes > 0
914
+ ? `${minutes}m ${seconds}s`
915
+ : `${seconds}s`;
916
+
917
+ // Summary
477
918
  renderSessionSummary(stats, stopReason);
478
919
  await prompts.waitForEnter();
479
920
  };
480
921
 
481
922
  /**
482
923
  * Setup keyboard handler
483
- * @param {Function} onStop - Stop callback
484
- * @returns {Function|null} Cleanup function
485
924
  */
486
925
  const setupKeyboardHandler = (onStop) => {
487
926
  if (!process.stdin.isTTY) return null;