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.
- package/package.json +1 -1
- package/src/menus/ai-agent-connect.js +181 -0
- package/src/menus/ai-agent-models.js +219 -0
- package/src/menus/ai-agent-oauth.js +292 -0
- package/src/menus/ai-agent-ui.js +141 -0
- package/src/menus/ai-agent.js +88 -1489
- package/src/pages/algo/copy-engine.js +449 -0
- package/src/pages/algo/copy-trading.js +11 -543
- package/src/pages/algo/smart-logs-data.js +218 -0
- package/src/pages/algo/smart-logs.js +9 -214
- package/src/pages/algo/ui-constants.js +144 -0
- package/src/pages/algo/ui-summary.js +184 -0
- package/src/pages/algo/ui.js +42 -526
- package/src/pages/stats-calculations.js +191 -0
- package/src/pages/stats-ui.js +381 -0
- package/src/pages/stats.js +14 -507
- package/src/services/ai/client-analysis.js +194 -0
- package/src/services/ai/client-models.js +333 -0
- package/src/services/ai/client.js +6 -489
- package/src/services/ai/index.js +2 -257
- package/src/services/ai/proxy-install.js +249 -0
- package/src/services/ai/proxy-manager.js +29 -411
- package/src/services/ai/proxy-remote.js +161 -0
- package/src/services/ai/strategy-supervisor.js +10 -765
- package/src/services/ai/supervisor-data.js +195 -0
- package/src/services/ai/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -0
- package/src/services/ai/supervisor-utils.js +158 -0
- package/src/services/ai/supervisor.js +50 -515
- package/src/services/ai/validation.js +250 -0
- package/src/services/hqx-server-events.js +110 -0
- package/src/services/hqx-server-handlers.js +217 -0
- package/src/services/hqx-server-latency.js +136 -0
- package/src/services/hqx-server.js +51 -403
- package/src/services/position-constants.js +28 -0
- package/src/services/position-manager.js +105 -554
- package/src/services/position-momentum.js +206 -0
- package/src/services/projectx/accounts.js +142 -0
- package/src/services/projectx/index.js +40 -289
- package/src/services/projectx/trading.js +180 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +32 -542
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
package/src/pages/stats.js
CHANGED
|
@@ -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,
|
|
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
|
|
212
|
-
|
|
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 (
|
|
282
|
-
const
|
|
283
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|