hedgequantx 2.5.36 → 2.5.37

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.36",
3
+ "version": "2.5.37",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -16,6 +16,10 @@ const { M1 } = require('../../../dist/lib/m/s1');
16
16
  const { MarketDataFeed } = require('../../../dist/lib/data');
17
17
  const { algoLogger } = require('./logger');
18
18
 
19
+ // AI Strategy Supervisor - observes, learns, and optimizes the strategy
20
+ const aiService = require('../../services/ai');
21
+ const StrategySupervisor = require('../../services/ai/strategy-supervisor');
22
+
19
23
 
20
24
 
21
25
  /**
@@ -167,6 +171,7 @@ const configureAlgo = async (account, contract) => {
167
171
  /**
168
172
  * Launch algo trading - HQX Ultra Scalping Strategy
169
173
  * Real-time market data + Strategy signals + Auto order execution
174
+ * AI Supervision: All connected agents monitor and supervise trading
170
175
  */
171
176
  const launchAlgo = async (service, account, contract, config) => {
172
177
  const { contracts, dailyTarget, maxRisk, showName } = config;
@@ -197,7 +202,9 @@ const launchAlgo = async (service, account, contract, config) => {
197
202
  losses: 0,
198
203
  latency: 0,
199
204
  connected: false,
200
- startTime: Date.now()
205
+ startTime: Date.now(),
206
+ aiSupervision: false,
207
+ aiMode: null
201
208
  };
202
209
 
203
210
  let running = true;
@@ -213,6 +220,14 @@ const launchAlgo = async (service, account, contract, config) => {
213
220
  const strategy = M1;
214
221
  strategy.initialize(contractId, tickSize, tickValue);
215
222
 
223
+ // Initialize AI Strategy Supervisor - agents observe, learn & optimize
224
+ const aiAgents = aiService.getAgents();
225
+ if (aiAgents.length > 0) {
226
+ const supervisorResult = StrategySupervisor.initialize(strategy, aiAgents, service, account.accountId);
227
+ stats.aiSupervision = supervisorResult.success;
228
+ stats.aiMode = supervisorResult.mode;
229
+ }
230
+
216
231
  // Initialize Market Data Feed
217
232
  const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
218
233
 
@@ -225,17 +240,44 @@ const launchAlgo = async (service, account, contract, config) => {
225
240
  algoLogger.engineStarting(ui, connectionType, dailyTarget, maxRisk);
226
241
  algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
227
242
 
243
+ // Log AI supervision status
244
+ if (stats.aiSupervision) {
245
+ algoLogger.info(ui, 'AI SUPERVISION', `${aiAgents.length} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
246
+ }
247
+
228
248
  // Handle strategy signals
229
249
  strategy.on('signal', async (signal) => {
230
250
  if (!running || pendingOrder || currentPosition !== 0) return;
231
251
 
232
252
  const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
233
253
 
254
+ // Feed signal to AI supervisor (agents observe the signal)
255
+ if (stats.aiSupervision) {
256
+ StrategySupervisor.feedSignal({ direction, entry, stopLoss, takeProfit, confidence });
257
+
258
+ // Check AI advice - agents may recommend caution based on learned patterns
259
+ const advice = StrategySupervisor.shouldTrade();
260
+ if (!advice.proceed) {
261
+ algoLogger.info(ui, 'AI HOLD', advice.reason);
262
+ return; // Skip - agents learned this pattern leads to losses
263
+ }
264
+ }
265
+
234
266
  // Calculate position size with kelly
235
- const kelly = Math.min(0.25, confidence);
236
- const riskAmount = Math.round(maxRisk * kelly);
237
- const riskPct = Math.round((riskAmount / maxRisk) * 100);
267
+ let kelly = Math.min(0.25, confidence);
268
+ let riskAmount = Math.round(maxRisk * kelly);
238
269
 
270
+ // AI may adjust size based on learning
271
+ if (stats.aiSupervision) {
272
+ const advice = StrategySupervisor.getCurrentAdvice();
273
+ if (advice.sizeMultiplier && advice.sizeMultiplier !== 1.0) {
274
+ kelly = kelly * advice.sizeMultiplier;
275
+ riskAmount = Math.round(riskAmount * advice.sizeMultiplier);
276
+ algoLogger.info(ui, 'AI ADJUST', `Size x${advice.sizeMultiplier.toFixed(2)} - ${advice.reason}`);
277
+ }
278
+ }
279
+
280
+ const riskPct = Math.round((riskAmount / maxRisk) * 100);
239
281
  algoLogger.positionSized(ui, contracts, kelly, riskAmount, riskPct);
240
282
 
241
283
  // Place order via API
@@ -319,6 +361,11 @@ const launchAlgo = async (service, account, contract, config) => {
319
361
  timestamp: tick.timestamp || Date.now()
320
362
  };
321
363
 
364
+ // Feed tick to AI supervisor (agents observe same data as strategy)
365
+ if (stats.aiSupervision) {
366
+ StrategySupervisor.feedTick(tickData);
367
+ }
368
+
322
369
  strategy.processTick(tickData);
323
370
 
324
371
  stats.latency = Date.now() - latencyStart;
@@ -443,6 +490,25 @@ const launchAlgo = async (service, account, contract, config) => {
443
490
 
444
491
  // Record in strategy for adaptation
445
492
  strategy.recordTradeResult(pnl);
493
+
494
+ // Feed trade result to AI supervisor - THIS IS WHERE AGENTS LEARN
495
+ if (stats.aiSupervision) {
496
+ StrategySupervisor.feedTradeResult({
497
+ side,
498
+ qty: contracts,
499
+ price: exitPrice,
500
+ pnl,
501
+ symbol: symbolName,
502
+ direction: side
503
+ });
504
+
505
+ // Log if AI learned something
506
+ const status = StrategySupervisor.getStatus();
507
+ if (status.patternsLearned.winning + status.patternsLearned.losing > 0) {
508
+ algoLogger.info(ui, 'AI LEARNING',
509
+ `${status.patternsLearned.winning}W/${status.patternsLearned.losing}L patterns`);
510
+ }
511
+ }
446
512
  }
447
513
  }
448
514
  } catch (e) {
@@ -510,6 +576,15 @@ const launchAlgo = async (service, account, contract, config) => {
510
576
  clearInterval(refreshInterval);
511
577
  clearInterval(pnlInterval);
512
578
 
579
+ // Stop AI Supervisor and get learning summary
580
+ if (stats.aiSupervision) {
581
+ const aiSummary = StrategySupervisor.stop();
582
+ stats.aiLearning = {
583
+ optimizations: aiSummary.optimizationsApplied || 0,
584
+ patternsLearned: (aiSummary.winningPatterns || 0) + (aiSummary.losingPatterns || 0)
585
+ };
586
+ }
587
+
513
588
  // Disconnect market feed with timeout
514
589
  try {
515
590
  await Promise.race([
@@ -308,6 +308,145 @@ Analyze and provide recommendation.`;
308
308
  }
309
309
  };
310
310
 
311
+ /**
312
+ * Analyze strategy performance and suggest optimizations
313
+ * Called periodically to help the strategy improve
314
+ *
315
+ * @param {Object} agent - AI agent
316
+ * @param {Object} performanceData - Strategy performance data
317
+ * @returns {Promise<Object|null>} Optimization suggestions
318
+ */
319
+ const analyzePerformance = async (agent, performanceData) => {
320
+ if (!agent || !performanceData) return null;
321
+
322
+ const systemPrompt = `You are an AI supervisor for HQX Ultra Scalping, a professional prop firm futures trading strategy.
323
+
324
+ The strategy uses advanced mathematical models:
325
+ - Order flow analysis (delta, cumulative delta, absorption)
326
+ - Market microstructure (bid/ask imbalance, volume profile)
327
+ - Statistical edge detection (z-score, standard deviation bands)
328
+ - Dynamic risk management (Kelly criterion, volatility-adjusted sizing)
329
+
330
+ Your job is to analyze performance data and suggest parameter optimizations.
331
+ Be precise and actionable. Focus on improving win rate, reducing drawdown, and optimizing risk/reward.
332
+
333
+ Respond ONLY in valid JSON format:
334
+ {
335
+ "assessment": "brief performance assessment",
336
+ "winRateAnalysis": "analysis of win/loss patterns",
337
+ "riskAnalysis": "analysis of risk management",
338
+ "optimizations": [
339
+ { "param": "parameter_name", "current": "current_value", "suggested": "new_value", "reason": "why" }
340
+ ],
341
+ "marketCondition": "trending|ranging|volatile|calm",
342
+ "confidence": 0-100
343
+ }`;
344
+
345
+ const prompt = `STRATEGY PERFORMANCE DATA - ANALYZE AND OPTIMIZE
346
+
347
+ Session Stats:
348
+ - Trades: ${performanceData.trades || 0}
349
+ - Wins: ${performanceData.wins || 0}
350
+ - Losses: ${performanceData.losses || 0}
351
+ - Win Rate: ${performanceData.winRate ? (performanceData.winRate * 100).toFixed(1) + '%' : 'N/A'}
352
+ - Total P&L: $${performanceData.pnl?.toFixed(2) || '0.00'}
353
+ - Avg Win: $${performanceData.avgWin?.toFixed(2) || 'N/A'}
354
+ - Avg Loss: $${performanceData.avgLoss?.toFixed(2) || 'N/A'}
355
+ - Largest Win: $${performanceData.largestWin?.toFixed(2) || 'N/A'}
356
+ - Largest Loss: $${performanceData.largestLoss?.toFixed(2) || 'N/A'}
357
+ - Max Drawdown: $${performanceData.maxDrawdown?.toFixed(2) || 'N/A'}
358
+ - Profit Factor: ${performanceData.profitFactor?.toFixed(2) || 'N/A'}
359
+
360
+ Current Parameters:
361
+ - Position Size: ${performanceData.positionSize || 'N/A'} contracts
362
+ - Daily Target: $${performanceData.dailyTarget || 'N/A'}
363
+ - Max Risk: $${performanceData.maxRisk || 'N/A'}
364
+ - Symbol: ${performanceData.symbol || 'N/A'}
365
+
366
+ Recent Trades:
367
+ ${performanceData.recentTrades?.map(t =>
368
+ `- ${t.side} ${t.qty}x @ ${t.price} → P&L: $${t.pnl?.toFixed(2) || 'N/A'}`
369
+ ).join('\n') || 'No recent trades'}
370
+
371
+ Market Context:
372
+ - Volatility: ${performanceData.volatility || 'N/A'}
373
+ - Trend: ${performanceData.trend || 'N/A'}
374
+ - Session: ${performanceData.session || 'N/A'}
375
+
376
+ Analyze and suggest optimizations to improve performance.`;
377
+
378
+ try {
379
+ const response = await callAI(agent, prompt, systemPrompt);
380
+ if (!response) return null;
381
+
382
+ // Parse JSON from response
383
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
384
+ if (jsonMatch) {
385
+ return JSON.parse(jsonMatch[0]);
386
+ }
387
+
388
+ return null;
389
+ } catch (error) {
390
+ return null;
391
+ }
392
+ };
393
+
394
+ /**
395
+ * Get real-time trading advice based on current market conditions
396
+ *
397
+ * @param {Object} agent - AI agent
398
+ * @param {Object} marketData - Current market data
399
+ * @returns {Promise<Object|null>} Trading advice
400
+ */
401
+ const getMarketAdvice = async (agent, marketData) => {
402
+ if (!agent || !marketData) return null;
403
+
404
+ const systemPrompt = `You are an AI supervisor for HQX Ultra Scalping futures strategy.
405
+ Analyze real-time market data and provide actionable advice.
406
+ Be concise and precise. The strategy will use your recommendations.
407
+
408
+ Respond ONLY in valid JSON:
409
+ {
410
+ "action": "AGGRESSIVE|NORMAL|CAUTIOUS|PAUSE",
411
+ "sizeMultiplier": 0.5-1.5,
412
+ "reason": "brief reason",
413
+ "confidence": 0-100
414
+ }`;
415
+
416
+ const prompt = `REAL-TIME MARKET ANALYSIS
417
+
418
+ Current Price: ${marketData.price || 'N/A'}
419
+ Bid: ${marketData.bid || 'N/A'} | Ask: ${marketData.ask || 'N/A'}
420
+ Spread: ${marketData.spread || 'N/A'}
421
+ Volume: ${marketData.volume || 'N/A'}
422
+ Delta: ${marketData.delta || 'N/A'}
423
+ Volatility: ${marketData.volatility || 'N/A'}
424
+
425
+ Recent Price Action:
426
+ - High: ${marketData.high || 'N/A'}
427
+ - Low: ${marketData.low || 'N/A'}
428
+ - Range: ${marketData.range || 'N/A'}
429
+
430
+ Current Position: ${marketData.position || 'FLAT'}
431
+ Session P&L: $${marketData.pnl?.toFixed(2) || '0.00'}
432
+
433
+ What should the strategy do?`;
434
+
435
+ try {
436
+ const response = await callAI(agent, prompt, systemPrompt);
437
+ if (!response) return null;
438
+
439
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
440
+ if (jsonMatch) {
441
+ return JSON.parse(jsonMatch[0]);
442
+ }
443
+
444
+ return null;
445
+ } catch (error) {
446
+ return null;
447
+ }
448
+ };
449
+
311
450
  /**
312
451
  * Fetch available models from Anthropic API (API Key auth)
313
452
  * @param {string} apiKey - API key
@@ -429,6 +568,8 @@ const fetchOpenAIModels = async (endpoint, apiKey) => {
429
568
  module.exports = {
430
569
  callAI,
431
570
  analyzeTrading,
571
+ analyzePerformance,
572
+ getMarketAdvice,
432
573
  callOpenAICompatible,
433
574
  callAnthropic,
434
575
  callGemini,
@@ -0,0 +1,675 @@
1
+ /**
2
+ * AI Strategy Supervisor
3
+ *
4
+ * Observes, learns from, and optimizes the HQX Ultra Scalping strategy in real-time.
5
+ *
6
+ * FUNCTIONS:
7
+ * 1. OBSERVE - Receive all market data, signals, and trades in real-time
8
+ * 2. LEARN - Analyze winning/losing trades to identify patterns
9
+ * 3. OPTIMIZE - Suggest and apply parameter improvements
10
+ * 4. SUPERVISE - Monitor risk and intervene when necessary
11
+ *
12
+ * In CONSENSUS mode (2+ agents), ALL agents must agree before applying changes.
13
+ */
14
+
15
+ const { analyzePerformance, getMarketAdvice, callAI } = require('./client');
16
+
17
+ // Singleton supervisor state
18
+ let supervisorState = {
19
+ active: false,
20
+ agents: [],
21
+ strategy: null,
22
+ service: null,
23
+ accountId: null,
24
+
25
+ // Real-time data (synced with strategy)
26
+ ticks: [],
27
+ signals: [],
28
+ trades: [],
29
+
30
+ // Learning data
31
+ winningPatterns: [],
32
+ losingPatterns: [],
33
+
34
+ // Performance tracking
35
+ performance: {
36
+ trades: 0,
37
+ wins: 0,
38
+ losses: 0,
39
+ totalPnL: 0,
40
+ maxDrawdown: 0,
41
+ currentDrawdown: 0,
42
+ peakPnL: 0,
43
+ winStreak: 0,
44
+ lossStreak: 0,
45
+ maxWinStreak: 0,
46
+ maxLossStreak: 0
47
+ },
48
+
49
+ // Optimization state
50
+ optimizations: [],
51
+ lastOptimizationTime: 0,
52
+ optimizationInterval: 60000, // Analyze every 60 seconds
53
+
54
+ // Current recommendations
55
+ currentAdvice: {
56
+ action: 'NORMAL',
57
+ sizeMultiplier: 1.0,
58
+ reason: 'Starting'
59
+ }
60
+ };
61
+
62
+ // Analysis interval
63
+ let analysisInterval = null;
64
+
65
+ /**
66
+ * Initialize supervisor with strategy and agents
67
+ */
68
+ const initialize = (strategy, agents, service, accountId) => {
69
+ supervisorState = {
70
+ ...supervisorState,
71
+ active: true,
72
+ agents: agents || [],
73
+ strategy,
74
+ service,
75
+ accountId,
76
+ ticks: [],
77
+ signals: [],
78
+ trades: [],
79
+ winningPatterns: [],
80
+ losingPatterns: [],
81
+ performance: {
82
+ trades: 0,
83
+ wins: 0,
84
+ losses: 0,
85
+ totalPnL: 0,
86
+ maxDrawdown: 0,
87
+ currentDrawdown: 0,
88
+ peakPnL: 0,
89
+ winStreak: 0,
90
+ lossStreak: 0,
91
+ maxWinStreak: 0,
92
+ maxLossStreak: 0
93
+ },
94
+ optimizations: [],
95
+ lastOptimizationTime: Date.now()
96
+ };
97
+
98
+ // Start continuous analysis loop
99
+ if (analysisInterval) clearInterval(analysisInterval);
100
+ analysisInterval = setInterval(analyzeAndOptimize, supervisorState.optimizationInterval);
101
+
102
+ return {
103
+ success: true,
104
+ agents: agents.length,
105
+ mode: agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL'
106
+ };
107
+ };
108
+
109
+ /**
110
+ * Stop supervisor
111
+ */
112
+ const stop = () => {
113
+ if (analysisInterval) {
114
+ clearInterval(analysisInterval);
115
+ analysisInterval = null;
116
+ }
117
+
118
+ const summary = {
119
+ ...supervisorState.performance,
120
+ optimizationsApplied: supervisorState.optimizations.length,
121
+ winningPatterns: supervisorState.winningPatterns.length,
122
+ losingPatterns: supervisorState.losingPatterns.length
123
+ };
124
+
125
+ supervisorState.active = false;
126
+
127
+ return summary;
128
+ };
129
+
130
+ /**
131
+ * Feed tick data (called on every market tick)
132
+ */
133
+ const feedTick = (tick) => {
134
+ if (!supervisorState.active) return;
135
+
136
+ supervisorState.ticks.push({
137
+ ...tick,
138
+ timestamp: Date.now()
139
+ });
140
+
141
+ // Keep last 5000 ticks for pattern analysis
142
+ if (supervisorState.ticks.length > 5000) {
143
+ supervisorState.ticks = supervisorState.ticks.slice(-5000);
144
+ }
145
+ };
146
+
147
+ /**
148
+ * Feed signal data (called when strategy generates a signal)
149
+ */
150
+ const feedSignal = (signal) => {
151
+ if (!supervisorState.active) return;
152
+
153
+ const signalData = {
154
+ ...signal,
155
+ timestamp: Date.now(),
156
+ ticksContext: supervisorState.ticks.slice(-50) // Last 50 ticks before signal
157
+ };
158
+
159
+ supervisorState.signals.push(signalData);
160
+
161
+ // Keep last 100 signals
162
+ if (supervisorState.signals.length > 100) {
163
+ supervisorState.signals = supervisorState.signals.slice(-100);
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Feed trade result (called when a trade completes)
169
+ * This is where LEARNING happens
170
+ */
171
+ const feedTradeResult = (trade) => {
172
+ if (!supervisorState.active) return;
173
+
174
+ const tradeData = {
175
+ ...trade,
176
+ timestamp: Date.now(),
177
+ // Capture context at time of trade
178
+ ticksBefore: supervisorState.ticks.slice(-100),
179
+ signalUsed: supervisorState.signals[supervisorState.signals.length - 1] || null
180
+ };
181
+
182
+ supervisorState.trades.push(tradeData);
183
+
184
+ // Update performance metrics
185
+ const perf = supervisorState.performance;
186
+ perf.trades++;
187
+ perf.totalPnL += trade.pnl || 0;
188
+
189
+ if (trade.pnl > 0) {
190
+ perf.wins++;
191
+ perf.winStreak++;
192
+ perf.lossStreak = 0;
193
+ perf.maxWinStreak = Math.max(perf.maxWinStreak, perf.winStreak);
194
+
195
+ // Learn from winning trade
196
+ learnFromTrade(tradeData, 'win');
197
+ } else if (trade.pnl < 0) {
198
+ perf.losses++;
199
+ perf.lossStreak++;
200
+ perf.winStreak = 0;
201
+ perf.maxLossStreak = Math.max(perf.maxLossStreak, perf.lossStreak);
202
+
203
+ // Learn from losing trade
204
+ learnFromTrade(tradeData, 'loss');
205
+ }
206
+
207
+ // Update drawdown
208
+ if (perf.totalPnL > perf.peakPnL) {
209
+ perf.peakPnL = perf.totalPnL;
210
+ perf.currentDrawdown = 0;
211
+ } else {
212
+ perf.currentDrawdown = perf.peakPnL - perf.totalPnL;
213
+ perf.maxDrawdown = Math.max(perf.maxDrawdown, perf.currentDrawdown);
214
+ }
215
+
216
+ // Trigger immediate analysis after losing streaks
217
+ if (perf.lossStreak >= 3) {
218
+ analyzeAndOptimize();
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Learn from a completed trade
224
+ * Extracts patterns from winning and losing trades
225
+ */
226
+ const learnFromTrade = (trade, result) => {
227
+ const pattern = {
228
+ timestamp: trade.timestamp,
229
+ result,
230
+ pnl: trade.pnl,
231
+ direction: trade.direction || trade.side,
232
+
233
+ // Market context before trade
234
+ priceAction: analyzePriceAction(trade.ticksBefore),
235
+ volumeProfile: analyzeVolume(trade.ticksBefore),
236
+ volatility: calculateVolatility(trade.ticksBefore),
237
+
238
+ // Signal characteristics
239
+ signalConfidence: trade.signalUsed?.confidence || null,
240
+ entryPrice: trade.price || trade.signalUsed?.entry,
241
+ stopLoss: trade.signalUsed?.stopLoss,
242
+ takeProfit: trade.signalUsed?.takeProfit
243
+ };
244
+
245
+ if (result === 'win') {
246
+ supervisorState.winningPatterns.push(pattern);
247
+ // Keep last 50 winning patterns
248
+ if (supervisorState.winningPatterns.length > 50) {
249
+ supervisorState.winningPatterns = supervisorState.winningPatterns.slice(-50);
250
+ }
251
+ } else {
252
+ supervisorState.losingPatterns.push(pattern);
253
+ // Keep last 50 losing patterns
254
+ if (supervisorState.losingPatterns.length > 50) {
255
+ supervisorState.losingPatterns = supervisorState.losingPatterns.slice(-50);
256
+ }
257
+ }
258
+ };
259
+
260
+ /**
261
+ * Analyze price action from ticks
262
+ */
263
+ const analyzePriceAction = (ticks) => {
264
+ if (!ticks || ticks.length < 2) return { trend: 'unknown', strength: 0 };
265
+
266
+ const prices = ticks.map(t => t.price).filter(Boolean);
267
+ if (prices.length < 2) return { trend: 'unknown', strength: 0 };
268
+
269
+ const first = prices[0];
270
+ const last = prices[prices.length - 1];
271
+ const change = last - first;
272
+ const range = Math.max(...prices) - Math.min(...prices);
273
+
274
+ return {
275
+ trend: change > 0 ? 'up' : change < 0 ? 'down' : 'flat',
276
+ strength: range > 0 ? Math.abs(change) / range : 0,
277
+ range,
278
+ change
279
+ };
280
+ };
281
+
282
+ /**
283
+ * Analyze volume from ticks
284
+ */
285
+ const analyzeVolume = (ticks) => {
286
+ if (!ticks || ticks.length === 0) return { total: 0, avg: 0, trend: 'unknown' };
287
+
288
+ const volumes = ticks.map(t => t.volume || 0);
289
+ const total = volumes.reduce((a, b) => a + b, 0);
290
+ const avg = total / volumes.length;
291
+
292
+ // Compare first half vs second half
293
+ const mid = Math.floor(volumes.length / 2);
294
+ const firstHalf = volumes.slice(0, mid).reduce((a, b) => a + b, 0);
295
+ const secondHalf = volumes.slice(mid).reduce((a, b) => a + b, 0);
296
+
297
+ return {
298
+ total,
299
+ avg,
300
+ trend: secondHalf > firstHalf * 1.2 ? 'increasing' : secondHalf < firstHalf * 0.8 ? 'decreasing' : 'stable'
301
+ };
302
+ };
303
+
304
+ /**
305
+ * Calculate volatility from ticks
306
+ */
307
+ const calculateVolatility = (ticks) => {
308
+ if (!ticks || ticks.length < 2) return 0;
309
+
310
+ const prices = ticks.map(t => t.price).filter(Boolean);
311
+ if (prices.length < 2) return 0;
312
+
313
+ const returns = [];
314
+ for (let i = 1; i < prices.length; i++) {
315
+ returns.push((prices[i] - prices[i-1]) / prices[i-1]);
316
+ }
317
+
318
+ const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
319
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length;
320
+
321
+ return Math.sqrt(variance);
322
+ };
323
+
324
+ /**
325
+ * Main analysis and optimization loop
326
+ * Called periodically and after significant events
327
+ */
328
+ const analyzeAndOptimize = async () => {
329
+ if (!supervisorState.active || supervisorState.agents.length === 0) return;
330
+
331
+ const perf = supervisorState.performance;
332
+
333
+ // Skip if not enough data
334
+ if (perf.trades < 3) return;
335
+
336
+ // Prepare performance data for AI analysis
337
+ const performanceData = {
338
+ trades: perf.trades,
339
+ wins: perf.wins,
340
+ losses: perf.losses,
341
+ winRate: perf.trades > 0 ? perf.wins / perf.trades : 0,
342
+ pnl: perf.totalPnL,
343
+ maxDrawdown: perf.maxDrawdown,
344
+ currentDrawdown: perf.currentDrawdown,
345
+ winStreak: perf.winStreak,
346
+ lossStreak: perf.lossStreak,
347
+ maxWinStreak: perf.maxWinStreak,
348
+ maxLossStreak: perf.maxLossStreak,
349
+
350
+ // Calculate averages
351
+ avgWin: perf.wins > 0 ?
352
+ supervisorState.trades.filter(t => t.pnl > 0).reduce((s, t) => s + t.pnl, 0) / perf.wins : 0,
353
+ avgLoss: perf.losses > 0 ?
354
+ Math.abs(supervisorState.trades.filter(t => t.pnl < 0).reduce((s, t) => s + t.pnl, 0) / perf.losses) : 0,
355
+
356
+ // Recent trades for context
357
+ recentTrades: supervisorState.trades.slice(-10).map(t => ({
358
+ side: t.direction || t.side,
359
+ qty: t.qty,
360
+ price: t.price,
361
+ pnl: t.pnl
362
+ })),
363
+
364
+ // Pattern summaries
365
+ winningPatternCount: supervisorState.winningPatterns.length,
366
+ losingPatternCount: supervisorState.losingPatterns.length,
367
+
368
+ // Common characteristics of losing trades
369
+ losingTradeAnalysis: analyzeLosingPatterns()
370
+ };
371
+
372
+ // Get optimization suggestions from all agents
373
+ const suggestions = [];
374
+
375
+ for (const agent of supervisorState.agents) {
376
+ try {
377
+ const suggestion = await getOptimizationFromAgent(agent, performanceData);
378
+ if (suggestion) {
379
+ suggestions.push({
380
+ agentId: agent.id,
381
+ agentName: agent.name,
382
+ ...suggestion
383
+ });
384
+ }
385
+ } catch (e) {
386
+ // Silent fail for individual agent
387
+ }
388
+ }
389
+
390
+ if (suggestions.length === 0) return;
391
+
392
+ // Process suggestions based on mode
393
+ const isConsensus = supervisorState.agents.length >= 2;
394
+
395
+ if (isConsensus) {
396
+ // CONSENSUS MODE: All agents must agree
397
+ const consensusResult = buildConsensus(suggestions);
398
+
399
+ if (consensusResult.isUnanimous && consensusResult.optimizations.length > 0) {
400
+ // Apply unanimous optimizations
401
+ for (const opt of consensusResult.optimizations) {
402
+ applyOptimization(opt);
403
+ }
404
+ }
405
+
406
+ // Update current advice based on consensus
407
+ if (consensusResult.action) {
408
+ supervisorState.currentAdvice = {
409
+ action: consensusResult.action,
410
+ sizeMultiplier: consensusResult.sizeMultiplier || 1.0,
411
+ reason: consensusResult.reason || 'Consensus recommendation'
412
+ };
413
+ }
414
+ } else {
415
+ // INDIVIDUAL MODE: Apply single agent's suggestions
416
+ const suggestion = suggestions[0];
417
+
418
+ if (suggestion.optimizations) {
419
+ for (const opt of suggestion.optimizations) {
420
+ applyOptimization(opt);
421
+ }
422
+ }
423
+
424
+ if (suggestion.action) {
425
+ supervisorState.currentAdvice = {
426
+ action: suggestion.action,
427
+ sizeMultiplier: suggestion.sizeMultiplier || 1.0,
428
+ reason: suggestion.reason || 'Agent recommendation'
429
+ };
430
+ }
431
+ }
432
+
433
+ supervisorState.lastOptimizationTime = Date.now();
434
+ };
435
+
436
+ /**
437
+ * Analyze patterns in losing trades
438
+ */
439
+ const analyzeLosingPatterns = () => {
440
+ const patterns = supervisorState.losingPatterns;
441
+ if (patterns.length === 0) return null;
442
+
443
+ // Find common characteristics
444
+ const trends = patterns.map(p => p.priceAction?.trend).filter(Boolean);
445
+ const volatilities = patterns.map(p => p.volatility).filter(Boolean);
446
+ const confidences = patterns.map(p => p.signalConfidence).filter(Boolean);
447
+
448
+ const trendCounts = {};
449
+ for (const t of trends) {
450
+ trendCounts[t] = (trendCounts[t] || 0) + 1;
451
+ }
452
+
453
+ const avgVolatility = volatilities.length > 0 ?
454
+ volatilities.reduce((a, b) => a + b, 0) / volatilities.length : 0;
455
+
456
+ const avgConfidence = confidences.length > 0 ?
457
+ confidences.reduce((a, b) => a + b, 0) / confidences.length : 0;
458
+
459
+ return {
460
+ commonTrend: Object.entries(trendCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown',
461
+ avgVolatility,
462
+ avgConfidence,
463
+ count: patterns.length
464
+ };
465
+ };
466
+
467
+ /**
468
+ * Get optimization suggestion from a single agent
469
+ */
470
+ const getOptimizationFromAgent = async (agent, performanceData) => {
471
+ const systemPrompt = `You are an AI supervisor for HQX Ultra Scalping, a professional futures trading strategy.
472
+
473
+ The strategy uses:
474
+ - Order flow analysis (delta, absorption, imbalance)
475
+ - Statistical models (z-score, standard deviation)
476
+ - Dynamic risk management (Kelly criterion)
477
+
478
+ ANALYZE the performance data and LEARN from the losing trades.
479
+ Suggest SPECIFIC optimizations to improve win rate and reduce losses.
480
+
481
+ Respond in JSON:
482
+ {
483
+ "assessment": "brief assessment",
484
+ "action": "AGGRESSIVE|NORMAL|CAUTIOUS|PAUSE",
485
+ "sizeMultiplier": 0.5-1.5,
486
+ "optimizations": [
487
+ {"param": "name", "direction": "increase|decrease", "amount": "10%", "reason": "why"}
488
+ ],
489
+ "learnings": "what we learned from losing trades",
490
+ "confidence": 0-100
491
+ }`;
492
+
493
+ const prompt = `STRATEGY PERFORMANCE ANALYSIS
494
+
495
+ Stats:
496
+ - Trades: ${performanceData.trades} (${performanceData.wins}W / ${performanceData.losses}L)
497
+ - Win Rate: ${(performanceData.winRate * 100).toFixed(1)}%
498
+ - P&L: $${performanceData.pnl.toFixed(2)}
499
+ - Max Drawdown: $${performanceData.maxDrawdown.toFixed(2)}
500
+ - Current Streak: ${performanceData.winStreak > 0 ? performanceData.winStreak + ' wins' : performanceData.lossStreak + ' losses'}
501
+
502
+ Losing Trade Analysis:
503
+ ${performanceData.losingTradeAnalysis ? `
504
+ - Common trend at entry: ${performanceData.losingTradeAnalysis.commonTrend}
505
+ - Avg volatility: ${(performanceData.losingTradeAnalysis.avgVolatility * 100).toFixed(3)}%
506
+ - Avg signal confidence: ${(performanceData.losingTradeAnalysis.avgConfidence * 100).toFixed(1)}%
507
+ - Total losing patterns: ${performanceData.losingTradeAnalysis.count}
508
+ ` : 'Not enough data'}
509
+
510
+ Recent Trades:
511
+ ${performanceData.recentTrades.map(t => `${t.side} @ ${t.price} → $${t.pnl?.toFixed(2)}`).join('\n')}
512
+
513
+ What should we LEARN and OPTIMIZE?`;
514
+
515
+ try {
516
+ const response = await callAI(agent, prompt, systemPrompt);
517
+ if (!response) return null;
518
+
519
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
520
+ if (jsonMatch) {
521
+ return JSON.parse(jsonMatch[0]);
522
+ }
523
+ return null;
524
+ } catch (e) {
525
+ return null;
526
+ }
527
+ };
528
+
529
+ /**
530
+ * Build consensus from multiple agent suggestions
531
+ */
532
+ const buildConsensus = (suggestions) => {
533
+ if (suggestions.length === 0) return { isUnanimous: false };
534
+
535
+ // Check action consensus
536
+ const actions = suggestions.map(s => s.action).filter(Boolean);
537
+ const allSameAction = actions.length === suggestions.length &&
538
+ actions.every(a => a === actions[0]);
539
+
540
+ // Check optimization consensus
541
+ const allOptimizations = suggestions.flatMap(s => s.optimizations || []);
542
+ const paramGroups = {};
543
+
544
+ for (const opt of allOptimizations) {
545
+ if (!opt.param) continue;
546
+ const key = `${opt.param}_${opt.direction}`;
547
+ if (!paramGroups[key]) {
548
+ paramGroups[key] = { ...opt, count: 0 };
549
+ }
550
+ paramGroups[key].count++;
551
+ }
552
+
553
+ // Find unanimous optimizations (all agents agree)
554
+ const unanimousOptimizations = Object.values(paramGroups)
555
+ .filter(g => g.count === suggestions.length)
556
+ .map(g => ({
557
+ param: g.param,
558
+ direction: g.direction,
559
+ amount: g.amount,
560
+ reason: `Unanimous (${suggestions.length} agents)`
561
+ }));
562
+
563
+ // Average size multiplier
564
+ const multipliers = suggestions.map(s => s.sizeMultiplier || 1.0);
565
+ const avgMultiplier = multipliers.reduce((a, b) => a + b, 0) / multipliers.length;
566
+
567
+ // Average confidence
568
+ const confidences = suggestions.map(s => s.confidence || 50);
569
+ const avgConfidence = Math.round(confidences.reduce((a, b) => a + b, 0) / confidences.length);
570
+
571
+ return {
572
+ isUnanimous: allSameAction && unanimousOptimizations.length > 0,
573
+ action: allSameAction ? actions[0] : 'CAUTIOUS',
574
+ sizeMultiplier: allSameAction ? avgMultiplier : 0.5,
575
+ optimizations: unanimousOptimizations,
576
+ confidence: avgConfidence,
577
+ reason: allSameAction ?
578
+ `${suggestions.length} agents agree` :
579
+ 'Agents disagree - being cautious',
580
+ votes: actions.reduce((acc, a) => { acc[a] = (acc[a] || 0) + 1; return acc; }, {})
581
+ };
582
+ };
583
+
584
+ /**
585
+ * Apply an optimization to the strategy
586
+ */
587
+ const applyOptimization = (optimization) => {
588
+ const strategy = supervisorState.strategy;
589
+ if (!strategy) return false;
590
+
591
+ // Record the optimization
592
+ supervisorState.optimizations.push({
593
+ timestamp: Date.now(),
594
+ ...optimization
595
+ });
596
+
597
+ // Try to apply to strategy if it supports it
598
+ try {
599
+ if (typeof strategy.applyAIOptimization === 'function') {
600
+ strategy.applyAIOptimization(optimization);
601
+ return true;
602
+ }
603
+
604
+ if (typeof strategy.setParameter === 'function') {
605
+ strategy.setParameter(optimization.param, optimization.direction, optimization.amount);
606
+ return true;
607
+ }
608
+ } catch (e) {
609
+ // Strategy doesn't support this optimization
610
+ }
611
+
612
+ return false;
613
+ };
614
+
615
+ /**
616
+ * Get current advice for the strategy
617
+ * Called before each trade decision
618
+ */
619
+ const getCurrentAdvice = () => {
620
+ if (!supervisorState.active) {
621
+ return { action: 'NORMAL', sizeMultiplier: 1.0, reason: 'No supervision' };
622
+ }
623
+
624
+ return supervisorState.currentAdvice;
625
+ };
626
+
627
+ /**
628
+ * Get supervision status
629
+ */
630
+ const getStatus = () => {
631
+ return {
632
+ active: supervisorState.active,
633
+ agents: supervisorState.agents.length,
634
+ mode: supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL',
635
+ performance: supervisorState.performance,
636
+ currentAdvice: supervisorState.currentAdvice,
637
+ optimizationsApplied: supervisorState.optimizations.length,
638
+ patternsLearned: {
639
+ winning: supervisorState.winningPatterns.length,
640
+ losing: supervisorState.losingPatterns.length
641
+ },
642
+ lastOptimization: supervisorState.lastOptimizationTime
643
+ };
644
+ };
645
+
646
+ /**
647
+ * Check if should proceed with trade based on AI advice
648
+ */
649
+ const shouldTrade = () => {
650
+ if (!supervisorState.active) return { proceed: true, multiplier: 1.0 };
651
+
652
+ const advice = supervisorState.currentAdvice;
653
+
654
+ if (advice.action === 'PAUSE') {
655
+ return { proceed: false, reason: advice.reason };
656
+ }
657
+
658
+ return {
659
+ proceed: true,
660
+ multiplier: advice.sizeMultiplier || 1.0,
661
+ action: advice.action
662
+ };
663
+ };
664
+
665
+ module.exports = {
666
+ initialize,
667
+ stop,
668
+ feedTick,
669
+ feedSignal,
670
+ feedTradeResult,
671
+ getCurrentAdvice,
672
+ shouldTrade,
673
+ getStatus,
674
+ analyzeAndOptimize
675
+ };
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const { connections } = require('../session');
10
- const { analyzeTrading } = require('./client');
10
+ const { analyzeTrading, analyzePerformance, getMarketAdvice } = require('./client');
11
11
 
12
12
  let aiService = null;
13
13
 
@@ -567,6 +567,383 @@ class AISupervisor {
567
567
  static getSessionCount() {
568
568
  return supervisionSessions.size;
569
569
  }
570
+
571
+ /**
572
+ * Feed market tick to all agents (sync with strategy)
573
+ * Agents receive the same data as the strategy in real-time
574
+ *
575
+ * @param {Object} tick - Market tick data { price, bid, ask, volume, timestamp }
576
+ */
577
+ static feedTick(tick) {
578
+ if (supervisionSessions.size === 0) return;
579
+
580
+ // Store latest tick in all sessions
581
+ for (const [agentId, session] of supervisionSessions.entries()) {
582
+ if (!session.marketData) {
583
+ session.marketData = { ticks: [], lastTick: null };
584
+ }
585
+ session.marketData.lastTick = tick;
586
+ session.marketData.ticks.push(tick);
587
+
588
+ // Keep only last 1000 ticks to prevent memory bloat
589
+ if (session.marketData.ticks.length > 1000) {
590
+ session.marketData.ticks = session.marketData.ticks.slice(-1000);
591
+ }
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Feed strategy signal to all agents (sync with strategy)
597
+ * Agents see every signal the strategy generates
598
+ *
599
+ * @param {Object} signal - Strategy signal { direction, entry, stopLoss, takeProfit, confidence }
600
+ */
601
+ static feedSignal(signal) {
602
+ if (supervisionSessions.size === 0) return;
603
+
604
+ const signalData = {
605
+ timestamp: Date.now(),
606
+ direction: signal.direction,
607
+ entry: signal.entry,
608
+ stopLoss: signal.stopLoss,
609
+ takeProfit: signal.takeProfit,
610
+ confidence: signal.confidence
611
+ };
612
+
613
+ // Store signal in all sessions
614
+ for (const [agentId, session] of supervisionSessions.entries()) {
615
+ if (!session.signals) {
616
+ session.signals = [];
617
+ }
618
+ session.signals.push(signalData);
619
+
620
+ // Keep only last 100 signals
621
+ if (session.signals.length > 100) {
622
+ session.signals = session.signals.slice(-100);
623
+ }
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Feed trade execution to all agents (sync with strategy)
629
+ * Agents see every trade executed
630
+ *
631
+ * @param {Object} trade - Trade data { side, qty, price, pnl, symbol }
632
+ */
633
+ static feedTrade(trade) {
634
+ if (supervisionSessions.size === 0) return;
635
+
636
+ const tradeData = {
637
+ timestamp: Date.now(),
638
+ side: trade.side,
639
+ qty: trade.qty,
640
+ price: trade.price,
641
+ pnl: trade.pnl,
642
+ symbol: trade.symbol
643
+ };
644
+
645
+ // Store trade in all sessions
646
+ for (const [agentId, session] of supervisionSessions.entries()) {
647
+ if (!session.trades) {
648
+ session.trades = [];
649
+ }
650
+ session.trades.push(tradeData);
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Update current position for all agents
656
+ *
657
+ * @param {Object} position - Position data { qty, side, entryPrice, pnl }
658
+ */
659
+ static updatePosition(position) {
660
+ if (supervisionSessions.size === 0) return;
661
+
662
+ for (const [agentId, session] of supervisionSessions.entries()) {
663
+ session.currentPosition = {
664
+ timestamp: Date.now(),
665
+ qty: position.qty,
666
+ side: position.side,
667
+ entryPrice: position.entryPrice,
668
+ pnl: position.pnl
669
+ };
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Update P&L for all agents
675
+ *
676
+ * @param {number} pnl - Current session P&L
677
+ * @param {number} balance - Account balance
678
+ */
679
+ static updatePnL(pnl, balance) {
680
+ if (supervisionSessions.size === 0) return;
681
+
682
+ for (const [agentId, session] of supervisionSessions.entries()) {
683
+ session.currentPnL = pnl;
684
+ session.currentBalance = balance;
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Check if agents recommend intervention (PAUSE, REDUCE_SIZE, etc.)
690
+ * In CONSENSUS mode, ALL agents must agree to continue trading
691
+ *
692
+ * @returns {Object} { shouldContinue: boolean, action: string, reason: string }
693
+ */
694
+ static checkIntervention() {
695
+ if (supervisionSessions.size === 0) {
696
+ return { shouldContinue: true, action: 'CONTINUE', reason: 'No AI supervision active' };
697
+ }
698
+
699
+ // Get last consensus or individual decision
700
+ const consensus = this.getConsensus();
701
+
702
+ if (consensus && consensus.isUnanimous) {
703
+ if (consensus.action === 'PAUSE' || consensus.action === 'STOP') {
704
+ return { shouldContinue: false, action: consensus.action, reason: 'AI agents recommend pause' };
705
+ }
706
+ if (consensus.action === 'REDUCE_SIZE') {
707
+ return { shouldContinue: true, action: 'REDUCE_SIZE', reason: 'AI agents recommend reducing size' };
708
+ }
709
+ } else if (consensus && !consensus.isUnanimous) {
710
+ // Agents disagree - be conservative, don't take new trades
711
+ return { shouldContinue: false, action: 'HOLD', reason: 'AI agents disagree - waiting for consensus' };
712
+ }
713
+
714
+ return { shouldContinue: true, action: 'CONTINUE', reason: 'AI supervision active' };
715
+ }
716
+
717
+ /**
718
+ * Get real-time sync status for display
719
+ * Shows what data the agents are receiving
720
+ *
721
+ * @returns {Object} Sync status
722
+ */
723
+ static getSyncStatus() {
724
+ if (supervisionSessions.size === 0) {
725
+ return { synced: false, agents: 0 };
726
+ }
727
+
728
+ const firstSession = supervisionSessions.values().next().value;
729
+
730
+ return {
731
+ synced: true,
732
+ agents: supervisionSessions.size,
733
+ lastTick: firstSession?.marketData?.lastTick?.timestamp || null,
734
+ tickCount: firstSession?.marketData?.ticks?.length || 0,
735
+ signalCount: firstSession?.signals?.length || 0,
736
+ tradeCount: firstSession?.trades?.length || 0,
737
+ currentPnL: firstSession?.currentPnL || 0,
738
+ currentPosition: firstSession?.currentPosition || null
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Request strategy optimization from all agents
744
+ * Agents analyze performance data and suggest improvements
745
+ * In CONSENSUS mode, only unanimous suggestions are applied
746
+ *
747
+ * @param {Object} performanceData - Strategy performance data
748
+ * @returns {Promise<Object|null>} Optimization suggestions (consensus)
749
+ */
750
+ static async requestOptimization(performanceData) {
751
+ if (supervisionSessions.size === 0) return null;
752
+
753
+ const allSessions = Array.from(supervisionSessions.values());
754
+ const suggestions = [];
755
+
756
+ // Get optimization suggestions from each agent
757
+ for (const session of allSessions) {
758
+ try {
759
+ const suggestion = await analyzePerformance(session.agent, performanceData);
760
+ if (suggestion) {
761
+ suggestions.push({
762
+ agentId: session.agentId,
763
+ agentName: session.agent.name,
764
+ ...suggestion
765
+ });
766
+ }
767
+ } catch (e) {
768
+ // Silent fail for individual agent
769
+ }
770
+ }
771
+
772
+ if (suggestions.length === 0) return null;
773
+
774
+ // If single agent, return its suggestion
775
+ if (suggestions.length === 1) {
776
+ return {
777
+ mode: 'INDIVIDUAL',
778
+ ...suggestions[0]
779
+ };
780
+ }
781
+
782
+ // CONSENSUS MODE: Find common optimizations
783
+ const consensusOptimizations = [];
784
+ const allOptimizations = suggestions.flatMap(s => s.optimizations || []);
785
+
786
+ // Group by parameter name
787
+ const paramGroups = {};
788
+ for (const opt of allOptimizations) {
789
+ if (!opt.param) continue;
790
+ if (!paramGroups[opt.param]) {
791
+ paramGroups[opt.param] = [];
792
+ }
793
+ paramGroups[opt.param].push(opt);
794
+ }
795
+
796
+ // Find unanimous suggestions (all agents agree on direction)
797
+ for (const [param, opts] of Object.entries(paramGroups)) {
798
+ if (opts.length === suggestions.length) {
799
+ // All agents suggested this param - check if they agree on direction
800
+ const directions = opts.map(o => {
801
+ const current = parseFloat(o.current) || 0;
802
+ const suggested = parseFloat(o.suggested) || 0;
803
+ return suggested > current ? 'increase' : suggested < current ? 'decrease' : 'same';
804
+ });
805
+
806
+ const allSame = directions.every(d => d === directions[0]);
807
+ if (allSame && directions[0] !== 'same') {
808
+ // Unanimous - use average of suggested values
809
+ const avgSuggested = opts.reduce((sum, o) => sum + (parseFloat(o.suggested) || 0), 0) / opts.length;
810
+ consensusOptimizations.push({
811
+ param,
812
+ current: opts[0].current,
813
+ suggested: avgSuggested.toFixed(2),
814
+ reason: `Unanimous (${suggestions.length} agents agree)`,
815
+ direction: directions[0]
816
+ });
817
+ }
818
+ }
819
+ }
820
+
821
+ // Calculate average confidence
822
+ const avgConfidence = Math.round(
823
+ suggestions.reduce((sum, s) => sum + (s.confidence || 0), 0) / suggestions.length
824
+ );
825
+
826
+ // Determine consensus market condition
827
+ const conditions = suggestions.map(s => s.marketCondition).filter(Boolean);
828
+ const conditionCounts = {};
829
+ for (const c of conditions) {
830
+ conditionCounts[c] = (conditionCounts[c] || 0) + 1;
831
+ }
832
+ const consensusCondition = Object.entries(conditionCounts)
833
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown';
834
+
835
+ return {
836
+ mode: 'CONSENSUS',
837
+ agentCount: suggestions.length,
838
+ isUnanimous: consensusOptimizations.length > 0,
839
+ optimizations: consensusOptimizations,
840
+ marketCondition: consensusCondition,
841
+ confidence: avgConfidence,
842
+ individualSuggestions: suggestions
843
+ };
844
+ }
845
+
846
+ /**
847
+ * Get real-time market advice from all agents
848
+ * Used for dynamic position sizing and risk adjustment
849
+ *
850
+ * @param {Object} marketData - Current market data
851
+ * @returns {Promise<Object|null>} Market advice (consensus)
852
+ */
853
+ static async getMarketAdvice(marketData) {
854
+ if (supervisionSessions.size === 0) return null;
855
+
856
+ const allSessions = Array.from(supervisionSessions.values());
857
+ const advices = [];
858
+
859
+ // Get advice from each agent
860
+ for (const session of allSessions) {
861
+ try {
862
+ const advice = await getMarketAdvice(session.agent, marketData);
863
+ if (advice) {
864
+ advices.push({
865
+ agentId: session.agentId,
866
+ agentName: session.agent.name,
867
+ ...advice
868
+ });
869
+ }
870
+ } catch (e) {
871
+ // Silent fail
872
+ }
873
+ }
874
+
875
+ if (advices.length === 0) return null;
876
+
877
+ // Single agent
878
+ if (advices.length === 1) {
879
+ return {
880
+ mode: 'INDIVIDUAL',
881
+ ...advices[0]
882
+ };
883
+ }
884
+
885
+ // CONSENSUS: All agents must agree on action
886
+ const actions = advices.map(a => a.action);
887
+ const allSameAction = actions.every(a => a === actions[0]);
888
+
889
+ if (allSameAction) {
890
+ // Unanimous action - average the size multiplier
891
+ const avgMultiplier = advices.reduce((sum, a) => sum + (a.sizeMultiplier || 1), 0) / advices.length;
892
+ const avgConfidence = Math.round(advices.reduce((sum, a) => sum + (a.confidence || 0), 0) / advices.length);
893
+
894
+ return {
895
+ mode: 'CONSENSUS',
896
+ isUnanimous: true,
897
+ action: actions[0],
898
+ sizeMultiplier: Math.round(avgMultiplier * 100) / 100,
899
+ confidence: avgConfidence,
900
+ reason: `${advices.length} agents unanimous`,
901
+ agentCount: advices.length
902
+ };
903
+ } else {
904
+ // Agents disagree - be conservative
905
+ return {
906
+ mode: 'CONSENSUS',
907
+ isUnanimous: false,
908
+ action: 'CAUTIOUS',
909
+ sizeMultiplier: 0.5,
910
+ confidence: 0,
911
+ reason: 'Agents disagree - reducing exposure',
912
+ agentCount: advices.length,
913
+ votes: actions.reduce((acc, a) => { acc[a] = (acc[a] || 0) + 1; return acc; }, {})
914
+ };
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Apply optimization to strategy
920
+ * Called when agents have consensus on improvements
921
+ *
922
+ * @param {Object} strategy - Strategy instance (M1)
923
+ * @param {Object} optimization - Optimization to apply
924
+ * @returns {boolean} Success
925
+ */
926
+ static applyOptimization(strategy, optimization) {
927
+ if (!strategy || !optimization) return false;
928
+
929
+ try {
930
+ // Check if strategy has optimization method
931
+ if (typeof strategy.applyOptimization === 'function') {
932
+ strategy.applyOptimization(optimization);
933
+ return true;
934
+ }
935
+
936
+ // Fallback: try to set individual parameters
937
+ if (typeof strategy.setParameter === 'function' && optimization.param) {
938
+ strategy.setParameter(optimization.param, optimization.suggested);
939
+ return true;
940
+ }
941
+
942
+ return false;
943
+ } catch (e) {
944
+ return false;
945
+ }
946
+ }
570
947
  }
571
948
 
572
949
  module.exports = AISupervisor;