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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.117",
3
+ "version": "2.9.118",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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
- // Determine market bias
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
- const ticksR = Math.round(gapR / tickSize);
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
- const ticksS = Math.round(gapS / tickSize);
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 (when no position)
331
- if (currentSecond % 20 === 0 && currentPosition === 0) {
332
- const scanLog = smartLogs.getScanningLog(true);
333
- ui.addLog('system', scanLog.message);
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 (exchange timestamp)
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
- // Only update if reasonable (0-5000ms) - avoids clock sync issues
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 };