hedgequantx 2.6.160 → 2.6.162

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/menus/ai-agent-connect.js +181 -0
  3. package/src/menus/ai-agent-models.js +219 -0
  4. package/src/menus/ai-agent-oauth.js +292 -0
  5. package/src/menus/ai-agent-ui.js +141 -0
  6. package/src/menus/ai-agent.js +88 -1489
  7. package/src/pages/algo/copy-engine.js +449 -0
  8. package/src/pages/algo/copy-trading.js +11 -543
  9. package/src/pages/algo/smart-logs-data.js +218 -0
  10. package/src/pages/algo/smart-logs.js +9 -214
  11. package/src/pages/algo/ui-constants.js +144 -0
  12. package/src/pages/algo/ui-summary.js +184 -0
  13. package/src/pages/algo/ui.js +42 -526
  14. package/src/pages/stats-calculations.js +191 -0
  15. package/src/pages/stats-ui.js +381 -0
  16. package/src/pages/stats.js +14 -507
  17. package/src/services/ai/client-analysis.js +194 -0
  18. package/src/services/ai/client-models.js +333 -0
  19. package/src/services/ai/client.js +6 -489
  20. package/src/services/ai/index.js +2 -257
  21. package/src/services/ai/proxy-install.js +249 -0
  22. package/src/services/ai/proxy-manager.js +29 -411
  23. package/src/services/ai/proxy-remote.js +161 -0
  24. package/src/services/ai/strategy-supervisor.js +10 -765
  25. package/src/services/ai/supervisor-data.js +195 -0
  26. package/src/services/ai/supervisor-optimize.js +215 -0
  27. package/src/services/ai/supervisor-sync.js +178 -0
  28. package/src/services/ai/supervisor-utils.js +158 -0
  29. package/src/services/ai/supervisor.js +50 -515
  30. package/src/services/ai/validation.js +250 -0
  31. package/src/services/hqx-server-events.js +110 -0
  32. package/src/services/hqx-server-handlers.js +217 -0
  33. package/src/services/hqx-server-latency.js +136 -0
  34. package/src/services/hqx-server.js +51 -403
  35. package/src/services/position-constants.js +28 -0
  36. package/src/services/position-manager.js +105 -554
  37. package/src/services/position-momentum.js +206 -0
  38. package/src/services/projectx/accounts.js +142 -0
  39. package/src/services/projectx/index.js +40 -289
  40. package/src/services/projectx/trading.js +180 -0
  41. package/src/services/rithmic/handlers.js +2 -208
  42. package/src/services/rithmic/index.js +32 -542
  43. package/src/services/rithmic/latency-tracker.js +182 -0
  44. package/src/services/rithmic/specs.js +146 -0
  45. package/src/services/rithmic/trade-history.js +254 -0
@@ -1,15 +1,8 @@
1
1
  /**
2
- * @fileoverview Professional Copy Trading System
2
+ * @fileoverview Professional Copy Trading Menu
3
3
  * @module pages/algo/copy-trading
4
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)
5
+ * Copy trading configuration and session management
13
6
  */
14
7
 
15
8
  const chalk = require('chalk');
@@ -21,6 +14,7 @@ const { AlgoUI, renderSessionSummary } = require('./ui');
21
14
  const { logger, prompts } = require('../../utils');
22
15
  const { checkMarketHours } = require('../../services/projectx/market');
23
16
  const { algoLogger } = require('./logger');
17
+ const { CopyEngine } = require('./copy-engine');
24
18
 
25
19
  // AI Strategy Supervisor
26
20
  const aiService = require('../../services/ai');
@@ -28,531 +22,6 @@ const StrategySupervisor = require('../../services/ai/strategy-supervisor');
28
22
 
29
23
  const log = logger.scope('CopyTrading');
