hedgequantx 2.9.117 → 2.9.118
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
|
@@ -7,6 +7,7 @@ const { loadStrategy } = require('../../lib/m');
|
|
|
7
7
|
const { MarketDataFeed } = require('../../lib/data');
|
|
8
8
|
const { SupervisionEngine } = require('../../services/ai-supervision');
|
|
9
9
|
const smartLogs = require('../../lib/smart-logs');
|
|
10
|
+
const { sessionLogger } = require('../../services/session-logger');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Execute algo strategy with market data
|
|
@@ -76,9 +77,20 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
76
77
|
// Set strategy for context-aware smart logs
|
|
77
78
|
smartLogs.setStrategy(strategyId);
|
|
78
79
|
|
|
80
|
+
// Start session logger for persistent logs
|
|
81
|
+
const logFile = sessionLogger.start({
|
|
82
|
+
strategy: strategyId,
|
|
83
|
+
account: accountName,
|
|
84
|
+
symbol: symbolName,
|
|
85
|
+
contracts,
|
|
86
|
+
target: dailyTarget,
|
|
87
|
+
risk: maxRisk
|
|
88
|
+
});
|
|
89
|
+
|
|
79
90
|
strategy.on('log', (log) => {
|
|
80
91
|
const type = log.type === 'debug' ? 'debug' : log.type === 'info' ? 'analysis' : 'system';
|
|
81
92
|
ui.addLog(type, log.message);
|
|
93
|
+
sessionLogger.log(type.toUpperCase(), log.message);
|
|
82
94
|
});
|
|
83
95
|
|
|
84
96
|
const marketFeed = new MarketDataFeed();
|
|
@@ -101,6 +113,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
101
113
|
const signalLog = smartLogs.getSignalLog(dir, symbolCode, (signal.confidence || 0) * 100, strategyName);
|
|
102
114
|
ui.addLog('signal', `${signalLog.message}`);
|
|
103
115
|
ui.addLog('signal', signalLog.details);
|
|
116
|
+
sessionLogger.signal(dir, signal.entry, signal.confidence, signalLog.details);
|
|
104
117
|
|
|
105
118
|
if (!running) {
|
|
106
119
|
const riskLog = smartLogs.getRiskCheckLog(false, 'Algo stopped');
|
|
@@ -187,6 +200,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
187
200
|
const entryLog = smartLogs.getEntryLog(direction.toUpperCase(), symbolCode, orderSize, entry);
|
|
188
201
|
ui.addLog('fill_' + (direction === 'long' ? 'buy' : 'sell'), entryLog.message);
|
|
189
202
|
ui.addLog('trade', entryLog.details);
|
|
203
|
+
sessionLogger.trade('ENTRY', direction.toUpperCase(), entry, orderSize, orderResult.orderId);
|
|
190
204
|
|
|
191
205
|
// Bracket orders
|
|
192
206
|
if (stopLoss && takeProfit) {
|
|
@@ -202,9 +216,11 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
202
216
|
}
|
|
203
217
|
} else {
|
|
204
218
|
ui.addLog('error', `Order failed: ${orderResult.error}`);
|
|
219
|
+
sessionLogger.error('Order failed', orderResult.error);
|
|
205
220
|
}
|
|
206
221
|
} catch (e) {
|
|
207
222
|
ui.addLog('error', `Order error: ${e.message}`);
|
|
223
|
+
sessionLogger.error('Order exception', e);
|
|
208
224
|
}
|
|
209
225
|
pendingOrder = false;
|
|
210
226
|
});
|
|
@@ -264,12 +280,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
264
280
|
const buyPressure = totalVol > 0 ? (buyVolume / totalVol) * 100 : 50;
|
|
265
281
|
const delta = buyVolume - sellVolume;
|
|
266
282
|
|
|
267
|
-
|
|
268
|
-
let bias = 'FLAT';
|
|
269
|
-
if (buyPressure > 55) bias = 'LONG';
|
|
270
|
-
else if (buyPressure < 45) bias = 'SHORT';
|
|
271
|
-
|
|
272
|
-
// Log bias when it changes, or every 5 seconds if strong signal
|
|
283
|
+
let bias = buyPressure > 55 ? 'LONG' : buyPressure < 45 ? 'SHORT' : 'FLAT';
|
|
273
284
|
const strongSignal = Math.abs(delta) > 20 || buyPressure > 65 || buyPressure < 35;
|
|
274
285
|
if (bias !== lastBias || (strongSignal && currentSecond % 5 === 0) || (!strongSignal && currentSecond % 15 === 0)) {
|
|
275
286
|
const biasLog = smartLogs.getMarketBiasLog(bias, delta, buyPressure);
|
|
@@ -282,64 +293,36 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
282
293
|
if (currentSecond % 30 === 0) {
|
|
283
294
|
const state = strategy.getAnalysisState?.(contractId, price);
|
|
284
295
|
if (state) {
|
|
296
|
+
sessionLogger.state(state.activeZones || 0, state.swingsDetected || 0, barCount, lastBias);
|
|
285
297
|
if (!state.ready) {
|
|
286
298
|
ui.addLog('system', state.message);
|
|
287
299
|
} else {
|
|
288
300
|
const resStr = state.nearestResistance ? state.nearestResistance.toFixed(2) : '--';
|
|
289
301
|
const supStr = state.nearestSupport ? state.nearestSupport.toFixed(2) : '--';
|
|
290
302
|
|
|
291
|
-
// Combined single line for zones info
|
|
292
303
|
ui.addLog('analysis', `Zones: ${state.activeZones} | R: ${resStr} | S: ${supStr} | Swings: ${state.swingsDetected}`);
|
|
293
|
-
|
|
294
|
-
// HF-grade proximity logs with precise distance info
|
|
295
304
|
if (price && state.nearestResistance) {
|
|
296
|
-
const gapR = state.nearestResistance - price;
|
|
297
|
-
|
|
298
|
-
const dirR = gapR > 0 ? 'below' : 'above';
|
|
299
|
-
const absTicksR = Math.abs(ticksR);
|
|
300
|
-
if (absTicksR <= 50) { // Only show if within 50 ticks
|
|
301
|
-
ui.addLog('analysis', `PROX R: ${Math.abs(gapR).toFixed(2)} pts (${absTicksR} ticks ${dirR}) | Trigger: price must sweep ABOVE then reject`);
|
|
302
|
-
}
|
|
305
|
+
const gapR = state.nearestResistance - price, ticksR = Math.abs(Math.round(gapR / tickSize));
|
|
306
|
+
if (ticksR <= 50) ui.addLog('analysis', `PROX R: ${Math.abs(gapR).toFixed(2)} pts (${ticksR} ticks) | Sweep ABOVE then reject`);
|
|
303
307
|
}
|
|
304
308
|
if (price && state.nearestSupport) {
|
|
305
|
-
const gapS = price - state.nearestSupport;
|
|
306
|
-
|
|
307
|
-
const dirS = gapS > 0 ? 'above' : 'below';
|
|
308
|
-
const absTicksS = Math.abs(ticksS);
|
|
309
|
-
if (absTicksS <= 50) { // Only show if within 50 ticks
|
|
310
|
-
ui.addLog('analysis', `PROX S: ${Math.abs(gapS).toFixed(2)} pts (${absTicksS} ticks ${dirS}) | Trigger: price must sweep BELOW then reject`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Strategy status - what we're waiting for
|
|
315
|
-
if (state.activeZones === 0) {
|
|
316
|
-
ui.addLog('risk', 'Building liquidity map - scanning swing points for zone formation...');
|
|
317
|
-
} else if (!state.nearestSupport && !state.nearestResistance) {
|
|
318
|
-
ui.addLog('risk', 'Zones detected but outside proximity range - waiting for price approach');
|
|
319
|
-
} else if (!state.nearestSupport) {
|
|
320
|
-
ui.addLog('analysis', 'Monitoring resistance for HIGH SWEEP opportunity (SHORT entry on rejection)');
|
|
321
|
-
} else if (!state.nearestResistance) {
|
|
322
|
-
ui.addLog('analysis', 'Monitoring support for LOW SWEEP opportunity (LONG entry on rejection)');
|
|
323
|
-
} else {
|
|
324
|
-
ui.addLog('ready', 'Both zones active - monitoring for liquidity sweep with rejection confirmation');
|
|
309
|
+
const gapS = price - state.nearestSupport, ticksS = Math.abs(Math.round(gapS / tickSize));
|
|
310
|
+
if (ticksS <= 50) ui.addLog('analysis', `PROX S: ${Math.abs(gapS).toFixed(2)} pts (${ticksS} ticks) | Sweep BELOW then reject`);
|
|
325
311
|
}
|
|
312
|
+
if (state.activeZones === 0) ui.addLog('risk', 'Building liquidity map...');
|
|
313
|
+
else if (!state.nearestSupport && !state.nearestResistance) ui.addLog('risk', 'Zones outside range');
|
|
314
|
+
else if (!state.nearestSupport) ui.addLog('analysis', 'Monitoring R for SHORT sweep');
|
|
315
|
+
else if (!state.nearestResistance) ui.addLog('analysis', 'Monitoring S for LONG sweep');
|
|
316
|
+
else ui.addLog('ready', 'Both zones active - awaiting sweep');
|
|
326
317
|
}
|
|
327
318
|
}
|
|
328
319
|
}
|
|
329
320
|
|
|
330
|
-
// Scanning log every 20 seconds
|
|
331
|
-
if (currentSecond % 20 === 0 && currentPosition === 0)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// Tick flow log every 45 seconds (less frequent)
|
|
337
|
-
if (currentSecond % 45 === 0) {
|
|
338
|
-
const tickLog = smartLogs.getTickFlowLog(tickCount, ticksPerSecond);
|
|
339
|
-
ui.addLog('debug', `${tickLog.message} ${tickLog.details}`);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// AI Agents status log every 60 seconds
|
|
321
|
+
// Scanning log every 20 seconds
|
|
322
|
+
if (currentSecond % 20 === 0 && currentPosition === 0) ui.addLog('system', smartLogs.getScanningLog(true).message);
|
|
323
|
+
// Tick flow log every 45 seconds
|
|
324
|
+
if (currentSecond % 45 === 0) { const t = smartLogs.getTickFlowLog(tickCount, ticksPerSecond); ui.addLog('debug', `${t.message} ${t.details}`); }
|
|
325
|
+
// AI Agents status every 60 seconds
|
|
343
326
|
if (currentSecond % 60 === 0 && supervisionEnabled && supervisionEngine) {
|
|
344
327
|
const status = supervisionEngine.getStatus();
|
|
345
328
|
const agentNames = status.agents.map(a => a.name.split(' ')[0]).join(', ');
|
|
@@ -371,18 +354,12 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
371
354
|
timestamp: tick.timestamp || Date.now()
|
|
372
355
|
});
|
|
373
356
|
|
|
374
|
-
// Calculate latency from Rithmic ssboe/usecs
|
|
375
|
-
// Priority: ssboe/usecs (real exchange time) > inter-tick timing (fallback)
|
|
357
|
+
// Calculate latency from Rithmic ssboe/usecs or inter-tick timing
|
|
376
358
|
if (tick.ssboe && tick.usecs !== undefined) {
|
|
377
|
-
// Rithmic sends ssboe (seconds since epoch) and usecs (microseconds)
|
|
378
359
|
const tickTimeMs = (tick.ssboe * 1000) + Math.floor(tick.usecs / 1000);
|
|
379
360
|
const latency = now - tickTimeMs;
|
|
380
|
-
|
|
381
|
-
if (latency >= 0 && latency < 5000) {
|
|
382
|
-
stats.latency = latency;
|
|
383
|
-
}
|
|
361
|
+
if (latency >= 0 && latency < 5000) stats.latency = latency;
|
|
384
362
|
} else if (lastTickTime > 0) {
|
|
385
|
-
// Fallback: estimate from inter-tick timing
|
|
386
363
|
const timeSinceLastTick = now - lastTickTime;
|
|
387
364
|
if (timeSinceLastTick < 100) {
|
|
388
365
|
tickLatencies.push(timeSinceLastTick);
|
|
@@ -393,10 +370,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
393
370
|
lastTickTime = now;
|
|
394
371
|
});
|
|
395
372
|
|
|
396
|
-
marketFeed.on('connected', () => {
|
|
397
|
-
stats.connected = true;
|
|
398
|
-
ui.addLog('connected', 'Market data connected');
|
|
399
|
-
});
|
|
373
|
+
marketFeed.on('connected', () => { stats.connected = true; ui.addLog('connected', 'Market data connected'); });
|
|
400
374
|
marketFeed.on('subscribed', (symbol) => ui.addLog('system', `Subscribed: ${symbol}`));
|
|
401
375
|
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
402
376
|
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
|
|
@@ -451,10 +425,14 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
451
425
|
if (stats.pnl >= dailyTarget) {
|
|
452
426
|
stopReason = 'target'; running = false;
|
|
453
427
|
ui.addLog('fill_win', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
428
|
+
sessionLogger.log('TARGET', `Daily target reached: +$${stats.pnl.toFixed(2)}`);
|
|
454
429
|
} else if (stats.pnl <= -maxRisk) {
|
|
455
430
|
stopReason = 'risk'; running = false;
|
|
456
431
|
ui.addLog('fill_loss', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
432
|
+
sessionLogger.log('RISK', `Max risk hit: -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
457
433
|
}
|
|
434
|
+
// Log P&L every poll
|
|
435
|
+
sessionLogger.pnl(stats.pnl, 0, currentPosition);
|
|
458
436
|
} catch (e) { /* silent */ }
|
|
459
437
|
};
|
|
460
438
|
|
|
@@ -496,7 +474,13 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
496
474
|
const durationMs = Date.now() - stats.startTime;
|
|
497
475
|
const h = Math.floor(durationMs / 3600000), m = Math.floor((durationMs % 3600000) / 60000), s = Math.floor((durationMs % 60000) / 1000);
|
|
498
476
|
stats.duration = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
477
|
+
|
|
478
|
+
// End session logger and get log file path
|
|
479
|
+
const sessionLogPath = sessionLogger.end(stats, stopReason?.toUpperCase() || 'MANUAL');
|
|
499
480
|
renderSessionSummary(stats, stopReason);
|
|
481
|
+
if (sessionLogPath) {
|
|
482
|
+
console.log(`\n Session log: ${sessionLogPath}`);
|
|
483
|
+
}
|
|
500
484
|
|
|
501
485
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
502
486
|
await new Promise(resolve => {
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Session Logger - Persistent logs for algo trading sessions
|
|
3
|
+
* @module services/session-logger
|
|
4
|
+
*
|
|
5
|
+
* Creates a log file per session with all events:
|
|
6
|
+
* - Strategy signals, trades, P&L
|
|
7
|
+
* - Market data (ticks, bars)
|
|
8
|
+
* - Zone/Swing detection
|
|
9
|
+
* - Errors and warnings
|
|
10
|
+
*
|
|
11
|
+
* Log files: ~/.hedgequantx/sessions/YYYY-MM-DD_HH-MM-SS_<strategy>.log
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const { SECURITY } = require('../config/settings');
|
|
18
|
+
|
|
19
|
+
class SessionLogger {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.sessionDir = path.join(os.homedir(), SECURITY.SESSION_DIR, 'sessions');
|
|
22
|
+
this.logFile = null;
|
|
23
|
+
this.sessionId = null;
|
|
24
|
+
this.buffer = [];
|
|
25
|
+
this.flushInterval = null;
|
|
26
|
+
this.metadata = {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start a new session log
|
|
31
|
+
* @param {Object} params - Session parameters
|
|
32
|
+
* @param {string} params.strategy - Strategy ID (e.g., 'hqx-2b')
|
|
33
|
+
* @param {string} params.account - Account name
|
|
34
|
+
* @param {string} params.symbol - Trading symbol
|
|
35
|
+
* @param {number} params.contracts - Number of contracts
|
|
36
|
+
* @param {number} params.target - Daily target
|
|
37
|
+
* @param {number} params.risk - Max risk
|
|
38
|
+
*/
|
|
39
|
+
start({ strategy, account, symbol, contracts, target, risk }) {
|
|
40
|
+
// Create session directory if needed
|
|
41
|
+
if (!fs.existsSync(this.sessionDir)) {
|
|
42
|
+
fs.mkdirSync(this.sessionDir, { recursive: true, mode: SECURITY.DIR_PERMISSIONS });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Generate session ID and file name
|
|
46
|
+
const now = new Date();
|
|
47
|
+
this.sessionId = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
48
|
+
const fileName = `${this.sessionId}_${strategy}.log`;
|
|
49
|
+
this.logFile = path.join(this.sessionDir, fileName);
|
|
50
|
+
|
|
51
|
+
// Store metadata
|
|
52
|
+
this.metadata = {
|
|
53
|
+
strategy,
|
|
54
|
+
account,
|
|
55
|
+
symbol,
|
|
56
|
+
contracts,
|
|
57
|
+
target,
|
|
58
|
+
risk,
|
|
59
|
+
startTime: now.toISOString(),
|
|
60
|
+
startTimestamp: Date.now()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Write header
|
|
64
|
+
const header = [
|
|
65
|
+
'================================================================================',
|
|
66
|
+
`HQX SESSION LOG - ${strategy.toUpperCase()}`,
|
|
67
|
+
'================================================================================',
|
|
68
|
+
`Session ID: ${this.sessionId}`,
|
|
69
|
+
`Started: ${now.toISOString()}`,
|
|
70
|
+
`Strategy: ${strategy}`,
|
|
71
|
+
`Account: ${account}`,
|
|
72
|
+
`Symbol: ${symbol}`,
|
|
73
|
+
`Contracts: ${contracts}`,
|
|
74
|
+
`Target: $${target}`,
|
|
75
|
+
`Risk: $${risk}`,
|
|
76
|
+
'================================================================================',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(this.logFile, header, { mode: SECURITY.FILE_PERMISSIONS });
|
|
81
|
+
|
|
82
|
+
// Start flush interval (every 2 seconds)
|
|
83
|
+
this.flushInterval = setInterval(() => this._flush(), 2000);
|
|
84
|
+
|
|
85
|
+
this._write('SYSTEM', 'Session started');
|
|
86
|
+
return this.logFile;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Log an event
|
|
91
|
+
* @param {string} type - Event type (SYSTEM, SIGNAL, TRADE, MARKET, ZONE, SWING, ERROR, etc.)
|
|
92
|
+
* @param {string} message - Log message
|
|
93
|
+
* @param {Object} [data] - Optional data object
|
|
94
|
+
*/
|
|
95
|
+
log(type, message, data = null) {
|
|
96
|
+
if (!this.logFile) return;
|
|
97
|
+
|
|
98
|
+
const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
99
|
+
const elapsed = this._getElapsed();
|
|
100
|
+
const dataStr = data ? ` | ${JSON.stringify(data)}` : '';
|
|
101
|
+
const line = `[${timestamp}] [${elapsed}] [${type.padEnd(8)}] ${message}${dataStr}`;
|
|
102
|
+
|
|
103
|
+
this.buffer.push(line);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Log market tick
|
|
108
|
+
*/
|
|
109
|
+
tick(price, size, bid, ask) {
|
|
110
|
+
this.log('TICK', `Price: ${price} | Size: ${size} | Bid: ${bid} | Ask: ${ask}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Log bar completion
|
|
115
|
+
*/
|
|
116
|
+
bar(bar) {
|
|
117
|
+
this.log('BAR', `O:${bar.open} H:${bar.high} L:${bar.low} C:${bar.close} V:${bar.volume}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log swing detection
|
|
122
|
+
*/
|
|
123
|
+
swing(type, price, strength) {
|
|
124
|
+
this.log('SWING', `${type} @ ${price} | Strength: ${strength}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Log zone detection
|
|
129
|
+
*/
|
|
130
|
+
zone(type, high, low, touches) {
|
|
131
|
+
this.log('ZONE', `${type} Zone @ ${high}-${low} | Touches: ${touches}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Log signal generation
|
|
136
|
+
*/
|
|
137
|
+
signal(direction, price, confidence, reason) {
|
|
138
|
+
this.log('SIGNAL', `${direction} @ ${price} | Confidence: ${(confidence * 100).toFixed(1)}% | ${reason}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Log trade execution
|
|
143
|
+
*/
|
|
144
|
+
trade(action, direction, price, qty, orderId) {
|
|
145
|
+
this.log('TRADE', `${action} ${direction} x${qty} @ ${price} | OrderID: ${orderId}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Log P&L update
|
|
150
|
+
*/
|
|
151
|
+
pnl(realized, unrealized, position) {
|
|
152
|
+
this.log('PNL', `Realized: $${realized.toFixed(2)} | Unrealized: $${unrealized.toFixed(2)} | Position: ${position}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Log strategy state
|
|
157
|
+
*/
|
|
158
|
+
state(zonesCount, swingsCount, barsCount, bias) {
|
|
159
|
+
this.log('STATE', `Zones: ${zonesCount} | Swings: ${swingsCount} | Bars: ${barsCount} | Bias: ${bias}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Log error
|
|
164
|
+
*/
|
|
165
|
+
error(message, error) {
|
|
166
|
+
this.log('ERROR', message, { error: error?.message || error });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Log warning
|
|
171
|
+
*/
|
|
172
|
+
warn(message) {
|
|
173
|
+
this.log('WARN', message);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Log debug info
|
|
178
|
+
*/
|
|
179
|
+
debug(message, data) {
|
|
180
|
+
this.log('DEBUG', message, data);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* End session and write summary
|
|
185
|
+
*/
|
|
186
|
+
end(stats, stopReason = 'MANUAL') {
|
|
187
|
+
if (!this.logFile) return null;
|
|
188
|
+
|
|
189
|
+
// Flush remaining buffer
|
|
190
|
+
this._flush();
|
|
191
|
+
|
|
192
|
+
const endTime = new Date();
|
|
193
|
+
const duration = this._formatDuration(Date.now() - this.metadata.startTimestamp);
|
|
194
|
+
|
|
195
|
+
const summary = [
|
|
196
|
+
'',
|
|
197
|
+
'================================================================================',
|
|
198
|
+
'SESSION SUMMARY',
|
|
199
|
+
'================================================================================',
|
|
200
|
+
`Ended: ${endTime.toISOString()}`,
|
|
201
|
+
`Duration: ${duration}`,
|
|
202
|
+
`Stop Reason: ${stopReason}`,
|
|
203
|
+
'--------------------------------------------------------------------------------',
|
|
204
|
+
`Trades: ${stats.trades || 0}`,
|
|
205
|
+
`Wins: ${stats.wins || 0}`,
|
|
206
|
+
`Losses: ${stats.losses || 0}`,
|
|
207
|
+
`Win Rate: ${stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : 0}%`,
|
|
208
|
+
`P&L: $${(stats.pnl || 0).toFixed(2)}`,
|
|
209
|
+
`Target: $${this.metadata.target}`,
|
|
210
|
+
'================================================================================',
|
|
211
|
+
'',
|
|
212
|
+
].join('\n');
|
|
213
|
+
|
|
214
|
+
fs.appendFileSync(this.logFile, summary);
|
|
215
|
+
|
|
216
|
+
// Stop flush interval
|
|
217
|
+
if (this.flushInterval) {
|
|
218
|
+
clearInterval(this.flushInterval);
|
|
219
|
+
this.flushInterval = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const logPath = this.logFile;
|
|
223
|
+
this.logFile = null;
|
|
224
|
+
this.sessionId = null;
|
|
225
|
+
this.buffer = [];
|
|
226
|
+
this.metadata = {};
|
|
227
|
+
|
|
228
|
+
return logPath;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get elapsed time string
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
_getElapsed() {
|
|
236
|
+
if (!this.metadata.startTimestamp) return '00:00:00';
|
|
237
|
+
const elapsed = Date.now() - this.metadata.startTimestamp;
|
|
238
|
+
return this._formatDuration(elapsed);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format duration in HH:MM:SS
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
_formatDuration(ms) {
|
|
246
|
+
const seconds = Math.floor(ms / 1000);
|
|
247
|
+
const h = Math.floor(seconds / 3600);
|
|
248
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
249
|
+
const s = seconds % 60;
|
|
250
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Write log entry immediately
|
|
255
|
+
* @private
|
|
256
|
+
*/
|
|
257
|
+
_write(type, message, data = null) {
|
|
258
|
+
this.log(type, message, data);
|
|
259
|
+
this._flush();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Flush buffer to file
|
|
264
|
+
* @private
|
|
265
|
+
*/
|
|
266
|
+
_flush() {
|
|
267
|
+
if (!this.logFile || this.buffer.length === 0) return;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const content = this.buffer.join('\n') + '\n';
|
|
271
|
+
fs.appendFileSync(this.logFile, content);
|
|
272
|
+
this.buffer = [];
|
|
273
|
+
} catch (err) {
|
|
274
|
+
// Ignore write errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get path to sessions directory
|
|
280
|
+
*/
|
|
281
|
+
getSessionsDir() {
|
|
282
|
+
return this.sessionDir;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* List recent session logs
|
|
287
|
+
* @param {number} limit - Max number of sessions to return
|
|
288
|
+
*/
|
|
289
|
+
listSessions(limit = 10) {
|
|
290
|
+
if (!fs.existsSync(this.sessionDir)) return [];
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const files = fs.readdirSync(this.sessionDir)
|
|
294
|
+
.filter(f => f.endsWith('.log'))
|
|
295
|
+
.sort()
|
|
296
|
+
.reverse()
|
|
297
|
+
.slice(0, limit);
|
|
298
|
+
|
|
299
|
+
return files.map(f => ({
|
|
300
|
+
file: f,
|
|
301
|
+
path: path.join(this.sessionDir, f),
|
|
302
|
+
date: f.slice(0, 10),
|
|
303
|
+
time: f.slice(11, 19).replace(/-/g, ':'),
|
|
304
|
+
strategy: f.slice(20, -4)
|
|
305
|
+
}));
|
|
306
|
+
} catch {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Singleton instance
|
|
313
|
+
const sessionLogger = new SessionLogger();
|
|
314
|
+
|
|
315
|
+
module.exports = { sessionLogger, SessionLogger };
|