hedgequantx 2.5.38 → 2.5.40

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.38",
3
+ "version": "2.5.40",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -481,84 +481,147 @@ const showStats = async (service) => {
481
481
 
482
482
  drawBoxFooter(boxWidth);
483
483
 
484
- // ========== AI BEHAVIOR GRAPH ==========
485
- const behaviorData = StrategySupervisor.getBehaviorHistory(boxWidth - 20);
484
+ // ========== AI BEHAVIOR DIAGRAM ==========
485
+ const behaviorData = StrategySupervisor.getBehaviorHistory(100);
486
486
  const learningStats = StrategySupervisor.getLearningStats();
487
487
 
488
- if (behaviorData.values.length > 2) {
489
- console.log();
490
- drawBoxHeader('AI AGENTS BEHAVIOR', boxWidth);
491
-
492
- const behaviorInnerWidth = boxWidth - 2;
493
- const yAxisWidth = 12;
494
- const chartWidth = behaviorInnerWidth - yAxisWidth - 4;
495
-
496
- // Scale values for better visualization (0-3 -> 0-100 with some padding)
497
- let scaledValues = behaviorData.values.map(v => (v + 0.5) * 25); // 0->12.5, 1->37.5, 2->62.5, 3->87.5
498
-
499
- // Ensure we have enough points for a nice curve
500
- if (scaledValues.length < 10) {
501
- const newValues = [];
502
- for (let i = 0; i < scaledValues.length - 1; i++) {
503
- newValues.push(scaledValues[i]);
504
- // Smooth interpolation
505
- const diff = scaledValues[i + 1] - scaledValues[i];
506
- newValues.push(scaledValues[i] + diff * 0.33);
507
- newValues.push(scaledValues[i] + diff * 0.66);
508
- }
509
- newValues.push(scaledValues[scaledValues.length - 1]);
510
- scaledValues = newValues;
511
- }
512
-
513
- // Limit data points to chart width
514
- if (scaledValues.length > chartWidth) {
515
- const step = Math.ceil(scaledValues.length / chartWidth);
516
- scaledValues = scaledValues.filter((_, i) => i % step === 0);
488
+ console.log();
489
+ drawBoxHeader('AI AGENTS BEHAVIOR', boxWidth);
490
+
491
+ const behaviorInnerWidth = boxWidth - 2;
492
+
493
+ // Count behavior occurrences
494
+ const behaviorCounts = { AGGRESSIVE: 0, NORMAL: 0, CAUTIOUS: 0, PAUSE: 0 };
495
+ const valueToAction = { 3: 'AGGRESSIVE', 2: 'NORMAL', 1: 'CAUTIOUS', 0: 'PAUSE' };
496
+
497
+ if (behaviorData.values.length > 0) {
498
+ for (const val of behaviorData.values) {
499
+ const action = valueToAction[Math.round(val)] || 'NORMAL';
500
+ behaviorCounts[action]++;
517
501
  }
502
+ } else {
503
+ behaviorCounts.NORMAL = 1; // Default
504
+ }
505
+
506
+ const total = Object.values(behaviorCounts).reduce((a, b) => a + b, 0) || 1;
507
+ const percentages = {
508
+ AGGRESSIVE: Math.round((behaviorCounts.AGGRESSIVE / total) * 100),
509
+ NORMAL: Math.round((behaviorCounts.NORMAL / total) * 100),
510
+ CAUTIOUS: Math.round((behaviorCounts.CAUTIOUS / total) * 100),
511
+ PAUSE: Math.round((behaviorCounts.PAUSE / total) * 100)
512
+ };
513
+
514
+ // Bar chart configuration
515
+ const labelWidth = 12;
516
+ const percentWidth = 6;
517
+ const barMaxWidth = behaviorInnerWidth - labelWidth - percentWidth - 6;
518
+
519
+ // Get current behavior
520
+ const currentValue = behaviorData.values.length > 0 ? behaviorData.values[behaviorData.values.length - 1] : 2;
521
+ const currentAction = valueToAction[Math.round(currentValue)] || 'NORMAL';
522
+
523
+ // Draw vertical bar chart (bars going up)
524
+ const chartHeight = 8;
525
+ const barWidth = Math.floor((barMaxWidth - 12) / 4); // 4 bars with spacing
526
+
527
+ // Calculate bar heights (max height = chartHeight)
528
+ const maxPercent = Math.max(...Object.values(percentages), 1);
529
+ const barHeights = {
530
+ AGGRESSIVE: Math.round((percentages.AGGRESSIVE / 100) * chartHeight),
531
+ NORMAL: Math.round((percentages.NORMAL / 100) * chartHeight),
532
+ CAUTIOUS: Math.round((percentages.CAUTIOUS / 100) * chartHeight),
533
+ PAUSE: Math.round((percentages.PAUSE / 100) * chartHeight)
534
+ };
535
+
536
+ // Colors for each behavior
537
+ const barColors = {
538
+ AGGRESSIVE: chalk.green,
539
+ NORMAL: chalk.cyan,
540
+ CAUTIOUS: chalk.yellow,
541
+ PAUSE: chalk.red
542
+ };
543
+
544
+ // Draw bars from top to bottom
545
+ const barLabels = ['AGGRESSIVE', 'NORMAL', 'CAUTIOUS', 'PAUSE'];
546
+ const shortLabels = ['AGR', 'NOR', 'CAU', 'PAU'];
547
+
548
+ // Calculate left padding to center the chart
549
+ const totalBarWidth = (barWidth * 4) + 9; // 4 bars + 3 spaces of 3 chars
550
+ const leftPad = Math.floor((behaviorInnerWidth - totalBarWidth - 4) / 2);
551
+
552
+ for (let row = chartHeight; row >= 1; row--) {
553
+ let line = ' '.repeat(leftPad);
518
554
 
519
- // Determine color based on current behavior
520
- const currentValue = behaviorData.values[behaviorData.values.length - 1];
521
- let chartColor = asciichart.white;
522
- if (currentValue >= 2.5) chartColor = asciichart.green; // AGGRESSIVE
523
- else if (currentValue >= 1.5) chartColor = asciichart.cyan; // NORMAL
524
- else if (currentValue >= 0.5) chartColor = asciichart.yellow; // CAUTIOUS
525
- else chartColor = asciichart.red; // PAUSE
526
-
527
- const behaviorConfig = {
528
- height: 6,
529
- min: 0,
530
- max: 100,
531
- colors: [chartColor],
532
- format: (x) => {
533
- if (x >= 75) return 'AGGRESSIVE'.padStart(yAxisWidth);
534
- if (x >= 50) return ' NORMAL'.padStart(yAxisWidth);
535
- if (x >= 25) return ' CAUTIOUS'.padStart(yAxisWidth);
536
- return ' PAUSE'.padStart(yAxisWidth);
555
+ for (let i = 0; i < 4; i++) {
556
+ const label = barLabels[i];
557
+ const height = barHeights[label];
558
+ const color = barColors[label];
559
+ const isCurrent = label === currentAction;
560
+
561
+ if (row <= height) {
562
+ // Draw filled bar
563
+ const block = isCurrent ? '█' : '▓';
564
+ line += color(block.repeat(barWidth));
565
+ } else {
566
+ // Empty space
567
+ line += ' '.repeat(barWidth);
537
568
  }
538
- };
539
-
540
- try {
541
- const behaviorChart = asciichart.plot(scaledValues, behaviorConfig);
542
- behaviorChart.split('\n').forEach(line => {
543
- let chartLine = ' ' + line;
544
- const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
545
- if (len < behaviorInnerWidth) chartLine += ' '.repeat(behaviorInnerWidth - len);
546
- console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
547
- });
548
- } catch (e) {
549
- // Fallback if chart fails
550
- const msg = ' BEHAVIOR DATA INSUFFICIENT';
551
- console.log(chalk.cyan('\u2551') + chalk.white(msg) + ' '.repeat(Math.max(0, behaviorInnerWidth - msg.length)) + chalk.cyan('\u2551'));
569
+
570
+ if (i < 3) line += ' '; // Space between bars
552
571
  }
553
572
 
554
- // Add learning stats line
555
- const durationMin = Math.floor(behaviorData.duration / 60000);
556
- const statsLine = ` SESSION: ${durationMin}m | PATTERNS: ${learningStats.patternsLearned.total} (${learningStats.patternsLearned.winning}W/${learningStats.patternsLearned.losing}L) | OPTIMIZATIONS: ${learningStats.optimizations} | SIGNALS: ${learningStats.signalsObserved}`;
557
- const statsLen = statsLine.length;
558
- console.log(chalk.cyan('\u2551') + chalk.white(statsLine) + ' '.repeat(Math.max(0, behaviorInnerWidth - statsLen)) + chalk.cyan('\u2551'));
559
-
560
- drawBoxFooter(boxWidth);
573
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
574
+ line += ' '.repeat(Math.max(0, behaviorInnerWidth - lineLen));
575
+ console.log(chalk.cyan('\u2551') + line + chalk.cyan('\u2551'));
576
+ }
577
+
578
+ // Draw baseline
579
+ let baseLine = ' '.repeat(leftPad) + '─'.repeat(totalBarWidth);
580
+ const baseLen = baseLine.length;
581
+ baseLine += ' '.repeat(Math.max(0, behaviorInnerWidth - baseLen));
582
+ console.log(chalk.cyan('\u2551') + chalk.white(baseLine) + chalk.cyan('\u2551'));
583
+
584
+ // Draw labels
585
+ let labelLine = ' '.repeat(leftPad);
586
+ for (let i = 0; i < 4; i++) {
587
+ const lbl = shortLabels[i];
588
+ const pad = Math.floor((barWidth - lbl.length) / 2);
589
+ labelLine += ' '.repeat(pad) + barColors[barLabels[i]](lbl) + ' '.repeat(barWidth - pad - lbl.length);
590
+ if (i < 3) labelLine += ' ';
591
+ }
592
+ const lblLen = labelLine.replace(/\x1b\[[0-9;]*m/g, '').length;
593
+ labelLine += ' '.repeat(Math.max(0, behaviorInnerWidth - lblLen));
594
+ console.log(chalk.cyan('\u2551') + labelLine + chalk.cyan('\u2551'));
595
+
596
+ // Draw percentages
597
+ let pctLine = ' '.repeat(leftPad);
598
+ for (let i = 0; i < 4; i++) {
599
+ const pct = percentages[barLabels[i]] + '%';
600
+ const pad = Math.floor((barWidth - pct.length) / 2);
601
+ pctLine += ' '.repeat(pad) + chalk.white(pct) + ' '.repeat(barWidth - pad - pct.length);
602
+ if (i < 3) pctLine += ' ';
561
603
  }
604
+ const pctLen = pctLine.replace(/\x1b\[[0-9;]*m/g, '').length;
605
+ pctLine += ' '.repeat(Math.max(0, behaviorInnerWidth - pctLen));
606
+ console.log(chalk.cyan('\u2551') + pctLine + chalk.cyan('\u2551'));
607
+
608
+ // Empty line
609
+ console.log(chalk.cyan('\u2551') + ' '.repeat(behaviorInnerWidth) + chalk.cyan('\u2551'));
610
+
611
+ // Current session stats line
612
+ const statsLine = ` CURRENT: ${barColors[currentAction](currentAction)} | SESSION PATTERNS: ${learningStats.patternsLearned.total} (${learningStats.patternsLearned.winning}W/${learningStats.patternsLearned.losing}L) | OPTIMIZATIONS: ${learningStats.optimizations}`;
613
+ const statsLen = statsLine.replace(/\x1b\[[0-9;]*m/g, '').length;
614
+ console.log(chalk.cyan('\u2551') + statsLine + ' '.repeat(Math.max(0, behaviorInnerWidth - statsLen)) + chalk.cyan('\u2551'));
615
+
616
+ // Lifetime stats line (memory across sessions)
617
+ const lifetimeStats = StrategySupervisor.getLifetimeStats();
618
+ if (lifetimeStats.totalSessions > 0) {
619
+ 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`;
620
+ const lifetimeLen = lifetimeLine.length;
621
+ console.log(chalk.cyan('\u2551') + chalk.magenta(lifetimeLine) + ' '.repeat(Math.max(0, behaviorInnerWidth - lifetimeLen)) + chalk.cyan('\u2551'));
622
+ }
623
+
624
+ drawBoxFooter(boxWidth);
562
625
  }
563
626
 
564
627
  // ========== EQUITY CURVE ==========
@@ -8,12 +8,114 @@
8
8
  * 2. LEARN - Analyze winning/losing trades to identify patterns
9
9
  * 3. OPTIMIZE - Suggest and apply parameter improvements
10
10
  * 4. SUPERVISE - Monitor risk and intervene when necessary
11
+ * 5. PERSIST - Save learned patterns and optimizations between sessions
11
12
  *
12
13
  * In CONSENSUS mode (2+ agents), ALL agents must agree before applying changes.
13
14
  */
14
15
 
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
15
19
  const { analyzePerformance, getMarketAdvice, callAI } = require('./client');
16
20
 
21
+ // Path for persisted learning data
22
+ const DATA_DIR = path.join(os.homedir(), '.hqx');
23
+ const LEARNING_FILE = path.join(DATA_DIR, 'ai-learning.json');
24
+
25
+ /**
26
+ * Load persisted learning data from disk
27
+ * Called on startup to restore previous learnings
28
+ */
29
+ const loadLearningData = () => {
30
+ try {
31
+ if (!fs.existsSync(DATA_DIR)) {
32
+ fs.mkdirSync(DATA_DIR, { recursive: true });
33
+ }
34
+
35
+ if (fs.existsSync(LEARNING_FILE)) {
36
+ const data = JSON.parse(fs.readFileSync(LEARNING_FILE, 'utf8'));
37
+ return {
38
+ winningPatterns: data.winningPatterns || [],
39
+ losingPatterns: data.losingPatterns || [],
40
+ optimizations: data.optimizations || [],
41
+ totalSessions: data.totalSessions || 0,
42
+ totalTrades: data.totalTrades || 0,
43
+ totalWins: data.totalWins || 0,
44
+ totalLosses: data.totalLosses || 0,
45
+ lifetimePnL: data.lifetimePnL || 0,
46
+ lastUpdated: data.lastUpdated || null
47
+ };
48
+ }
49
+ } catch (e) {
50
+ // Silent fail - start fresh
51
+ }
52
+
53
+ return {
54
+ winningPatterns: [],
55
+ losingPatterns: [],
56
+ optimizations: [],
57
+ totalSessions: 0,
58
+ totalTrades: 0,
59
+ totalWins: 0,
60
+ totalLosses: 0,
61
+ lifetimePnL: 0,
62
+ lastUpdated: null
63
+ };
64
+ };
65
+
66
+ /**
67
+ * Save learning data to disk
68
+ * Called after each trade and on session end
69
+ */
70
+ const saveLearningData = () => {
71
+ try {
72
+ if (!fs.existsSync(DATA_DIR)) {
73
+ fs.mkdirSync(DATA_DIR, { recursive: true });
74
+ }
75
+
76
+ // Load existing data first
77
+ const existing = loadLearningData();
78
+
79
+ // Merge with current session data
80
+ const dataToSave = {
81
+ // Patterns - keep last 100 of each type (merge and dedupe by timestamp)
82
+ winningPatterns: mergePatterns(existing.winningPatterns, supervisorState.winningPatterns, 100),
83
+ losingPatterns: mergePatterns(existing.losingPatterns, supervisorState.losingPatterns, 100),
84
+
85
+ // Optimizations history - keep last 50
86
+ optimizations: [...existing.optimizations, ...supervisorState.optimizations].slice(-50),
87
+
88
+ // Lifetime stats
89
+ totalSessions: existing.totalSessions + (supervisorState.active ? 0 : 1),
90
+ totalTrades: existing.totalTrades + supervisorState.performance.trades,
91
+ totalWins: existing.totalWins + supervisorState.performance.wins,
92
+ totalLosses: existing.totalLosses + supervisorState.performance.losses,
93
+ lifetimePnL: existing.lifetimePnL + supervisorState.performance.totalPnL,
94
+
95
+ // Metadata
96
+ lastUpdated: new Date().toISOString()
97
+ };
98
+
99
+ fs.writeFileSync(LEARNING_FILE, JSON.stringify(dataToSave, null, 2));
100
+ return true;
101
+ } catch (e) {
102
+ return false;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Merge pattern arrays, keeping most recent unique entries
108
+ */
109
+ const mergePatterns = (existing, current, maxCount) => {
110
+ const merged = [...existing, ...current];
111
+
112
+ // Sort by timestamp descending (most recent first)
113
+ merged.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
114
+
115
+ // Keep only maxCount most recent
116
+ return merged.slice(0, maxCount);
117
+ };
118
+
17
119
  // Singleton supervisor state
18
120
  let supervisorState = {
19
121
  active: false,
@@ -69,10 +171,14 @@ let analysisInterval = null;
69
171
 
70
172
  /**
71
173
  * Initialize supervisor with strategy and agents
174
+ * Loads previous learning data to continue improving
72
175
  */
73
176
  const initialize = (strategy, agents, service, accountId) => {
74
177
  const now = Date.now();
75
178
 
179
+ // Load previously learned patterns and optimizations
180
+ const previousLearning = loadLearningData();
181
+
76
182
  supervisorState = {
77
183
  ...supervisorState,
78
184
  active: true,
@@ -83,8 +189,17 @@ const initialize = (strategy, agents, service, accountId) => {
83
189
  ticks: [],
84
190
  signals: [],
85
191
  trades: [],
86
- winningPatterns: [],
87
- losingPatterns: [],
192
+ // Restore previous learning
193
+ winningPatterns: previousLearning.winningPatterns || [],
194
+ losingPatterns: previousLearning.losingPatterns || [],
195
+ previousOptimizations: previousLearning.optimizations || [],
196
+ lifetimeStats: {
197
+ sessions: previousLearning.totalSessions || 0,
198
+ trades: previousLearning.totalTrades || 0,
199
+ wins: previousLearning.totalWins || 0,
200
+ losses: previousLearning.totalLosses || 0,
201
+ pnl: previousLearning.lifetimePnL || 0
202
+ },
88
203
  performance: {
89
204
  trades: 0,
90
205
  wins: 0,
@@ -124,7 +239,7 @@ const initialize = (strategy, agents, service, accountId) => {
124
239
  };
125
240
 
126
241
  /**
127
- * Stop supervisor
242
+ * Stop supervisor and save learned data
128
243
  */
129
244
  const stop = () => {
130
245
  if (analysisInterval) {
@@ -132,11 +247,16 @@ const stop = () => {
132
247
  analysisInterval = null;
133
248
  }
134
249
 
250
+ // Save all learned data before stopping
251
+ const saved = saveLearningData();
252
+
135
253
  const summary = {
136
254
  ...supervisorState.performance,
137
255
  optimizationsApplied: supervisorState.optimizations.length,
138
256
  winningPatterns: supervisorState.winningPatterns.length,
139
- losingPatterns: supervisorState.losingPatterns.length
257
+ losingPatterns: supervisorState.losingPatterns.length,
258
+ dataSaved: saved,
259
+ lifetimeStats: supervisorState.lifetimeStats
140
260
  };
141
261
 
142
262
  supervisorState.active = false;
@@ -768,6 +888,44 @@ const getLearningStats = () => {
768
888
  };
769
889
  };
770
890
 
891
+ /**
892
+ * Get lifetime stats across all sessions
893
+ * Shows cumulative learning progress
894
+ */
895
+ const getLifetimeStats = () => {
896
+ const saved = loadLearningData();
897
+
898
+ return {
899
+ totalSessions: saved.totalSessions,
900
+ totalTrades: saved.totalTrades,
901
+ totalWins: saved.totalWins,
902
+ totalLosses: saved.totalLosses,
903
+ lifetimeWinRate: saved.totalTrades > 0 ?
904
+ ((saved.totalWins / saved.totalTrades) * 100).toFixed(1) + '%' : 'N/A',
905
+ lifetimePnL: saved.lifetimePnL,
906
+ patternsLearned: {
907
+ winning: saved.winningPatterns?.length || 0,
908
+ losing: saved.losingPatterns?.length || 0
909
+ },
910
+ optimizationsApplied: saved.optimizations?.length || 0,
911
+ lastUpdated: saved.lastUpdated
912
+ };
913
+ };
914
+
915
+ /**
916
+ * Clear all learned data (reset AI memory)
917
+ */
918
+ const clearLearningData = () => {
919
+ try {
920
+ if (fs.existsSync(LEARNING_FILE)) {
921
+ fs.unlinkSync(LEARNING_FILE);
922
+ }
923
+ return true;
924
+ } catch (e) {
925
+ return false;
926
+ }
927
+ };
928
+
771
929
  module.exports = {
772
930
  initialize,
773
931
  stop,
@@ -779,5 +937,8 @@ module.exports = {
779
937
  getStatus,
780
938
  analyzeAndOptimize,
781
939
  getBehaviorHistory,
782
- getLearningStats
940
+ getLearningStats,
941
+ getLifetimeStats,
942
+ clearLearningData,
943
+ loadLearningData
783
944
  };