hedgequantx 2.6.160 → 2.6.161

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.6.160",
3
+ "version": "2.6.161",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -17,535 +17,19 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
19
  const { analyzePerformance, getMarketAdvice, callAI } = require('./client');
20
+ const {
21
+ calculateTrendStrength, calculateRecentRange, calculatePriceVelocity,
22
+ getMarketSession, getMinutesSinceOpen, analyzePriceLevelInteraction,
23
+ determineLevelType, analyzePriceAction, analyzeVolume, calculateVolatility
24
+ } = require('./supervisor-utils');
20
25
 
21
26
  // 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
27
 
25
- /**
26
- * Load persisted learning data from disk
27
- * Contains full memory of strategy behavior over weeks/months
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
-
38
- // Clean old sessions (keep only last 1 month / 31 days)
39
- const oneMonthAgo = Date.now() - (31 * 24 * 60 * 60 * 1000);
40
- const sessions = (data.sessions || []).filter(s =>
41
- new Date(s.date).getTime() > oneMonthAgo
42
- );
43
-
44
- return {
45
- // Pattern memory
46
- winningPatterns: data.winningPatterns || [],
47
- losingPatterns: data.losingPatterns || [],
48
-
49
- // Optimization history
50
- optimizations: data.optimizations || [],
51
-
52
- // Symbol-specific data (NQ, ES, etc.)
53
- symbols: data.symbols || {},
54
-
55
- // Full session history (last 30 days)
56
- sessions: sessions,
57
-
58
- // Hourly performance analysis
59
- hourlyStats: data.hourlyStats || {},
60
-
61
- // Day of week analysis
62
- dayOfWeekStats: data.dayOfWeekStats || {},
63
-
64
- // Strategy behavior profile
65
- strategyProfile: data.strategyProfile || {
66
- bestHours: [],
67
- worstHours: [],
68
- avgWinStreak: 0,
69
- avgLossStreak: 0,
70
- preferredConditions: null
71
- },
72
-
73
- // Lifetime stats
74
- totalSessions: data.totalSessions || 0,
75
- totalTrades: data.totalTrades || 0,
76
- totalWins: data.totalWins || 0,
77
- totalLosses: data.totalLosses || 0,
78
- lifetimePnL: data.lifetimePnL || 0,
79
-
80
- lastUpdated: data.lastUpdated || null,
81
- firstSession: data.firstSession || null
82
- };
83
- }
84
- } catch (e) {
85
- // Silent fail - start fresh
86
- }
87
-
88
- return {
89
- winningPatterns: [],
90
- losingPatterns: [],
91
- optimizations: [],
92
- symbols: {},
93
- sessions: [],
94
- hourlyStats: {},
95
- dayOfWeekStats: {},
96
- strategyProfile: {
97
- bestHours: [],
98
- worstHours: [],
99
- avgWinStreak: 0,
100
- avgLossStreak: 0,
101
- preferredConditions: null
102
- },
103
- totalSessions: 0,
104
- totalTrades: 0,
105
- totalWins: 0,
106
- totalLosses: 0,
107
- lifetimePnL: 0,
108
- lastUpdated: null,
109
- firstSession: null
110
- };
111
- };
112
-
113
- /**
114
- * Get or create symbol data structure
115
- */
116
- const getSymbolData = (symbolName) => {
117
- const data = loadLearningData();
118
- if (!data.symbols[symbolName]) {
119
- return {
120
- name: symbolName,
121
- levels: [], // Key price levels
122
- sessions: [], // Trading sessions history
123
- patterns: [], // Symbol-specific patterns
124
- stats: {
125
- trades: 0,
126
- wins: 0,
127
- losses: 0,
128
- pnl: 0
129
- }
130
- };
131
- }
132
- return data.symbols[symbolName];
133
- };
134
-
135
- /**
136
- * Record a key price level for a symbol
137
- * Called when trades happen at significant levels
138
- */
139
- const recordPriceLevel = (symbolName, price, type, outcome) => {
140
- // type: 'support', 'resistance', 'breakout', 'rejection'
141
- // outcome: 'win', 'loss', 'neutral'
142
-
143
- const level = {
144
- price: Math.round(price * 4) / 4, // Round to nearest 0.25
145
- type,
146
- outcome,
147
- timestamp: Date.now(),
148
- date: new Date().toISOString().split('T')[0],
149
- hour: new Date().getHours()
150
- };
151
-
152
- if (!supervisorState.currentSymbol) {
153
- supervisorState.currentSymbol = symbolName;
154
- }
155
-
156
- if (!supervisorState.symbolLevels) {
157
- supervisorState.symbolLevels = [];
158
- }
159
-
160
- // Check if level already exists (within 2 ticks)
161
- const tickSize = symbolName.includes('NQ') ? 0.25 : 0.25;
162
- const existing = supervisorState.symbolLevels.find(l =>
163
- Math.abs(l.price - level.price) <= tickSize * 2
164
- );
165
-
166
- if (existing) {
167
- // Update existing level
168
- existing.touches = (existing.touches || 1) + 1;
169
- existing.lastTouch = Date.now();
170
- existing.outcomes = existing.outcomes || [];
171
- existing.outcomes.push(outcome);
172
- } else {
173
- // New level
174
- level.touches = 1;
175
- level.outcomes = [outcome];
176
- supervisorState.symbolLevels.push(level);
177
- }
178
- };
179
-
180
- /**
181
- * Analyze current price against known levels
182
- * Returns nearby important levels
183
- */
184
- const analyzeNearbyLevels = (symbolName, currentPrice) => {
185
- const data = loadLearningData();
186
- const symbolData = data.symbols[symbolName];
187
-
188
- if (!symbolData || !symbolData.levels || symbolData.levels.length === 0) {
189
- return { nearbyLevels: [], message: 'No historical levels' };
190
- }
191
-
192
- const tickSize = symbolName.includes('NQ') ? 0.25 : 0.25;
193
- const range = tickSize * 20; // Look within 20 ticks
194
-
195
- const nearbyLevels = symbolData.levels
196
- .filter(l => Math.abs(l.price - currentPrice) <= range)
197
- .sort((a, b) => Math.abs(a.price - currentPrice) - Math.abs(b.price - currentPrice))
198
- .slice(0, 5) // Top 5 nearest levels
199
- .map(l => {
200
- const winRate = l.outcomes ?
201
- l.outcomes.filter(o => o === 'win').length / l.outcomes.length : 0;
202
- return {
203
- price: l.price,
204
- distance: Math.round((l.price - currentPrice) / tickSize),
205
- type: l.type,
206
- touches: l.touches || 1,
207
- winRate: Math.round(winRate * 100),
208
- direction: l.price > currentPrice ? 'above' : 'below'
209
- };
210
- });
211
-
212
- return {
213
- nearbyLevels,
214
- message: nearbyLevels.length > 0 ?
215
- `${nearbyLevels.length} known levels nearby` :
216
- 'No known levels nearby'
217
- };
218
- };
219
-
220
- /**
221
- * Save learning data to disk
222
- * Full memory of strategy behavior over 1 month
223
- */
224
- const saveLearningData = () => {
225
- try {
226
- if (!fs.existsSync(DATA_DIR)) {
227
- fs.mkdirSync(DATA_DIR, { recursive: true });
228
- }
229
-
230
- // Load existing data first
231
- const existing = loadLearningData();
232
-
233
- const now = new Date();
234
- const currentHour = now.getHours();
235
- const currentDay = now.getDay(); // 0=Sunday, 1=Monday, etc.
236
- const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
237
-
238
- // Merge symbol data
239
- const symbols = { ...existing.symbols };
240
- if (supervisorState.currentSymbol) {
241
- const sym = supervisorState.currentSymbol;
242
- if (!symbols[sym]) {
243
- symbols[sym] = {
244
- name: sym,
245
- levels: [],
246
- sessions: [],
247
- stats: { trades: 0, wins: 0, losses: 0, pnl: 0 },
248
- hourlyStats: {},
249
- dayOfWeekStats: {}
250
- };
251
- }
252
-
253
- // Merge levels - keep last 50 most important
254
- const existingLevels = symbols[sym].levels || [];
255
- const newLevels = supervisorState.symbolLevels || [];
256
- symbols[sym].levels = mergeLevels(existingLevels, newLevels, 50);
257
-
258
- // Add current session summary
259
- symbols[sym].sessions.push({
260
- date: now.toISOString(),
261
- hour: currentHour,
262
- dayOfWeek: dayNames[currentDay],
263
- trades: supervisorState.performance.trades,
264
- wins: supervisorState.performance.wins,
265
- losses: supervisorState.performance.losses,
266
- pnl: supervisorState.performance.totalPnL,
267
- levelsWorked: newLevels.length,
268
- maxWinStreak: supervisorState.performance.maxWinStreak,
269
- maxLossStreak: supervisorState.performance.maxLossStreak
270
- });
271
-
272
- // Keep only last 31 days of sessions per symbol
273
- const oneMonthAgo = Date.now() - (31 * 24 * 60 * 60 * 1000);
274
- symbols[sym].sessions = symbols[sym].sessions.filter(s =>
275
- new Date(s.date).getTime() > oneMonthAgo
276
- );
277
-
278
- // Update symbol stats
279
- symbols[sym].stats.trades += supervisorState.performance.trades;
280
- symbols[sym].stats.wins += supervisorState.performance.wins;
281
- symbols[sym].stats.losses += supervisorState.performance.losses;
282
- symbols[sym].stats.pnl += supervisorState.performance.totalPnL;
283
- }
284
-
285
- // Update hourly stats (which hours perform best)
286
- const hourlyStats = { ...existing.hourlyStats };
287
- const hourKey = String(currentHour);
288
- if (!hourlyStats[hourKey]) {
289
- hourlyStats[hourKey] = { trades: 0, wins: 0, losses: 0, pnl: 0 };
290
- }
291
- hourlyStats[hourKey].trades += supervisorState.performance.trades;
292
- hourlyStats[hourKey].wins += supervisorState.performance.wins;
293
- hourlyStats[hourKey].losses += supervisorState.performance.losses;
294
- hourlyStats[hourKey].pnl += supervisorState.performance.totalPnL;
295
-
296
- // Update day of week stats
297
- const dayOfWeekStats = { ...existing.dayOfWeekStats };
298
- const dayKey = dayNames[currentDay];
299
- if (!dayOfWeekStats[dayKey]) {
300
- dayOfWeekStats[dayKey] = { trades: 0, wins: 0, losses: 0, pnl: 0 };
301
- }
302
- dayOfWeekStats[dayKey].trades += supervisorState.performance.trades;
303
- dayOfWeekStats[dayKey].wins += supervisorState.performance.wins;
304
- dayOfWeekStats[dayKey].losses += supervisorState.performance.losses;
305
- dayOfWeekStats[dayKey].pnl += supervisorState.performance.totalPnL;
306
-
307
- // Build strategy profile from data
308
- const strategyProfile = buildStrategyProfile(hourlyStats, dayOfWeekStats, existing.sessions);
309
-
310
- // Current session record
311
- const currentSession = {
312
- date: now.toISOString(),
313
- symbol: supervisorState.currentSymbol,
314
- hour: currentHour,
315
- dayOfWeek: dayNames[currentDay],
316
- trades: supervisorState.performance.trades,
317
- wins: supervisorState.performance.wins,
318
- losses: supervisorState.performance.losses,
319
- pnl: supervisorState.performance.totalPnL,
320
- maxWinStreak: supervisorState.performance.maxWinStreak,
321
- maxLossStreak: supervisorState.performance.maxLossStreak,
322
- optimizationsApplied: supervisorState.optimizations.length,
323
- levelsLearned: (supervisorState.symbolLevels || []).length
324
- };
325
-
326
- // Merge sessions (keep 1 month)
327
- const oneMonthAgo = Date.now() - (31 * 24 * 60 * 60 * 1000);
328
- const sessions = [...existing.sessions, currentSession].filter(s =>
329
- new Date(s.date).getTime() > oneMonthAgo
330
- );
331
-
332
- // Build data to save
333
- const dataToSave = {
334
- // Patterns - keep last 100 of each type
335
- winningPatterns: mergePatterns(existing.winningPatterns, supervisorState.winningPatterns, 100),
336
- losingPatterns: mergePatterns(existing.losingPatterns, supervisorState.losingPatterns, 100),
337
-
338
- // Optimizations history - keep last 50
339
- optimizations: [...existing.optimizations, ...supervisorState.optimizations].slice(-50),
340
-
341
- // Symbol-specific data
342
- symbols,
343
-
344
- // Full session history (1 month)
345
- sessions,
346
-
347
- // Performance by hour and day
348
- hourlyStats,
349
- dayOfWeekStats,
350
-
351
- // Strategy behavior profile
352
- strategyProfile,
353
-
354
- // Lifetime stats
355
- totalSessions: existing.totalSessions + 1,
356
- totalTrades: existing.totalTrades + supervisorState.performance.trades,
357
- totalWins: existing.totalWins + supervisorState.performance.wins,
358
- totalLosses: existing.totalLosses + supervisorState.performance.losses,
359
- lifetimePnL: existing.lifetimePnL + supervisorState.performance.totalPnL,
360
-
361
- // Metadata
362
- firstSession: existing.firstSession || now.toISOString(),
363
- lastUpdated: now.toISOString()
364
- };
365
-
366
- fs.writeFileSync(LEARNING_FILE, JSON.stringify(dataToSave, null, 2));
367
- return true;
368
- } catch (e) {
369
- return false;
370
- }
371
- };
372
-
373
- /**
374
- * Build strategy profile from historical data
375
- * Identifies best/worst hours, days, and patterns
376
- */
377
- const buildStrategyProfile = (hourlyStats, dayOfWeekStats, sessions) => {
378
- // Find best and worst hours
379
- const hours = Object.entries(hourlyStats)
380
- .map(([hour, stats]) => ({
381
- hour: parseInt(hour),
382
- winRate: stats.trades > 0 ? (stats.wins / stats.trades) * 100 : 0,
383
- pnl: stats.pnl,
384
- trades: stats.trades
385
- }))
386
- .filter(h => h.trades >= 3) // Need at least 3 trades to be significant
387
- .sort((a, b) => b.winRate - a.winRate);
388
-
389
- const bestHours = hours.slice(0, 3).map(h => h.hour);
390
- const worstHours = hours.slice(-3).map(h => h.hour);
391
-
392
- // Find best and worst days
393
- const days = Object.entries(dayOfWeekStats)
394
- .map(([day, stats]) => ({
395
- day,
396
- winRate: stats.trades > 0 ? (stats.wins / stats.trades) * 100 : 0,
397
- pnl: stats.pnl,
398
- trades: stats.trades
399
- }))
400
- .filter(d => d.trades >= 3)
401
- .sort((a, b) => b.winRate - a.winRate);
402
-
403
- const bestDays = days.slice(0, 2).map(d => d.day);
404
- const worstDays = days.slice(-2).map(d => d.day);
405
-
406
- // Calculate average streaks from sessions
407
- let totalWinStreaks = 0, totalLossStreaks = 0, streakCount = 0;
408
- for (const session of sessions) {
409
- if (session.maxWinStreak) totalWinStreaks += session.maxWinStreak;
410
- if (session.maxLossStreak) totalLossStreaks += session.maxLossStreak;
411
- streakCount++;
412
- }
413
-
414
- return {
415
- bestHours,
416
- worstHours,
417
- bestDays,
418
- worstDays,
419
- avgWinStreak: streakCount > 0 ? Math.round(totalWinStreaks / streakCount * 10) / 10 : 0,
420
- avgLossStreak: streakCount > 0 ? Math.round(totalLossStreaks / streakCount * 10) / 10 : 0,
421
- totalSessionsAnalyzed: sessions.length
422
- };
423
- };
424
-
425
- /**
426
- * Merge pattern arrays, keeping most recent unique entries
427
- */
428
- const mergePatterns = (existing, current, maxCount) => {
429
- const merged = [...existing, ...current];
430
-
431
- // Sort by timestamp descending (most recent first)
432
- merged.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
433
-
434
- // Keep only maxCount most recent
435
- return merged.slice(0, maxCount);
436
- };
437
-
438
- /**
439
- * Merge price levels, prioritizing most touched and most recent
440
- */
441
- const mergeLevels = (existing, current, maxCount) => {
442
- const merged = [...existing];
443
-
444
- for (const level of current) {
445
- const tickSize = 0.25;
446
- const existingIdx = merged.findIndex(l =>
447
- Math.abs(l.price - level.price) <= tickSize * 2
448
- );
449
-
450
- if (existingIdx >= 0) {
451
- // Update existing level
452
- merged[existingIdx].touches = (merged[existingIdx].touches || 1) + (level.touches || 1);
453
- merged[existingIdx].lastTouch = Math.max(merged[existingIdx].lastTouch || 0, level.lastTouch || level.timestamp || 0);
454
- merged[existingIdx].outcomes = [...(merged[existingIdx].outcomes || []), ...(level.outcomes || [])].slice(-20);
455
- } else {
456
- merged.push(level);
457
- }
458
- }
459
-
460
- // Sort by importance (touches * recency)
461
- const now = Date.now();
462
- merged.sort((a, b) => {
463
- const scoreA = (a.touches || 1) * (1 / (1 + (now - (a.lastTouch || a.timestamp || 0)) / 86400000));
464
- const scoreB = (b.touches || 1) * (1 / (1 + (now - (b.lastTouch || b.timestamp || 0)) / 86400000));
465
- return scoreB - scoreA;
466
- });
467
-
468
- return merged.slice(0, maxCount);
469
- };
470
-
471
- /**
472
- * DEPRECATED - moved inline
473
- */
474
- const mergePatternsDEP = (existing, current, maxCount) => {
475
- const merged = [...existing, ...current];
476
- merged.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
477
-
478
- // Keep only maxCount most recent
479
- return merged.slice(0, maxCount);
480
- };
481
-
482
- // Singleton supervisor state
483
- let supervisorState = {
484
- active: false,
485
- agents: [],
486
- strategy: null,
487
- service: null,
488
- accountId: null,
489
-
490
- // Current symbol being traded
491
- currentSymbol: null,
492
-
493
- // Real-time data (synced with strategy)
494
- ticks: [],
495
- signals: [],
496
- trades: [],
497
-
498
- // Symbol-specific levels learned this session
499
- symbolLevels: [],
500
-
501
- // Learning data
502
- winningPatterns: [],
503
- losingPatterns: [],
504
-
505
- // Performance tracking
506
- performance: {
507
- trades: 0,
508
- wins: 0,
509
- losses: 0,
510
- totalPnL: 0,
511
- maxDrawdown: 0,
512
- currentDrawdown: 0,
513
- peakPnL: 0,
514
- winStreak: 0,
515
- lossStreak: 0,
516
- maxWinStreak: 0,
517
- maxLossStreak: 0
518
- },
519
-
520
- // Optimization state
521
- optimizations: [],
522
- lastOptimizationTime: 0,
523
- optimizationInterval: 60000, // Analyze every 60 seconds
524
-
525
- // Current recommendations
526
- currentAdvice: {
527
- action: 'NORMAL',
528
- sizeMultiplier: 1.0,
529
- reason: 'Starting'
530
- },
531
-
532
- // Behavior history for graph (action over time)
533
- // Values: 0=PAUSE, 1=CAUTIOUS, 2=NORMAL, 3=AGGRESSIVE
534
- behaviorHistory: [],
535
- behaviorStartTime: null,
536
-
537
- // Lifetime stats loaded from previous sessions
538
- lifetimeStats: null,
539
-
540
- // Previous sessions memory (loaded on init)
541
- previousSessions: [],
542
-
543
- // Hourly performance tracking
544
- hourlyPerformance: {}
545
- };
546
-
547
- // Analysis interval
548
- let analysisInterval = null;
28
+ const {
29
+ DATA_DIR, LEARNING_FILE, loadLearningData, getSymbolData,
30
+ recordPriceLevel, analyzeNearbyLevels, saveLearningData,
31
+ buildStrategyProfile, mergePatterns, mergeLevels, clearLearningData
32
+ } = require('./supervisor-data');
549
33
 