30
24
 
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
- this.lastLogTime = 0;
68
- this.positionEntryTime = null;
69
-
70
- // Retry configuration
71
- this.maxRetries = 3;
72
- this.retryDelayBase = 100; // ms
73
-
74
- // Slippage protection (ticks)
75
- this.maxSlippageTicks = 4;
76
- }
77
-
78
- /**
79
- * Get unique position key (cross-platform compatible)
80
- */
81
- getPositionKey(position) {
82
- return position.contractId || position.symbol || position.id;
83
- }
84
-
85
- /**
86
- * Resolve symbol for target platform
87
- */
88
- resolveSymbol(position, targetAccount) {
89
- const targetType = targetAccount.type;
90
-
91
- if (targetType === 'rithmic') {
92
- return {
93
- symbol: position.symbol || this.symbol.name,
94
- exchange: position.exchange || this.symbol.exchange || 'CME',
95
- contractId: null
96
- };
97
- } else {
98
- return {
99
- contractId: position.contractId || this.symbol.id || this.symbol.contractId,
100
- symbol: null,
101
- exchange: null
102
- };
103
- }
104
- }
105
-
106
- /**
107
- * Build order data for specific platform
108
- */
109
- buildOrderData(params, platformType) {
110
- const { accountId, contractId, symbol, exchange, side, size, type, price } = params;
111
-
112
- if (platformType === 'rithmic') {
113
- return {
114
- accountId,
115
- symbol,
116
- exchange: exchange || 'CME',
117
- size,
118
- side,
119
- type,
120
- price: price || 0
121
- };
122
- } else {
123
- return {
124
- accountId,
125
- contractId,
126
- type,
127
- side,
128
- size
129
- };
130
- }
131
- }
132
-
133
- /**
134
- * Execute order with retry logic
135
- */
136
- async executeOrderWithRetry(follower, orderData, retryCount = 0) {
137
- try {
138
- const startTime = Date.now();
139
- const result = await follower.service.placeOrder(orderData);
140
- const latency = Date.now() - startTime;
141
-
142
- if (result.success) {
143
- this.orderCount++;
144
- this.stats.latency = Math.round((this.stats.latency + latency) / 2);
145
- return { success: true, latency };
146
- }
147
-
148
- // Retry on failure
149
- if (retryCount < this.maxRetries) {
150
- const delay = this.retryDelayBase * Math.pow(2, retryCount);
151
- await this.sleep(delay);
152
- return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
153
- }
154
-
155
- this.failedOrders++;
156
- return { success: false, error: result.error || 'Max retries exceeded' };
157
- } catch (err) {
158
- if (retryCount < this.maxRetries) {
159
- const delay = this.retryDelayBase * Math.pow(2, retryCount);
160
- await this.sleep(delay);
161
- return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
162
- }
163
-
164
- this.failedOrders++;
165
- return { success: false, error: err.message };
166
- }
167
- }
168
-
169
- /**
170
- * Queue order for a follower (ensures sequential execution per follower)
171
- */
172
- async queueOrder(followerIdx, orderFn) {
173
- if (!this.orderQueues.has(followerIdx)) {
174
- this.orderQueues.set(followerIdx, []);
175
- }
176
-
177
- return new Promise((resolve) => {
178
- this.orderQueues.get(followerIdx).push({ fn: orderFn, resolve });
179
- this.processQueue(followerIdx);
180
- });
181
- }
182
-
183
- /**
184
- * Process order queue for a follower
185
- */
186
- async processQueue(followerIdx) {
187
- if (this.processingQueue.get(followerIdx)) return;
188
-
189
- const queue = this.orderQueues.get(followerIdx);
190
- if (!queue || queue.length === 0) return;
191
-
192
- this.processingQueue.set(followerIdx, true);
193
-
194
- while (queue.length > 0 && this.running) {
195
- const { fn, resolve } = queue.shift();
196
- try {
197
- const result = await fn();
198
- resolve(result);
199
- } catch (err) {
200
- resolve({ success: false, error: err.message });
201
- }
202
- }
203
-
204
- this.processingQueue.set(followerIdx, false);
205
- }
206
-
207
- /**
208
- * Copy position open to all followers (parallel execution)
209
- */
210
- async copyPositionOpen(position) {
211
- const side = position.quantity > 0 ? 'LONG' : 'SHORT';
212
- const orderSide = position.quantity > 0 ? 0 : 1;
213
- const displaySymbol = position.symbol || this.symbol.name;
214
- const size = Math.abs(position.quantity);
215
- const entry = position.averagePrice || 0;
216
-
217
- // Track entry time for smart logs
218
- this.positionEntryTime = Date.now();
219
-
220
- algoLogger.positionOpened(this.ui, displaySymbol, side, size, entry);
221
-
222
- // Feed to AI supervisor
223
- if (this.stats.aiSupervision) {
224
- StrategySupervisor.feedSignal({
225
- direction: side.toLowerCase(),
226
- entry,
227
- stopLoss: null,
228
- takeProfit: null,
229
- confidence: 0.5
230
- });
231
- }
232
-
233
- // Execute on all followers in parallel
234
- const promises = this.followers.map((follower, idx) => {
235
- return this.queueOrder(idx, async () => {
236
- const resolved = this.resolveSymbol(position, follower);
237
- const orderData = this.buildOrderData({
238
- accountId: follower.account.accountId,
239
- contractId: resolved.contractId,
240
- symbol: resolved.symbol,
241
- exchange: resolved.exchange,
242
- side: orderSide,
243
- size: follower.contracts,
244
- type: 2 // Market
245
- }, follower.type);
246
-
247
- algoLogger.info(this.ui, 'COPY ORDER', `${side} ${follower.contracts}x -> ${follower.propfirm}`);
248
-
249
- const result = await this.executeOrderWithRetry(follower, orderData);
250
-
251
- if (result.success) {
252
- algoLogger.orderFilled(this.ui, displaySymbol, side, follower.contracts, entry);
253
-
254
- // Track follower position
255
- const posKey = this.getPositionKey(position);
256
- this.followerPositions.set(`${idx}:${posKey}`, {
257
- ...position,
258
- followerIdx: idx,
259
- openTime: Date.now()
260
- });
261
- } else {
262
- algoLogger.orderRejected(this.ui, displaySymbol, result.error);
263
- }
264
-
265
- return result;
266
- });
267
- });
268
-
269
- const results = await Promise.all(promises);
270
- const successCount = results.filter(r => r.success).length;
271
-
272
- if (successCount === this.followers.length) {
273
- algoLogger.info(this.ui, 'ALL COPIED', `${successCount}/${this.followers.length} followers`);
274
- } else if (successCount > 0) {
275
- algoLogger.info(this.ui, 'PARTIAL COPY', `${successCount}/${this.followers.length} followers`);
276
- }
277
-
278
- return results;
279
- }
280
-
281
- /**
282
- * Copy position close to all followers (parallel execution)
283
- */
284
- async copyPositionClose(position, exitPrice, pnl) {
285
- const side = position.quantity > 0 ? 'LONG' : 'SHORT';
286
- const closeSide = position.quantity > 0 ? 1 : 0;
287
- const displaySymbol = position.symbol || this.symbol.name;
288
- const size = Math.abs(position.quantity);
289
-
290
- // Reset entry time
291
- this.positionEntryTime = null;
292
-
293
- algoLogger.positionClosed(this.ui, displaySymbol, side, size, exitPrice, pnl);
294
-
295
- // Feed to AI supervisor
296
- if (this.stats.aiSupervision) {
297
- StrategySupervisor.feedTradeResult({
298
- side,
299
- qty: size,
300
- price: exitPrice,
301
- pnl,
302
- symbol: displaySymbol,
303
- direction: side
304
- });
305
-
306
- const aiStatus = StrategySupervisor.getStatus();
307
- if (aiStatus.patternsLearned.winning + aiStatus.patternsLearned.losing > 0) {
308
- algoLogger.info(this.ui, 'AI LEARNING',
309
- `${aiStatus.patternsLearned.winning}W/${aiStatus.patternsLearned.losing}L patterns`);
310
- }
311
- }
312
-
313
- // Close on all followers in parallel
314
- const posKey = this.getPositionKey(position);
315
-
316
- const promises = this.followers.map((follower, idx) => {
317
- return this.queueOrder(idx, async () => {
318
- const resolved = this.resolveSymbol(position, follower);
319
- const posIdentifier = follower.type === 'rithmic'
320
- ? (position.symbol || this.symbol.name)
321
- : (position.contractId || this.symbol.id);
322
-
323
- algoLogger.info(this.ui, 'CLOSE ORDER', `${displaySymbol} -> ${follower.propfirm}`);
324
-
325
- // Try closePosition first
326
- let result = await follower.service.closePosition(
327
- follower.account.accountId,
328
- posIdentifier
329
- );
330
-
331
- if (!result.success) {
332
- // Fallback: market order
333
- const orderData = this.buildOrderData({
334
- accountId: follower.account.accountId,
335
- contractId: resolved.contractId,
336
- symbol: resolved.symbol,
337
- exchange: resolved.exchange,
338
- side: closeSide,
339
- size: follower.contracts,
340
- type: 2
341
- }, follower.type);
342
-
343
- result = await this.executeOrderWithRetry(follower, orderData);
344
- }
345
-
346
- if (result.success) {
347
- algoLogger.info(this.ui, 'CLOSED', `${displaySymbol} on ${follower.propfirm}`);
348
- this.followerPositions.delete(`${idx}:${posKey}`);
349
- } else {
350
- algoLogger.error(this.ui, 'CLOSE FAILED', `${follower.propfirm}: ${result.error}`);
351
- }
352
-
353
- return result;
354
- });
355
- });
356
-
357
- const results = await Promise.all(promises);
358
- const successCount = results.filter(r => r.success).length;
359
-
360
- if (successCount === this.followers.length) {
361
- this.stats.trades++;
362
- this.stats.sessionPnl += pnl; // Track session P&L
363
- if (pnl >= 0) this.stats.wins++;
364
- else this.stats.losses++;
365
- }
366
-
367
- return results;
368
- }
369
-
370
- /**
371
- * Poll lead positions and detect changes
372
- */
373
- async pollLeadPositions() {
374
- if (!this.running) return;
375
-
376
- const startTime = Date.now();
377
-
378
- try {
379
- const result = await this.lead.service.getPositions(this.lead.account.accountId);
380
- if (!result.success) return;
381
-
382
- const currentPositions = result.positions || [];
383
- const currentMap = new Map();
384
-
385
- // Build current positions map
386
- for (const pos of currentPositions) {
387
- if (pos.quantity === 0) continue;
388
- const key = this.getPositionKey(pos);
389
- currentMap.set(key, pos);
390
- }
391
-
392
- // Detect new positions (opened)
393
- for (const [key, pos] of currentMap) {
394
- if (!this.leadPositions.has(key)) {
395
- // New position - copy to followers
396
- await this.copyPositionOpen(pos);
397
- this.leadPositions.set(key, pos);
398
- } else {
399
- // Position exists - check for size change (scaling)
400
- const oldPos = this.leadPositions.get(key);
401
- if (Math.abs(pos.quantity) !== Math.abs(oldPos.quantity)) {
402
- // Size changed - update tracked position (scaling in/out)
403
- this.leadPositions.set(key, pos);
404
- }
405
- }
406
- }
407
-
408
- // Detect closed positions
409
- for (const [key, oldPos] of this.leadPositions) {
410
- if (!currentMap.has(key)) {
411
- // Position closed - close on followers
412
- const exitPrice = oldPos.averagePrice || 0;
413
- const pnl = oldPos.profitAndLoss || 0;
414
- await this.copyPositionClose(oldPos, exitPrice, pnl);
415
- this.leadPositions.delete(key);
416
- }
417
- }
418
-
419
- // Update P&L from current positions
420
- const totalPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
421
- this.stats.pnl = totalPnL;
422
-
423
- // Check limits
424
- if (totalPnL >= this.dailyTarget) {
425
- this.stop('target');
426
- algoLogger.info(this.ui, 'TARGET REACHED', `+$${totalPnL.toFixed(2)}`);
427
- } else if (totalPnL <= -this.maxRisk) {
428
- this.stop('risk');
429
- algoLogger.error(this.ui, 'MAX RISK HIT', `-$${Math.abs(totalPnL).toFixed(2)}`);
430
- }
431
-
432
- // Adaptive polling - faster when positions are open
433
- const pollTime = Date.now() - startTime;
434
- this.stats.latency = pollTime;
435
-
436
- if (this.leadPositions.size > 0) {
437
- this.pollInterval = Math.max(100, Math.min(250, pollTime * 2));
438
- } else {
439
- this.pollInterval = Math.max(250, Math.min(500, pollTime * 3));
440
- }
441
-
442
- this.pollCount++;
443
-
444
- // Smart logs - only on STATE CHANGES (not every second when in position)
445
- const now = Date.now();
446
- if (now - this.lastLogTime > 1000) {
447
- const smartLogs = require('./smart-logs');
448
-
449
- if (this.leadPositions.size === 0) {
450
- // Not in position - show market analysis (varied messages)
451
- // Use scanning log since copy trading doesn't have strategy model values
452
- const scanLog = smartLogs.getScanningLog(true);
453
- this.ui.addLog('info', `${scanLog.message} poll #${this.pollCount} | ${pollTime}ms`);
454
- }
455
- // When IN POSITION: Don't spam logs every second
456
- // Position updates come from order fills and exit events (copyPositionOpen/copyPositionClose)
457
- this.lastLogTime = now;
458
- }
459
-
460
- } catch (err) {
461
- log.warn('Poll error', { error: err.message });
462
- }
463
- }
464
-
465
- /**
466
- * Reconcile follower positions with lead
467
- */
468
- async reconcilePositions() {
469
- // Get all follower positions and compare with lead
470
- for (let idx = 0; idx < this.followers.length; idx++) {
471
- const follower = this.followers[idx];
472
-
473
- try {
474
- const result = await follower.service.getPositions(follower.account.accountId);
475
- if (!result.success) continue;
476
-
477
- const followerPositions = result.positions || [];
478
-
479
- // Check each lead position has corresponding follower position
480
- for (const [key, leadPos] of this.leadPositions) {
481
- const hasFollowerPos = followerPositions.some(fp => {
482
- const fpKey = this.getPositionKey(fp);
483
- return fpKey === key && fp.quantity !== 0;
484
- });
485
-
486
- if (!hasFollowerPos) {
487
- // Missing position on follower - need to open
488
- algoLogger.info(this.ui, 'RECONCILE', `Missing ${key} on ${follower.propfirm}`);
489
- await this.copyPositionOpen(leadPos);
490
- }
491
- }
492
-
493
- // Check for orphaned follower positions (position on follower but not on lead)
494
- for (const fp of followerPositions) {
495
- if (fp.quantity === 0) continue;
496
- const fpKey = this.getPositionKey(fp);
497
-
498
- if (!this.leadPositions.has(fpKey)) {
499
- // Orphaned position - close it
500
- algoLogger.info(this.ui, 'RECONCILE', `Orphaned ${fpKey} on ${follower.propfirm}`);
501
-
502
- const posIdentifier = follower.type === 'rithmic' ? fp.symbol : fp.contractId;
503
- await follower.service.closePosition(follower.account.accountId, posIdentifier);
504
- }
505
- }
506
-
507
- } catch (err) {
508
- log.warn('Reconcile error', { follower: follower.propfirm, error: err.message });
509
- }
510
- }
511
- }
512
-
513
- /**
514
- * Start the copy engine
515
- */
516
- async start() {
517
- this.running = true;
518
- this.stats.connected = true;
519
-
520
- algoLogger.info(this.ui, 'ENGINE STARTED', `Polling every ${this.pollInterval}ms`);
521
- algoLogger.info(this.ui, 'FOLLOWERS', `${this.followers.length} account(s)`);
522
-
523
- // Initial reconciliation
524
- await this.reconcilePositions();
525
-
526
- // Main polling loop
527
- while (this.running) {
528
- await this.pollLeadPositions();
529
- await this.sleep(this.pollInterval);
530
- }
531
-
532
- return this.stopReason;
533
- }
534
-
535
- /**
536
- * Stop the copy engine
537
- */
538
- stop(reason = 'manual') {
539
- this.running = false;
540
- this.stopReason = reason;
541
- this.stats.connected = false;
542
- }
543
-
544
- /**
545
- * Sleep utility
546
- */
547
- sleep(ms) {
548
- return new Promise(resolve => setTimeout(resolve, ms));
549
- }
550
- }
551
-
552
- // ============================================================================
553
- // COPY TRADING MENU
554
- // ============================================================================
555
-
556
25
  /**
557
26
  * Copy Trading Menu
558
27
  */
