hedgequantx 2.6.161 → 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 (42) 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/supervisor-optimize.js +215 -0
  25. package/src/services/ai/supervisor-sync.js +178 -0
  26. package/src/services/ai/supervisor.js +50 -515
  27. package/src/services/ai/validation.js +250 -0
  28. package/src/services/hqx-server-events.js +110 -0
  29. package/src/services/hqx-server-handlers.js +217 -0
  30. package/src/services/hqx-server-latency.js +136 -0
  31. package/src/services/hqx-server.js +51 -403
  32. package/src/services/position-constants.js +28 -0
  33. package/src/services/position-manager.js +105 -554
  34. package/src/services/position-momentum.js +206 -0
  35. package/src/services/projectx/accounts.js +142 -0
  36. package/src/services/projectx/index.js +40 -289
  37. package/src/services/projectx/trading.js +180 -0
  38. package/src/services/rithmic/handlers.js +2 -208
  39. package/src/services/rithmic/index.js +32 -542
  40. package/src/services/rithmic/latency-tracker.js +182 -0
  41. package/src/services/rithmic/specs.js +146 -0
  42. package/src/services/rithmic/trade-history.js +254 -0
@@ -9,14 +9,15 @@
9
9
 
10
10
  const chalk = require('chalk');
11
11
  const ora = require('ora');
12
- const asciichart = require('asciichart');
13
12
 
14
13
  const { connections } = require('../services');
15
- const { getLogoWidth, visibleLength, drawBoxHeader, drawBoxFooter, getColWidths, draw2ColHeader, draw2ColSeparator, fmtRow } = require('../ui');
14
+ const { getLogoWidth, drawBoxHeader, drawBoxFooter, getColWidths, draw2ColHeader, draw2ColSeparator, fmtRow } = require('../ui');
16
15
  const { prompts } = require('../utils');
17
16
  const aiService = require('../services/ai');
18
17
  const AISupervisor = require('../services/ai/supervisor');
19
18
  const StrategySupervisor = require('../services/ai/strategy-supervisor');
19
+ const { calculateTradeStats, calculateQuantMetrics, calculateHQXScore, extractSymbol } = require('./stats-calculations');
20
+ const { renderAISupervision, renderAIBehavior, renderEquityCurve, renderTradesHistory, renderHQXScore } = require('./stats-ui');
20
21
 