550
34
  /**
551
35
  * Initialize supervisor with strategy and agents
@@ -946,168 +430,6 @@ const feedTradeResult = (trade) => {
946
430
  }
947
431
  };
948
432
 
949
- /**
950
- * Calculate trend strength from ticks
951
- * Compares short-term vs medium-term trend
952
- */
953
- const calculateTrendStrength = (ticks) => {
954
- if (!ticks || ticks.length < 20) return { strength: 0, direction: 'unknown' };
955
-
956
- const prices = ticks.map(t => t.price).filter(Boolean);
957
- if (prices.length < 20) return { strength: 0, direction: 'unknown' };
958
-
959
- // Short-term trend (last 20)
960
- const shortTerm = prices.slice(-20);
961
- const shortChange = shortTerm[shortTerm.length - 1] - shortTerm[0];
962
-
963
- // Medium-term trend (last 50 or all)
964
- const mediumTerm = prices.slice(-50);
965
- const mediumChange = mediumTerm[mediumTerm.length - 1] - mediumTerm[0];
966
-
967
- // Strength = how aligned are short and medium trends
968
- const aligned = (shortChange > 0 && mediumChange > 0) || (shortChange < 0 && mediumChange < 0);
969
- const avgRange = Math.abs(Math.max(...prices) - Math.min(...prices)) || 1;
970
-
971
- return {
972
- strength: aligned ? Math.min(1, (Math.abs(shortChange) + Math.abs(mediumChange)) / avgRange) : 0,
973
- direction: mediumChange > 0 ? 'bullish' : mediumChange < 0 ? 'bearish' : 'neutral',
974
- shortTermDirection: shortChange > 0 ? 'up' : shortChange < 0 ? 'down' : 'flat',
975
- aligned
976
- };
977
- };
978
-
979
- /**
980
- * Calculate recent price range
981
- */
982
- const calculateRecentRange = (ticks, count) => {
983
- if (!ticks || ticks.length === 0) return { high: 0, low: 0, range: 0 };
984
-
985
- const prices = ticks.slice(-count).map(t => t.price).filter(Boolean);
986
- if (prices.length === 0) return { high: 0, low: 0, range: 0 };
987
-
988
- const high = Math.max(...prices);
989
- const low = Math.min(...prices);
990
-
991
- return { high, low, range: high - low };
992
- };
993
-
994
- /**
995
- * Calculate price velocity (speed of movement)
996
- */
997
- const calculatePriceVelocity = (ticks, count) => {
998
- if (!ticks || ticks.length < 2) return { velocity: 0, acceleration: 0 };
999
-
1000
- const recentTicks = ticks.slice(-count);
1001
- if (recentTicks.length < 2) return { velocity: 0, acceleration: 0 };
1002
-
1003
- const prices = recentTicks.map(t => t.price).filter(Boolean);
1004
- if (prices.length < 2) return { velocity: 0, acceleration: 0 };
1005
-
1006
- // Velocity = price change per tick
1007
- const totalChange = prices[prices.length - 1] - prices[0];
1008
- const velocity = totalChange / prices.length;
1009
-
1010
- // Acceleration = change in velocity
1011
- const midPoint = Math.floor(prices.length / 2);
1012
- const firstHalfVelocity = (prices[midPoint] - prices[0]) / midPoint;
1013
- const secondHalfVelocity = (prices[prices.length - 1] - prices[midPoint]) / (prices.length - midPoint);
1014
- const acceleration = secondHalfVelocity - firstHalfVelocity;
1015
-
1016
- return { velocity, acceleration };
1017
- };
1018
-
1019
- /**
1020
- * Get current market session
1021
- */
1022
- const getMarketSession = (hour) => {
1023
- // US Eastern Time sessions (adjust if needed)
1024
- if (hour >= 9 && hour < 12) return 'morning';
1025
- if (hour >= 12 && hour < 14) return 'midday';
1026
- if (hour >= 14 && hour < 16) return 'afternoon';
1027
- if (hour >= 16 && hour < 18) return 'close';
1028
- if (hour >= 18 || hour < 9) return 'overnight';
1029
- return 'unknown';
1030
- };
1031
-
1032
- /**
1033
- * Get minutes since market open (9:30 AM ET)
1034
- */
1035
- const getMinutesSinceOpen = (hour) => {
1036
- const marketOpenHour = 9;
1037
- const marketOpenMinute = 30;
1038
- const now = new Date();
1039
- const currentMinutes = hour * 60 + now.getMinutes();
1040
- const openMinutes = marketOpenHour * 60 + marketOpenMinute;
1041
- return Math.max(0, currentMinutes - openMinutes);
1042
- };
1043
-
1044
- /**
1045
- * Analyze price level interaction
1046
- * Determines if entry was near support/resistance
1047
- */
1048
- const analyzePriceLevelInteraction = (entryPrice, ticks) => {
1049
- if (!ticks || ticks.length < 20) return null;
1050
-
1051
- const prices = ticks.map(t => t.price).filter(Boolean);
1052
- if (prices.length < 20) return null;
1053
-
1054
- const high = Math.max(...prices);
1055
- const low = Math.min(...prices);
1056
- const range = high - low || 1;
1057
-
1058
- // Position within range (0 = at low, 1 = at high)
1059
- const positionInRange = (entryPrice - low) / range;
1060
-
1061
- // Distance to recent high/low
1062
- const distanceToHigh = high - entryPrice;
1063
- const distanceToLow = entryPrice - low;
1064
-
1065
- // Determine level type
1066
- let levelType = 'middle';
1067
- if (positionInRange > 0.8) levelType = 'near_high';
1068
- else if (positionInRange < 0.2) levelType = 'near_low';
1069
- else if (positionInRange > 0.6) levelType = 'upper_middle';
1070
- else if (positionInRange < 0.4) levelType = 'lower_middle';
1071
-
1072
- return {
1073
- positionInRange: Math.round(positionInRange * 100) / 100,
1074
- distanceToHigh,
1075
- distanceToLow,
1076
- recentHigh: high,
1077
- recentLow: low,
1078
- levelType
1079
- };
1080
- };
1081
-
1082
- /**
1083
- * Determine what type of level this price represents
1084
- */
1085
- const determineLevelType = (price, ticks) => {
1086
- if (!ticks || ticks.length < 10) return 'unknown';
1087
-
1088
- const prices = ticks.map(t => t.price).filter(Boolean);
1089
- if (prices.length < 10) return 'unknown';
1090
-
1091
- const high = Math.max(...prices);
1092
- const low = Math.min(...prices);
1093
- const range = high - low || 1;
1094
- const tickSize = 0.25;
1095
-
1096
- // Check if near recent high (potential resistance)
1097
- if (Math.abs(price - high) <= tickSize * 4) return 'resistance';
1098
-
1099
- // Check if near recent low (potential support)
1100
- if (Math.abs(price - low) <= tickSize * 4) return 'support';
1101
-
1102
- // Check for breakout (above recent high)
1103
- if (price > high) return 'breakout_high';
1104
-
1105
- // Check for breakdown (below recent low)
1106
- if (price < low) return 'breakout_low';
1107
-
1108
- return 'middle';
1109
- };
1110
-
1111
433
  /**
1112
434
  * Learn from a completed trade
1113
435
  * Extracts patterns from winning and losing trades
@@ -1215,69 +537,6 @@ const getHourlyWinRate = (hour) => {
1215
537
  return null;
1216
538
  };
1217
539
 
1218
- /**
1219
- * Analyze price action from ticks
1220
- */
1221
- const analyzePriceAction = (ticks) => {
1222
- if (!ticks || ticks.length < 2) return { trend: 'unknown', strength: 0 };
1223
-
1224
- const prices = ticks.map(t => t.price).filter(Boolean);
1225
- if (prices.length < 2) return { trend: 'unknown', strength: 0 };
1226
-
1227
- const first = prices[0];
1228
- const last = prices[prices.length - 1];
1229
- const change = last - first;
1230
- const range = Math.max(...prices) - Math.min(...prices);
1231
-
1232
- return {
1233
- trend: change > 0 ? 'up' : change < 0 ? 'down' : 'flat',
1234
- strength: range > 0 ? Math.abs(change) / range : 0,
1235
- range,
1236
- change
1237
- };
1238
- };
1239
-
1240
- /**
1241
- * Analyze volume from ticks
1242
- */
1243
- const analyzeVolume = (ticks) => {
1244
- if (!ticks || ticks.length === 0) return { total: 0, avg: 0, trend: 'unknown' };
1245
-
1246
- const volumes = ticks.map(t => t.volume || 0);
1247
- const total = volumes.reduce((a, b) => a + b, 0);
1248
- const avg = total / volumes.length;
1249
-
1250
- // Compare first half vs second half
1251
- const mid = Math.floor(volumes.length / 2);
1252
- const firstHalf = volumes.slice(0, mid).reduce((a, b) => a + b, 0);
1253
- const secondHalf = volumes.slice(mid).reduce((a, b) => a + b, 0);
1254
-
1255
- return {
1256
- total,
1257
- avg,
1258
- trend: secondHalf > firstHalf * 1.2 ? 'increasing' : secondHalf < firstHalf * 0.8 ? 'decreasing' : 'stable'
1259
- };
1260
- };
1261
-
1262
- /**
1263
- * Calculate volatility from ticks
1264
- */
1265
- const calculateVolatility = (ticks) => {
1266
- if (!ticks || ticks.length < 2) return 0;
1267
-
1268
- const prices = ticks.map(t => t.price).filter(Boolean);
1269
- if (prices.length < 2) return 0;
1270
-
1271
- const returns = [];
1272
- for (let i = 1; i < prices.length; i++) {
1273
- returns.push((prices[i] - prices[i-1]) / prices[i-1]);
1274
- }
1275
-
1276
- const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
1277
- const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length;
1278
-
1279
- return Math.sqrt(variance);
1280
- };
1281
540
 