@@ -608,14 +77,14 @@ const copyTradingMenu = async () => {
608
77
  followers.length === 0 ? 'FOLLOWER ACCOUNT:' : 'ADD ANOTHER FOLLOWER:',
609
78
  allAccounts,
610
79
  excludeIndices,
611
- followers.length > 0 // Allow skip if at least one follower
80
+ followers.length > 0
612
81
  );
613
82
 
614
83
  if (followerIdx === null || followerIdx === -1) {
615
- if (followers.length === 0) return; // Cancel
84
+ if (followers.length === 0) return;
616
85
  selectingFollowers = false;
617
86
  } else if (followerIdx === -2) {
618
- selectingFollowers = false; // Done adding
87
+ selectingFollowers = false;
619
88
  } else {
620
89
  followers.push(allAccounts[followerIdx]);
621
90
  excludeIndices.push(followerIdx);
@@ -725,7 +194,6 @@ const copyTradingMenu = async () => {
725
194
  const fetchAllAccounts = async (allConns) => {
726
195
  const allAccounts = [];
727
196
 
728
- // Fetch in parallel
729
197
  const promises = allConns.map(async (conn) => {
730
198
  try {
731
199
  const result = await conn.service.getTradingAccounts();
@@ -813,7 +281,6 @@ const selectSymbol = async (service) => {
813
281
  return (a.name || '').localeCompare(b.name || '');
814
282
  });
815
283
 
816
- // Display contracts (uniform format: NAME - DESCRIPTION)
817
284
  const options = contracts.slice(0, 30).map(c => {
818
285
  const name = c.name || c.symbol || c.baseSymbol;
819
286
  const desc = c.description || '';
@@ -868,7 +335,7 @@ const launchCopyTrading = async (config) => {
868
335
  target: dailyTarget,
869
336
  risk: maxRisk,
870
337
  pnl: 0,
871
- sessionPnl: 0, // P&L from THIS session only (HQX trades)
338
+ sessionPnl: 0,
872
339
  trades: 0,
873
340
  wins: 0,
874
341
  losses: 0,
@@ -878,10 +345,10 @@ const launchCopyTrading = async (config) => {
878
345
  startTime: Date.now(),
879
346
  aiSupervision: false,
880
347
  aiMode: null,
881
- agentCount: 0 // Number of AI agents active
348
+ agentCount: 0
882
349
  };
883
350
 
884
- // Initialize AI Supervisor - only if user enabled it
351
+ // Initialize AI Supervisor
885
352
  if (enableAI) {
886
353
  const aiAgents = aiService.getAgents();
887
354
  stats.agentCount = aiAgents.length;
@@ -905,6 +372,7 @@ const launchCopyTrading = async (config) => {
905
372
  algoLogger.info(ui, 'COPY MODE', `Lead: ${lead.propfirm} -> ${followers.length} follower(s)`);
906
373
 
907
374
  if (stats.aiSupervision) {
375
+ const aiAgents = aiService.getAgents();
908
376
  algoLogger.info(ui, 'AI SUPERVISION', `${aiAgents.length} agent(s) - LEARNING ACTIVE`);
909
377
  }
910
378
 
@@ -959,7 +427,7 @@ const launchCopyTrading = async (config) => {
959
427
  ? `${minutes}m ${seconds}s`
960
428
  : `${seconds}s`;
961
429
 
962
- // Close log file with session summary
430
+ // Close log file
963
431
  try { ui.closeLog(stats); } catch {}
964
432
 
965
433
  ui.cleanup();