21
22
  /**
22
23
  * Show Stats Page
@@ -191,73 +192,14 @@ const showStats = async (service) => {
191
192
  }
192
193
 
193
194
  // ========== AGGREGATE STATS FROM TRADE HISTORY (API DATA) ==========
194
- // Calculate stats from COMPLETED trades only (those with P&L != 0)
195
- // This matches what we display in TRADES HISTORY
196
-
197
- let stats = {
198
- totalTrades: 0, winningTrades: 0, losingTrades: 0,
199
- totalWinAmount: 0, totalLossAmount: 0,
200
- bestTrade: 0, worstTrade: 0, totalVolume: 0,
201
- maxConsecutiveWins: 0, maxConsecutiveLosses: 0,
202
- longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
203
- };
204
-
205
195
  // Filter to completed trades only (P&L != 0, not null)
206
196
  const completedTrades = allTrades.filter(t => {
207
197
  const pnl = t.profitAndLoss || t.pnl;
208
198
  return pnl !== null && pnl !== undefined && pnl !== 0;
209
199
  });
210
200
 
211
- // Calculate stats from completed trades only
212
- // Include fees in P&L calculations to match TopStep
213
- if (completedTrades.length > 0) {
214
- stats.totalTrades = completedTrades.length;
215
- let consecutiveWins = 0, consecutiveLosses = 0;
216
-
217
- // Sort by time for consecutive win/loss calculation
218
- const sortedTrades = [...completedTrades].sort((a, b) => {
219
- const timeA = new Date(a.creationTimestamp || a.timestamp || 0).getTime();
220
- const timeB = new Date(b.creationTimestamp || b.timestamp || 0).getTime();
221
- return timeA - timeB;
222
- });
223
-
224
- for (const trade of sortedTrades) {
225
- const grossPnl = trade.profitAndLoss || trade.pnl || 0;
226
- const fees = Math.abs(trade.fees || trade.commission || 0);
227
- const netPnl = grossPnl - fees; // Net P&L after fees (like TopStep)
228
- const size = trade.size || trade.quantity || 1;
229
- const exitSide = trade.side; // 0=BUY exit (was SHORT), 1=SELL exit (was LONG)
230
-
231
- stats.totalVolume += Math.abs(size);
232
-
233
- // Determine original trade direction from exit side
234
- // Exit side 0 = BUY to close = was SHORT
235
- // Exit side 1 = SELL to close = was LONG
236
- if (exitSide === 1) {
237
- stats.longTrades++;
238
- if (netPnl > 0) stats.longWins++;
239
- } else if (exitSide === 0) {
240
- stats.shortTrades++;
241
- if (netPnl > 0) stats.shortWins++;
242
- }
243
-
244
- if (netPnl > 0) {
245
- stats.winningTrades++;
246
- stats.totalWinAmount += netPnl;
247
- consecutiveWins++;
248
- consecutiveLosses = 0;
249
- if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
250
- if (netPnl > stats.bestTrade) stats.bestTrade = netPnl;
251
- } else if (netPnl < 0) {
252
- stats.losingTrades++;
253
- stats.totalLossAmount += Math.abs(netPnl);
254
- consecutiveLosses++;
255
- consecutiveWins = 0;
256
- if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
257
- if (netPnl < stats.worstTrade) stats.worstTrade = netPnl;
258
- }
259
- }
260
- }
201
+ // Calculate stats from completed trades (uses stats-calculations.js)
202
+ const stats = calculateTradeStats(completedTrades);
261
203
 
262
204
  spinner.succeed('STATS LOADED');
263
205
  console.log();
@@ -278,43 +220,9 @@ const showStats = async (service) => {
278
220
  const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : 'N/A';
279
221
  const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : 'N/A';
280
222
 
281
- // Quantitative metrics (calculated from completed trades only, with fees)
282
- const tradePnLs = completedTrades.map(t => {
283
- const grossPnl = t.profitAndLoss || t.pnl || 0;
284
- const fees = Math.abs(t.fees || t.commission || 0);
285
- return grossPnl - fees; // Net P&L
286
- });
287
- const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
288
-
289
- // Standard deviation
290
- const variance = tradePnLs.length > 0
291
- ? tradePnLs.reduce((sum, pnl) => sum + Math.pow(pnl - avgReturn, 2), 0) / tradePnLs.length
292
- : 0;
293
- const stdDev = Math.sqrt(variance);
294
-
295
- // Downside deviation
296
- const downsideReturns = tradePnLs.filter(pnl => pnl < 0);
297
- const downsideVariance = downsideReturns.length > 0
298
- ? downsideReturns.reduce((sum, pnl) => sum + Math.pow(pnl, 2), 0) / downsideReturns.length
299
- : 0;
300
- const downsideDev = Math.sqrt(downsideVariance);
301
-
302
- // Ratios
303
- const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : 'N/A';
304
- const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : 'N/A';
305
-
306
- // Max Drawdown
307
- let maxDrawdown = 0;
308
- let peak = totalStartingBalance || 0;
309
- let equity = peak;
310
- if (peak > 0 && tradePnLs.length > 0) {
311
- tradePnLs.forEach(pnl => {
312
- equity += pnl;
313
- if (equity > peak) peak = equity;
314
- const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
315
- if (drawdown > maxDrawdown) maxDrawdown = drawdown;
316
- });
317
- }
223
+ // Quantitative metrics (uses stats-calculations.js)
224
+ const quantMetrics = calculateQuantMetrics(completedTrades, totalStartingBalance);
225
+ const { avgReturn, stdDev, sharpeRatio, sortinoRatio, maxDrawdown, tradePnLs } = quantMetrics;
318
226
 
319
227
  const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
320
228
  const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : 'N/A';
@@ -393,424 +301,23 @@ const showStats = async (service) => {
393
301
 
394
302
  if (aiAgents.length > 0) {
395
303
  console.log();
396
- drawBoxHeader('AI SUPERVISION', boxWidth);
397
- draw2ColHeader('AGENTS', 'PERFORMANCE', boxWidth);
398
-
399
- // Agent mode - INDIVIDUAL (1 agent) or CONSENSUS (2+ agents)
400
- const isConsensusMode = aiAgents.length >= 2;
401
- const agentMode = isConsensusMode ? 'CONSENSUS' : 'INDIVIDUAL';
402
- const modeColor = isConsensusMode ? chalk.magenta : chalk.cyan;
403
-
404
- // Get consensus data if in consensus mode
405
- const consensusData = isConsensusMode ? AISupervisor.getConsensus() : null;
406
-
407
- // Supervision metrics
408
- let totalDecisions = 0;
409
- let totalInterventions = 0;
410
- let totalOptimizations = 0;
411
- let totalRiskWarnings = 0;
412
- let totalSessionTime = 0;
413
-
414
- for (const status of supervisionStatus) {
415
- if (status.active) {
416
- totalDecisions += status.metrics?.totalDecisions || 0;
417
- totalInterventions += status.metrics?.interventions || 0;
418
- totalOptimizations += status.metrics?.optimizations || 0;
419
- totalRiskWarnings += status.metrics?.riskWarnings || 0;
420
- totalSessionTime += status.duration || 0;
421
- }
422
- }
423
-
424
- const sessionTimeStr = totalSessionTime > 0
425
- ? Math.floor(totalSessionTime / 60000) + 'm ' + Math.floor((totalSessionTime % 60000) / 1000) + 's'
426
- : 'INACTIVE';
427
-
428
- // Get real supervision data
429
- const supervisionData = AISupervisor.getAggregatedData();
430
- const supervisedAccounts = supervisionData.totalAccounts;
431
- const supervisedPnL = supervisionData.totalPnL;
432
-
433
- // Calculate max agent name length to fit in column (label=18 + space=1 + padding buffer)
434
- const maxAgentNameLen = col1 - 20;
435
-
436
- // Performance column data (right side)
437
- const perfData = [
438
- { label: 'SUPERVISED ACCOUNTS:', value: chalk.white(String(supervisedAccounts)) },
439
- { label: 'SUPERVISED P&L:', value: supervisedPnL >= 0 ? chalk.green('$' + supervisedPnL.toFixed(2)) : chalk.red('$' + supervisedPnL.toFixed(2)) },
440
- { label: 'POSITIONS:', value: chalk.white(String(supervisionData.totalPositions)) },
441
- { label: 'OPEN ORDERS:', value: chalk.white(String(supervisionData.totalOrders)) },
442
- { label: 'TRADES TODAY:', value: chalk.white(String(supervisionData.totalTrades)) }
443
- ];
444
-
445
- // Agents column data (left side) - each agent on its own line with ● indicator
446
- const agentsData = [
447
- { label: 'CONNECTED:', value: chalk.green(String(aiAgents.length) + ' AGENT' + (aiAgents.length > 1 ? 'S' : '')) },
448
- { label: 'MODE:', value: modeColor(agentMode) },
449
- { label: 'SESSION:', value: sessionTimeStr === 'INACTIVE' ? chalk.yellow(sessionTimeStr) : chalk.white(sessionTimeStr) }
450
- ];
451
-
452
- // Add consensus info if in consensus mode
453
- if (isConsensusMode && consensusData) {
454
- const isUnanimous = consensusData.isUnanimous;
455
- const consensusAction = consensusData.action || 'PENDING';
456
- // Show action with unanimity status
457
- const consensusDisplay = isUnanimous
458
- ? chalk.green(consensusAction + ' (UNANIMOUS)')
459
- : chalk.yellow(consensusAction + ' (DISAGREEMENT)');
460
- agentsData.push({ label: 'DECISION:', value: consensusDisplay });
461
- } else if (isConsensusMode) {
462
- agentsData.push({ label: 'DECISION:', value: chalk.white('WAITING...') });
463
- }
464
-
465
- // Add each agent as a separate line with ● indicator
466
- aiAgents.forEach((agent, idx) => {
467
- const agentLabel = idx === 0 ? 'AGENTS:' : '';
468
- const agentName = agent.name.length > maxAgentNameLen
469
- ? agent.name.substring(0, maxAgentNameLen - 4) + '..'
470
- : agent.name;
471
- const agentDisplay = chalk.green('● ') + chalk.white(agentName);
472
- agentsData.push({ label: agentLabel, value: agentDisplay });
473
- });
474
-
475
- // Print rows - match left and right columns
476
- const maxRows = Math.max(agentsData.length, perfData.length);
477
- for (let i = 0; i < maxRows; i++) {
478
- const leftData = agentsData[i] || { label: '', value: '' };
479
- const rightData = perfData[i] || { label: '', value: '' };
480
- console.log(chalk.cyan('\u2551') + fmtRow(leftData.label, leftData.value, col1) + chalk.cyan('\u2502') + fmtRow(rightData.label, rightData.value, col2) + chalk.cyan('\u2551'));
481
- }
482
-
483
- drawBoxFooter(boxWidth);
484
-
485
- // ========== AI BEHAVIOR DIAGRAM (HORIZONTAL BARS) ==========
486
- const behaviorData = StrategySupervisor.getBehaviorHistory(100);
487
- const learningStats = StrategySupervisor.getLearningStats();
488
-
489
- console.log();
490
- drawBoxHeader('AI AGENTS BEHAVIOR', boxWidth);
491
-
492
- const behaviorInnerWidth = boxWidth - 2;
493
-
494
- // Count behavior occurrences
495
- const behaviorCounts = { AGGRESSIVE: 0, NORMAL: 0, CAUTIOUS: 0, PAUSE: 0 };
496
- const valueToAction = { 3: 'AGGRESSIVE', 2: 'NORMAL', 1: 'CAUTIOUS', 0: 'PAUSE' };
497
-
498
- if (behaviorData.values.length > 0) {
499
- for (const val of behaviorData.values) {
500
- const action = valueToAction[Math.round(val)] || 'NORMAL';
501
- behaviorCounts[action]++;
502
- }
503
- } else {
504
- behaviorCounts.NORMAL = 1; // Default
505
- }
506
-
507
- const total = Object.values(behaviorCounts).reduce((a, b) => a + b, 0) || 1;
508
- const percentages = {
509
- AGGRESSIVE: Math.round((behaviorCounts.AGGRESSIVE / total) * 100),
510
- NORMAL: Math.round((behaviorCounts.NORMAL / total) * 100),
511
- CAUTIOUS: Math.round((behaviorCounts.CAUTIOUS / total) * 100),
512
- PAUSE: Math.round((behaviorCounts.PAUSE / total) * 100)
513
- };
514
-
515
- // Get current behavior
516
- const currentValue = behaviorData.values.length > 0 ? behaviorData.values[behaviorData.values.length - 1] : 2;
517
- const currentAction = valueToAction[Math.round(currentValue)] || 'NORMAL';
518
-
519
- // Colors for each behavior
520
- const barColors = {
521
- AGGRESSIVE: chalk.green,
522
- NORMAL: chalk.cyan,
523
- CAUTIOUS: chalk.yellow,
524
- PAUSE: chalk.red
525
- };
526
-
527
- // Horizontal bar chart configuration
528
- const barLabels = ['AGGRESSIVE', 'NORMAL', 'CAUTIOUS', 'PAUSE'];
529
- const shortLabels = ['AGR', 'NOR', 'CAU', 'PAU'];
530
- const labelWidth = 6; // "AGR " etc.
531
- const pctWidth = 6; // "100% "
532
- const spacing = 4; // spaces between bars
533
-
534
- // Calculate bar width for each category (total 4 bars with spacing)
535
- const availableWidth = behaviorInnerWidth - (labelWidth * 4) - (pctWidth * 4) - (spacing * 3) - 4;
536
- const maxBarWidth = Math.floor(availableWidth / 4);
537
-
538
- // Calculate bar widths based on percentage (max 100% = maxBarWidth)
539
- const barWidths = {
540
- AGGRESSIVE: Math.round((percentages.AGGRESSIVE / 100) * maxBarWidth),
541
- NORMAL: Math.round((percentages.NORMAL / 100) * maxBarWidth),
542
- CAUTIOUS: Math.round((percentages.CAUTIOUS / 100) * maxBarWidth),
543
- PAUSE: Math.round((percentages.PAUSE / 100) * maxBarWidth)
544
- };
545
-
546
- // Draw horizontal bars (each bar is a row)
547
- for (let i = 0; i < 4; i++) {
548
- const label = barLabels[i];
549
- const shortLabel = shortLabels[i];
550
- const pct = percentages[label];
551
- const barWidth = barWidths[label];
552
- const color = barColors[label];
553
- const isCurrent = label === currentAction;
554
-
555
- // Build the bar
556
- const block = isCurrent ? '█' : '▓';
557
- const bar = barWidth > 0 ? color(block.repeat(barWidth)) : '';
558
- const emptySpace = ' '.repeat(Math.max(0, maxBarWidth - barWidth));
559
-
560
- // Format: " AGR ████████████████ 100% "
561
- const labelPart = ' ' + color(shortLabel.padEnd(labelWidth));
562
- const barPart = bar + emptySpace;
563
- const pctPart = chalk.white((pct + '%').padStart(pctWidth));
564
-
565
- let line = labelPart + barPart + pctPart;
566
- const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
567
- line += ' '.repeat(Math.max(0, behaviorInnerWidth - lineLen));
568
- console.log(chalk.cyan('\u2551') + line + chalk.cyan('\u2551'));
569
- }
570
-
571
- // Empty line
572
- console.log(chalk.cyan('\u2551') + ' '.repeat(behaviorInnerWidth) + chalk.cyan('\u2551'));
573
-
574
- // Current session stats line
575
- const statsLine = ` CURRENT: ${barColors[currentAction](currentAction)} | SESSION PATTERNS: ${learningStats.patternsLearned.total} (${learningStats.patternsLearned.winning}W/${learningStats.patternsLearned.losing}L) | OPTIMIZATIONS: ${learningStats.optimizations}`;
576
- const statsLen = statsLine.replace(/\x1b\[[0-9;]*m/g, '').length;
577
- console.log(chalk.cyan('\u2551') + statsLine + ' '.repeat(Math.max(0, behaviorInnerWidth - statsLen)) + chalk.cyan('\u2551'));
578
-
579
- // Lifetime stats line (memory across sessions)
580
- const lifetimeStats = StrategySupervisor.getLifetimeStats();
581
- if (lifetimeStats.totalSessions > 0) {
582
- const lifetimeLine = ` LIFETIME: ${lifetimeStats.totalSessions} sessions | ${lifetimeStats.totalTrades} trades | WR: ${lifetimeStats.lifetimeWinRate} | P&L: $${lifetimeStats.lifetimePnL.toFixed(2)} | ${lifetimeStats.patternsLearned.winning + lifetimeStats.patternsLearned.losing} patterns learned`;
583
- const lifetimeLen = lifetimeLine.length;
584
- console.log(chalk.cyan('\u2551') + chalk.magenta(lifetimeLine) + ' '.repeat(Math.max(0, behaviorInnerWidth - lifetimeLen)) + chalk.cyan('\u2551'));
585
- }
586
-
587
- drawBoxFooter(boxWidth);
304
+ renderAISupervision(aiAgents, supervisionStatus, AISupervisor, boxWidth, col1, col2);
305
+ renderAIBehavior(StrategySupervisor, boxWidth);
588
306
  }
589
307
 
590
308
  // ========== EQUITY CURVE ==========
591
309
  console.log();
592
- drawBoxHeader('EQUITY CURVE', boxWidth);
593
-
594
- const chartInnerWidth = boxWidth - 2;
595
-
596
- if (allTrades.length > 0) {
597
- const yAxisWidth = 10;
598
- const chartAreaWidth = chartInnerWidth - yAxisWidth - 4;
599
-
600
- let equityData = [totalStartingBalance || 100000];
601
- let eqVal = equityData[0];
602
- allTrades.forEach(trade => {
603
- eqVal += (trade.profitAndLoss || trade.pnl || 0);
604
- equityData.push(eqVal);
605
- });
606
-
607
- const maxDataPoints = chartAreaWidth - 5;
608
- if (equityData.length > maxDataPoints) {
609
- const step = Math.ceil(equityData.length / maxDataPoints);
610
- equityData = equityData.filter((_, i) => i % step === 0);
611
- }
612
-
613
- const chartConfig = {
614
- height: 10,
615
- colors: [equityData[equityData.length - 1] < equityData[0] ? asciichart.red : asciichart.green],
616
- format: (x) => ('$' + (x / 1000).toFixed(0) + 'K').padStart(yAxisWidth)
617
- };
618
-
619
- const chart = asciichart.plot(equityData, chartConfig);
620
- chart.split('\n').forEach(line => {
621
- let chartLine = ' ' + line;
622
- const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
623
- if (len < chartInnerWidth) chartLine += ' '.repeat(chartInnerWidth - len);
624
- console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
625
- });
626
- } else {
627
- const msg = connectionTypes.rithmic > 0
628
- ? ' NO TRADE HISTORY (RITHMIC DOES NOT PROVIDE TRADE HISTORY API)'
629
- : ' NO TRADE DATA AVAILABLE';
630
- console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(Math.max(0, chartInnerWidth - msg.length)) + chalk.cyan('\u2551'));
631
- }
632
-
633
- drawBoxFooter(boxWidth);
310
+ renderEquityCurve(allTrades, totalStartingBalance, connectionTypes, boxWidth);
634
311
 
635
312
  // ========== TRADES HISTORY ==========
636
313
  console.log();
637
- drawBoxHeader('TRADES HISTORY', boxWidth);
638
-
639
- const innerWidth = boxWidth - 2;
640
-
641
- // Helper to extract symbol from contractId (e.g., "CON.F.US.EP.H25" -> "ES H25")
642
- const extractSymbol = (contractId) => {
643
- if (!contractId) return 'N/A';
644
- // ProjectX format: CON.F.US.{SYMBOL}.{MONTH}
645
- const parts = contractId.split('.');
646
- if (parts.length >= 5) {
647
- const sym = parts[3];
648
- const month = parts[4];
649
- const symbolMap = { 'EP': 'ES', 'ENQ': 'NQ', 'MES': 'MES', 'MNQ': 'MNQ', 'YM': 'YM', 'NKD': 'NKD', 'RTY': 'RTY' };
650
- return (symbolMap[sym] || sym) + ' ' + month;
651
- }
652
- // Rithmic format: already clean symbol
653
- if (contractId.length <= 10) return contractId;
654
- return contractId.substring(0, 10);
655
- };
656
-
657
- if (allTrades.length > 0) {
658
- // Column widths - total must equal innerWidth
659
- // Format: " Time | Symbol | Side | P&L | Fees | Net | Account... "
660
- const colTime = 9;
661
- const colSymbol = 10;
662
- const colSide = 6;
663
- const colPnl = 10;
664
- const colFees = 8;
665
- const colNet = 10;
666
- // Each column has "| " after it (2 chars), plus leading space (1 char)
667
- const fixedCols = colTime + colSymbol + colSide + colPnl + colFees + colNet;
668
- const separatorChars = 6 * 2; // 6 "| " separators
669
- const leadingSpace = 1;
670
- const colAccount = innerWidth - fixedCols - separatorChars - leadingSpace;
671
-
672
- // Header - build with exact spacing
673
- const headerParts = [
674
- ' ' + 'TIME'.padEnd(colTime),
675
- 'SYMBOL'.padEnd(colSymbol),
676
- 'SIDE'.padEnd(colSide),
677
- 'P&L'.padEnd(colPnl),
678
- 'FEES'.padEnd(colFees),
679
- 'NET'.padEnd(colNet),
680
- 'ACCOUNT'.padEnd(colAccount)
681
- ];
682
- const header = headerParts.join('| ');
683
- console.log(chalk.cyan('\u2551') + chalk.white(header) + chalk.cyan('\u2551'));
684
- console.log(chalk.cyan('\u255F') + chalk.cyan('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2562'));
685
-
686
- // Show only COMPLETED trades (with P&L), sorted by time (most recent first)
687
- // Filter out entry fills (P&L = 0 or null) - only show exit fills with real P&L
688
- const completedTrades = allTrades.filter(t => {
689
- const pnl = t.profitAndLoss || t.pnl;
690
- return pnl !== null && pnl !== undefined && pnl !== 0;
691
- });
692
-
693
- const sortedTrades = [...completedTrades].sort((a, b) => {
694
- const timeA = new Date(a.creationTimestamp || a.timestamp || 0).getTime();
695
- const timeB = new Date(b.creationTimestamp || b.timestamp || 0).getTime();
696
- return timeB - timeA;
697
- });
698
-
699
- for (const trade of sortedTrades) {
700
- const timestamp = trade.creationTimestamp || trade.timestamp;
701
- const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
702
- const symbol = extractSymbol(trade.contractId || trade.symbol);
703
- const pnl = trade.profitAndLoss || trade.pnl || 0;
704
- const fees = trade.fees || trade.commission || 0;
705
- const netPnl = pnl - Math.abs(fees);
706
-
707
- // Format values
708
- const pnlText = pnl >= 0 ? `+$${pnl.toFixed(0)}` : `-$${Math.abs(pnl).toFixed(0)}`;
709
- const feesText = fees !== 0 ? `-$${Math.abs(fees).toFixed(2)}` : '$0';
710
- const netText = netPnl >= 0 ? `+$${netPnl.toFixed(0)}` : `-$${Math.abs(netPnl).toFixed(0)}`;
711
-
712
- // For completed trades, show the original direction (opposite of exit side)
713
- const exitSide = trade.side; // 0=BUY exit means was SHORT, 1=SELL exit means was LONG
714
- const tradeSide = exitSide === 0 ? 'SHORT' : 'LONG';
715
- const accountName = (trade.accountName || 'N/A').substring(0, colAccount - 3);
716
-
717
- // Build row with exact widths
718
- const timeStr = time.padEnd(colTime);
719
- const symbolStr = symbol.padEnd(colSymbol);
720
- const sideStr = tradeSide.padEnd(colSide);
721
- const pnlStr = pnlText.padEnd(colPnl);
722
- const feesStr = feesText.padEnd(colFees);
723
- const netStr = netText.padEnd(colNet);
724
- const accountStr = accountName.padEnd(colAccount);
725
-
726
- // Colored versions
727
- const pnlColored = pnl >= 0 ? chalk.green(pnlStr) : chalk.red(pnlStr);
728
- const feesColored = chalk.yellow(feesStr);
729
- const netColored = netPnl >= 0 ? chalk.green(netStr) : chalk.red(netStr);
730
- const sideColored = tradeSide === 'LONG' ? chalk.green(sideStr) : chalk.red(sideStr);
731
-
732
- // Build row with same format as header
733
- const rowParts = [
734
- ' ' + timeStr,
735
- symbolStr,
736
- sideColored,
737
- pnlColored,
738
- feesColored,
739
- netColored,
740
- accountStr
741
- ];
742
- const row = rowParts.join('| ');
743
- console.log(chalk.cyan('\u2551') + row + chalk.cyan('\u2551'));
744
- }
745
-
746
- if (sortedTrades.length === 0) {
747
- const msg = ' NO COMPLETED TRADES YET';
748
- console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
749
- }
750
- } else {
751
- const msg = connectionTypes.rithmic > 0
752
- ? ' NO TRADE HISTORY (RITHMIC API LIMITATION)'
753
- : ' NO TRADE HISTORY AVAILABLE';
754
- console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
755
- }
756
-
757
- drawBoxFooter(boxWidth);
314
+ renderTradesHistory(completedTrades, connectionTypes, extractSymbol, boxWidth);
758
315
 
759
316
  // ========== HQX SCORE ==========
760
- // Only show if we have trade data to score
761
317
  if (hasTradeData || stats.totalTrades > 0) {
762
318
  console.log();
763
- drawBoxHeader('HQX SCORE', boxWidth);
764
-
765
- const winRateNum = winRate !== 'N/A' ? parseFloat(winRate) : 0;
766
- const winRateScore = Math.min(100, winRateNum * 1.5);
767
- const profitFactorScore = profitFactor === '∞' ? 100 : profitFactor === 'N/A' ? 0 : Math.min(100, parseFloat(profitFactor) * 40);
768
- const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
769
- const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
770
- ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
771
- : 50;
772
- const volumeScore = Math.min(100, stats.totalTrades * 2);
773
- const returnNum = returnPercent !== 'N/A' ? parseFloat(returnPercent) : 0;
774
- const returnScore = Math.min(100, Math.max(0, returnNum * 10 + 50));
775
-
776
- const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
777
- const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
778
- const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
779
-
780
- const makeBar = (score, width = 20) => {
781
- const filled = Math.round((score / 100) * width);
782
- const empty = width - filled;
783
- const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
784
- return color('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(empty));
785
- };
786
-
787
- const metricsDisplay = [
788
- { name: 'WIN RATE', score: winRateScore },
789
- { name: 'PROFIT FACTOR', score: profitFactorScore },
790
- { name: 'CONSISTENCY', score: consistencyScore },
791
- { name: 'RISK MANAGEMENT', score: riskScore },
792
- { name: 'VOLUME', score: volumeScore },
793
- { name: 'RETURNS', score: returnScore }
794
- ];
795
-
796
- const barWidth = 30;
797
- const labelWidth = 18;
798
-
799
- const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScore))} / 100 [GRADE: ${scoreColor(scoreGrade)}]`;
800
- const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
801
- console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
802
- console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
803
-
804
- for (const metric of metricsDisplay) {
805
- const label = (' ' + metric.name + ':').padEnd(labelWidth);
806
- const bar = makeBar(metric.score, barWidth);
807
- const pct = (metric.score.toFixed(0) + '%').padStart(5);
808
- const line = label + bar + ' ' + pct;
809
- const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
810
- console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
811
- }
812
-
813
- drawBoxFooter(boxWidth);
319
+ const hqxData = calculateHQXScore(stats, totalStartingBalance, returnPercent, profitFactor, winRate);
320
+ renderHQXScore(hqxData, boxWidth);
814
321
  }
815
322
 
816
323
  console.log();