1282
541
  /**
1283
542
  * Main analysis and optimization loop
@@ -2019,20 +1278,6 @@ const getLifetimeStats = () => {
2019
1278
  };
2020
1279
  };
2021
1280
 
2022
- /**
2023
- * Clear all learned data (reset AI memory)
2024
- */
2025
- const clearLearningData = () => {
2026
- try {
2027
- if (fs.existsSync(LEARNING_FILE)) {
2028
- fs.unlinkSync(LEARNING_FILE);
2029
- }
2030
- return true;
2031
- } catch (e) {
2032
- return false;
2033
- }
2034
- };
2035
-
2036
1281
  module.exports = {
2037
1282
  // Core lifecycle
2038
1283
  initialize,
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Supervisor Data Management
3
+ * Handles persistence and data operations for AI learning
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const DATA_DIR = path.join(os.homedir(), '.hqx');
13
+ const LEARNING_FILE = path.join(DATA_DIR, 'ai-learning.json');
14
+
15
+ const loadLearningData = () => {
16
+ try {
17
+ if (!fs.existsSync(DATA_DIR)) {
18
+ fs.mkdirSync(DATA_DIR, { recursive: true });
19
+ }
20
+ if (fs.existsSync(LEARNING_FILE)) {
21
+ const data = JSON.parse(fs.readFileSync(LEARNING_FILE, 'utf8'));
22
+ const oneMonthAgo = Date.now() - (31 * 24 * 60 * 60 * 1000);
23
+ const sessions = (data.sessions || []).filter(s =>
24
+ new Date(s.date).getTime() > oneMonthAgo
25
+ );
26
+ return {
27
+ winningPatterns: data.winningPatterns || [],
28
+ losingPatterns: data.losingPatterns || [],
29
+ optimizations: data.optimizations || [],
30
+ symbols: data.symbols || {},
31
+ sessions,
32
+ hourlyStats: data.hourlyStats || {},
33
+ dayOfWeekStats: data.dayOfWeekStats || {},
34
+ strategyProfile: data.strategyProfile || {
35
+ bestHours: [], worstHours: [], avgWinStreak: 0, avgLossStreak: 0, preferredConditions: null
36
+ },
37
+ totalSessions: data.totalSessions || 0,
38
+ totalTrades: data.totalTrades || 0,
39
+ totalWins: data.totalWins || 0,
40
+ totalLosses: data.totalLosses || 0,
41
+ lifetimePnL: data.lifetimePnL || 0,
42
+ lastUpdated: data.lastUpdated || null,
43
+ firstSession: data.firstSession || null
44
+ };
45
+ }
46
+ } catch (e) { /* Silent fail */ }
47
+ return {
48
+ winningPatterns: [], losingPatterns: [], optimizations: [], symbols: {}, sessions: [],
49
+ hourlyStats: {}, dayOfWeekStats: {},
50
+ strategyProfile: { bestHours: [], worstHours: [], avgWinStreak: 0, avgLossStreak: 0, preferredConditions: null },
51
+ totalSessions: 0, totalTrades: 0, totalWins: 0, totalLosses: 0, lifetimePnL: 0,
52
+ lastUpdated: null, firstSession: null
53
+ };
54
+ };
55
+
56
+ const getSymbolData = (symbolName) => {
57
+ const data = loadLearningData();
58
+ if (!data.symbols[symbolName]) {
59
+ return {
60
+ name: symbolName, levels: { support: [], resistance: [] },
61
+ patterns: { winning: [], losing: [] }, stats: { trades: 0, wins: 0, avgWin: 0, avgLoss: 0 }
62
+ };
63
+ }
64
+ return data.symbols[symbolName];
65
+ };
66
+
67
+ const recordPriceLevel = (symbolName, price, type, outcome) => {
68
+ const data = loadLearningData();
69
+ if (!data.symbols[symbolName]) {
70
+ data.symbols[symbolName] = {
71
+ name: symbolName, levels: { support: [], resistance: [] },
72
+ patterns: { winning: [], losing: [] }, stats: { trades: 0, wins: 0, avgWin: 0, avgLoss: 0 }
73
+ };
74
+ }
75
+ const symbol = data.symbols[symbolName];
76
+ const levelKey = type === 'support' ? 'support' : 'resistance';
77
+ const existingLevel = symbol.levels[levelKey].find(l => Math.abs(l.price - price) <= 0.5);
78
+ if (existingLevel) {
79
+ existingLevel.touches++;
80
+ existingLevel.lastTouched = Date.now();
81
+ if (outcome === 'held') existingLevel.held++;
82
+ else existingLevel.broken++;
83
+ } else {
84
+ symbol.levels[levelKey].push({
85
+ price, touches: 1, held: outcome === 'held' ? 1 : 0,
86
+ broken: outcome === 'broken' ? 1 : 0, firstSeen: Date.now(), lastTouched: Date.now()
87
+ });
88
+ }
89
+ symbol.levels[levelKey].sort((a, b) => b.touches - a.touches);
90
+ if (symbol.levels[levelKey].length > 20) {
91
+ symbol.levels[levelKey] = symbol.levels[levelKey].slice(0, 20);
92
+ }
93
+ saveLearningData(data);
94
+ };
95
+
96
+ const analyzeNearbyLevels = (symbolName, currentPrice) => {
97
+ const symbolData = getSymbolData(symbolName);
98
+ const nearby = { support: [], resistance: [] };
99
+ const range = 10;
100
+ symbolData.levels.support.forEach(level => {
101
+ if (Math.abs(level.price - currentPrice) <= range) {
102
+ nearby.support.push({ ...level, distance: currentPrice - level.price });
103
+ }
104
+ });
105
+ symbolData.levels.resistance.forEach(level => {
106
+ if (Math.abs(level.price - currentPrice) <= range) {
107
+ nearby.resistance.push({ ...level, distance: level.price - currentPrice });
108
+ }
109
+ });
110
+ nearby.support.sort((a, b) => a.distance - b.distance);
111
+ nearby.resistance.sort((a, b) => a.distance - b.distance);
112
+ return nearby;
113
+ };
114
+
115
+ const saveLearningData = (data) => {
116
+ try {
117
+ if (!fs.existsSync(DATA_DIR)) {
118
+ fs.mkdirSync(DATA_DIR, { recursive: true });
119
+ }
120
+ data.lastUpdated = new Date().toISOString();
121
+ fs.writeFileSync(LEARNING_FILE, JSON.stringify(data, null, 2));
122
+ return true;
123
+ } catch (e) {
124
+ return false;
125
+ }
126
+ };
127
+
128
+ const buildStrategyProfile = (hourlyStats, dayOfWeekStats, sessions) => {
129
+ const profile = { bestHours: [], worstHours: [], avgWinStreak: 0, avgLossStreak: 0, preferredConditions: null };
130
+ const hourlyArray = Object.entries(hourlyStats)
131
+ .filter(([_, stats]) => stats.trades >= 5)
132
+ .map(([hour, stats]) => ({ hour: parseInt(hour), winRate: stats.wins / stats.trades, trades: stats.trades, pnl: stats.pnl }));
133
+ hourlyArray.sort((a, b) => b.winRate - a.winRate);
134
+ profile.bestHours = hourlyArray.slice(0, 3).map(h => ({ hour: h.hour, winRate: Math.round(h.winRate * 100) }));
135
+ profile.worstHours = hourlyArray.slice(-3).reverse().map(h => ({ hour: h.hour, winRate: Math.round(h.winRate * 100) }));
136
+ if (sessions.length >= 5) {
137
+ const winStreaks = sessions.map(s => s.maxWinStreak || 0).filter(Boolean);
138
+ const lossStreaks = sessions.map(s => s.maxLossStreak || 0).filter(Boolean);
139
+ profile.avgWinStreak = winStreaks.length > 0 ? winStreaks.reduce((a, b) => a + b, 0) / winStreaks.length : 0;
140
+ profile.avgLossStreak = lossStreaks.length > 0 ? lossStreaks.reduce((a, b) => a + b, 0) / lossStreaks.length : 0;
141
+ }
142
+ return profile;
143
+ };
144
+
145
+ const mergePatterns = (existing, current, maxCount) => {
146
+ const merged = [...existing];
147
+ current.forEach(pattern => {
148
+ const similar = merged.find(p => p.timeContext?.hour === pattern.timeContext?.hour && p.direction === pattern.direction);
149
+ if (!similar) merged.push(pattern);
150
+ });
151
+ merged.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
152
+ return merged.slice(0, maxCount);
153
+ };
154
+
155
+ const mergeLevels = (existing, current, maxCount) => {
156
+ const merged = [...existing];
157
+ current.forEach(level => {
158
+ const similar = merged.find(l => Math.abs(l.price - level.price) <= 0.5);
159
+ if (similar) {
160
+ similar.touches += level.touches || 1;
161
+ similar.held += level.held || 0;
162
+ similar.broken += level.broken || 0;
163
+ similar.lastTouched = Math.max(similar.lastTouched || 0, level.lastTouched || Date.now());
164
+ } else {
165
+ merged.push(level);
166
+ }
167
+ });
168
+ merged.sort((a, b) => b.touches - a.touches);
169
+ return merged.slice(0, maxCount);
170
+ };
171
+
172
+ const clearLearningData = () => {
173
+ try {
174
+ if (fs.existsSync(LEARNING_FILE)) {
175
+ fs.unlinkSync(LEARNING_FILE);
176
+ }
177
+ return true;
178
+ } catch (e) {
179
+ return false;
180
+ }
181
+ };
182
+
183
+ module.exports = {
184
+ DATA_DIR,
185
+ LEARNING_FILE,
186
+ loadLearningData,
187
+ getSymbolData,
188
+ recordPriceLevel,
189
+ analyzeNearbyLevels,
190
+ saveLearningData,
191
+ buildStrategyProfile,
192
+ mergePatterns,
193
+ mergeLevels,
194
+ clearLearningData
195
+ };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Supervisor Utility Functions
3
+ * Pure functions for market analysis - no state dependencies
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const calculateTrendStrength = (ticks) => {
9
+ if (!ticks || ticks.length < 20) return { strength: 0, direction: 'unknown' };
10
+ const prices = ticks.map(t => t.price).filter(Boolean);
11
+ if (prices.length < 20) return { strength: 0, direction: 'unknown' };
12
+ const shortTerm = prices.slice(-20);
13
+ const shortChange = shortTerm[shortTerm.length - 1] - shortTerm[0];
14
+ const mediumTerm = prices.slice(-50);
15
+ const mediumChange = mediumTerm[mediumTerm.length - 1] - mediumTerm[0];
16
+ const aligned = (shortChange > 0 && mediumChange > 0) || (shortChange < 0 && mediumChange < 0);
17
+ const avgRange = Math.abs(Math.max(...prices) - Math.min(...prices)) || 1;
18
+ return {
19
+ strength: aligned ? Math.min(1, (Math.abs(shortChange) + Math.abs(mediumChange)) / avgRange) : 0,
20
+ direction: mediumChange > 0 ? 'bullish' : mediumChange < 0 ? 'bearish' : 'neutral',
21
+ shortTermDirection: shortChange > 0 ? 'up' : shortChange < 0 ? 'down' : 'flat',
22
+ aligned
23
+ };
24
+ };
25
+
26
+ const calculateRecentRange = (ticks, count) => {
27
+ if (!ticks || ticks.length === 0) return { high: 0, low: 0, range: 0 };
28
+ const prices = ticks.slice(-count).map(t => t.price).filter(Boolean);
29
+ if (prices.length === 0) return { high: 0, low: 0, range: 0 };
30
+ const high = Math.max(...prices);
31
+ const low = Math.min(...prices);
32
+ return { high, low, range: high - low };
33
+ };
34
+
35
+ const calculatePriceVelocity = (ticks, count) => {
36
+ if (!ticks || ticks.length < 2) return { velocity: 0, acceleration: 0 };
37
+ const recentTicks = ticks.slice(-count);
38
+ if (recentTicks.length < 2) return { velocity: 0, acceleration: 0 };
39
+ const prices = recentTicks.map(t => t.price).filter(Boolean);
40
+ if (prices.length < 2) return { velocity: 0, acceleration: 0 };
41
+ const totalChange = prices[prices.length - 1] - prices[0];
42
+ const velocity = totalChange / prices.length;
43
+ const midPoint = Math.floor(prices.length / 2);
44
+ const firstHalfVelocity = (prices[midPoint] - prices[0]) / midPoint;
45
+ const secondHalfVelocity = (prices[prices.length - 1] - prices[midPoint]) / (prices.length - midPoint);
46
+ const acceleration = secondHalfVelocity - firstHalfVelocity;
47
+ return { velocity, acceleration };
48
+ };
49
+
50
+ const getMarketSession = (hour) => {
51
+ if (hour >= 9 && hour < 12) return 'morning';
52
+ if (hour >= 12 && hour < 14) return 'midday';
53
+ if (hour >= 14 && hour < 16) return 'afternoon';
54
+ if (hour >= 16 && hour < 18) return 'close';
55
+ if (hour >= 18 || hour < 9) return 'overnight';
56
+ return 'unknown';
57
+ };
58
+
59
+ const getMinutesSinceOpen = (hour) => {
60
+ const now = new Date();
61
+ const currentMinutes = hour * 60 + now.getMinutes();
62
+ const openMinutes = 9 * 60 + 30;
63
+ return Math.max(0, currentMinutes - openMinutes);
64
+ };
65
+
66
+ const analyzePriceLevelInteraction = (entryPrice, ticks) => {
67
+ if (!ticks || ticks.length < 20) return null;
68
+ const prices = ticks.map(t => t.price).filter(Boolean);
69
+ if (prices.length < 20) return null;
70
+ const high = Math.max(...prices);
71
+ const low = Math.min(...prices);
72
+ const range = high - low || 1;
73
+ const positionInRange = (entryPrice - low) / range;
74
+ let levelType = 'middle';
75
+ if (positionInRange > 0.8) levelType = 'near_high';
76
+ else if (positionInRange < 0.2) levelType = 'near_low';
77
+ else if (positionInRange > 0.6) levelType = 'upper_middle';
78
+ else if (positionInRange < 0.4) levelType = 'lower_middle';
79
+ return {
80
+ positionInRange: Math.round(positionInRange * 100) / 100,
81
+ distanceToHigh: high - entryPrice,
82
+ distanceToLow: entryPrice - low,
83
+ recentHigh: high,
84
+ recentLow: low,
85
+ levelType
86
+ };
87
+ };
88
+
89
+ const determineLevelType = (price, ticks) => {
90
+ if (!ticks || ticks.length < 10) return 'unknown';
91
+ const prices = ticks.map(t => t.price).filter(Boolean);
92
+ if (prices.length < 10) return 'unknown';
93
+ const high = Math.max(...prices);
94
+ const low = Math.min(...prices);
95
+ const tickSize = 0.25;
96
+ if (Math.abs(price - high) <= tickSize * 4) return 'resistance';
97
+ if (Math.abs(price - low) <= tickSize * 4) return 'support';
98
+ if (price > high) return 'breakout_high';
99
+ if (price < low) return 'breakout_low';
100
+ return 'middle';
101
+ };
102
+
103
+ const analyzePriceAction = (ticks) => {
104
+ if (!ticks || ticks.length < 2) return { trend: 'unknown', strength: 0 };
105
+ const prices = ticks.map(t => t.price).filter(Boolean);
106
+ if (prices.length < 2) return { trend: 'unknown', strength: 0 };
107
+ const first = prices[0];
108
+ const last = prices[prices.length - 1];
109
+ const change = last - first;
110
+ const range = Math.max(...prices) - Math.min(...prices);
111
+ return {
112
+ trend: change > 0 ? 'up' : change < 0 ? 'down' : 'flat',
113
+ strength: range > 0 ? Math.abs(change) / range : 0,
114
+ range,
115
+ change
116
+ };
117
+ };
118
+
119
+ const analyzeVolume = (ticks) => {
120
+ if (!ticks || ticks.length === 0) return { total: 0, avg: 0, trend: 'unknown' };
121
+ const volumes = ticks.map(t => t.volume || 0);
122
+ const total = volumes.reduce((a, b) => a + b, 0);
123
+ const avg = total / volumes.length;
124
+ const mid = Math.floor(volumes.length / 2);
125
+ const firstHalf = volumes.slice(0, mid).reduce((a, b) => a + b, 0);
126
+ const secondHalf = volumes.slice(mid).reduce((a, b) => a + b, 0);
127
+ return {
128
+ total,
129
+ avg,
130
+ trend: secondHalf > firstHalf * 1.2 ? 'increasing' : secondHalf < firstHalf * 0.8 ? 'decreasing' : 'stable'
131
+ };
132
+ };
133
+
134
+ const calculateVolatility = (ticks) => {
135
+ if (!ticks || ticks.length < 2) return 0;
136
+ const prices = ticks.map(t => t.price).filter(Boolean);
137
+ if (prices.length < 2) return 0;
138
+ const returns = [];
139
+ for (let i = 1; i < prices.length; i++) {
140
+ returns.push((prices[i] - prices[i-1]) / prices[i-1]);
141
+ }
142
+ const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
143
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length;
144
+ return Math.sqrt(variance);
145
+ };
146
+
147
+ module.exports = {
148
+ calculateTrendStrength,
149
+ calculateRecentRange,
150
+ calculatePriceVelocity,
151
+ getMarketSession,
152
+ getMinutesSinceOpen,
153
+ analyzePriceLevelInteraction,
154
+ determineLevelType,
155
+ analyzePriceAction,
156
+ analyzeVolume,
157
+ calculateVolatility
158
+ };