hedgequantx 2.6.161 → 2.6.162

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/menus/ai-agent-connect.js +181 -0
  3. package/src/menus/ai-agent-models.js +219 -0
  4. package/src/menus/ai-agent-oauth.js +292 -0
  5. package/src/menus/ai-agent-ui.js +141 -0
  6. package/src/menus/ai-agent.js +88 -1489
  7. package/src/pages/algo/copy-engine.js +449 -0
  8. package/src/pages/algo/copy-trading.js +11 -543
  9. package/src/pages/algo/smart-logs-data.js +218 -0
  10. package/src/pages/algo/smart-logs.js +9 -214
  11. package/src/pages/algo/ui-constants.js +144 -0
  12. package/src/pages/algo/ui-summary.js +184 -0
  13. package/src/pages/algo/ui.js +42 -526
  14. package/src/pages/stats-calculations.js +191 -0
  15. package/src/pages/stats-ui.js +381 -0
  16. package/src/pages/stats.js +14 -507
  17. package/src/services/ai/client-analysis.js +194 -0
  18. package/src/services/ai/client-models.js +333 -0
  19. package/src/services/ai/client.js +6 -489
  20. package/src/services/ai/index.js +2 -257
  21. package/src/services/ai/proxy-install.js +249 -0
  22. package/src/services/ai/proxy-manager.js +29 -411
  23. package/src/services/ai/proxy-remote.js +161 -0
  24. package/src/services/ai/supervisor-optimize.js +215 -0
  25. package/src/services/ai/supervisor-sync.js +178 -0
  26. package/src/services/ai/supervisor.js +50 -515
  27. package/src/services/ai/validation.js +250 -0
  28. package/src/services/hqx-server-events.js +110 -0
  29. package/src/services/hqx-server-handlers.js +217 -0
  30. package/src/services/hqx-server-latency.js +136 -0
  31. package/src/services/hqx-server.js +51 -403
  32. package/src/services/position-constants.js +28 -0
  33. package/src/services/position-manager.js +105 -554
  34. package/src/services/position-momentum.js +206 -0
  35. package/src/services/projectx/accounts.js +142 -0
  36. package/src/services/projectx/index.js +40 -289
  37. package/src/services/projectx/trading.js +180 -0
  38. package/src/services/rithmic/handlers.js +2 -208
  39. package/src/services/rithmic/index.js +32 -542
  40. package/src/services/rithmic/latency-tracker.js +182 -0
  41. package/src/services/rithmic/specs.js +146 -0
  42. package/src/services/rithmic/trade-history.js +254 -0
@@ -8,129 +8,8 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
 
11
- // Box drawing characters
12
- const BOX = {
13
- TOP: '\u2554', BOT: '\u255A', V: '\u2551', H: '\u2550',
14
- TR: '\u2557', BR: '\u255D', ML: '\u2560', MR: '\u2563',
15
- TM: '\u2564', BM: '\u2567', MM: '\u256A', VS: '\u2502'
16
- };
17
-
18
- // Spinner characters
19
- const SPINNER = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
20
-
21
- // Log type colors - HF grade BOLD (FIXED colors - no variation)
22
- const LOG_COLORS = {
23
- fill_buy: chalk.green,
24
- fill_sell: chalk.red,
25
- fill_win: chalk.green,
26
- fill_loss: chalk.red,
27
- win: chalk.green,
28
- loss: chalk.red,
29
- be: chalk.yellow,
30
- entry: chalk.cyan,
31
- filled: chalk.green,
32
- connected: chalk.cyan,
33
- ready: chalk.green,
34
- error: chalk.red,
35
- reject: chalk.red,
36
- info: chalk.gray, // FIXED: gray (not white) - stable color
37
- system: chalk.magenta,
38
- signal: chalk.yellow,
39
- trade: chalk.green,
40
- warning: chalk.yellow,
41
- success: chalk.green,
42
- analysis: chalk.magenta
43
- };
44
-
45
- // Log type icons - Unicode icons with colors (ALL 8 chars wide for alignment)
46
- const LOG_ICONS = {
47
- fill_buy: '⬆ BUY ',
48
- fill_sell: '⬇ SELL ',
49
- fill_win: '✔ WIN ',
50
- fill_loss: '✘ LOSS ',
51
- win: '✔ WIN ',
52
- loss: '✘ LOSS ',
53
- be: '★ BE ',
54
- entry: '➡ ENTRY',
55
- filled: '✔ FILL ',
56
- connected: '✔ CONN ',
57
- ready: '✔ READY',
58
- error: '✘ ERROR',
59
- reject: '✘ REJCT',
60
- info: '➡ INFO ',
61
- system: '★ SYSTM',
62
- signal: '★ SIGNL',
63
- trade: '✔ TRADE',
64
- success: '✔ SUCCS',
65
- warning: '⬅ WARNG',
66
- analysis: '★ ANLYS'
67
- };
68
-
69
- /**
70
- * Strip ANSI codes from string
71
- */
72
- const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
73
-
74
- /**
75
- * Colorize message: positive numbers=cyan, negative=red, symbols=yellow
76
- */
77
- const colorizeMessage = (msg) => {
78
- if (!msg) return '';
79
-
80
- // Already has ANSI codes? Return as-is
81
- if (msg.includes('\x1B[')) return msg;
82
-
83
- return msg
84
- // Positive money: +$xxx.xx -> cyan
85
- .replace(/\+\$[\d,]+\.?\d*/g, (m) => chalk.cyan.bold(m))
86
- // Negative money: -$xxx.xx -> red
87
- .replace(/-\$[\d,]+\.?\d*/g, (m) => chalk.red.bold(m))
88
- // Positive numbers with + prefix: +123 -> cyan
89
- .replace(/\+\d+\.?\d*/g, (m) => chalk.cyan(m))
90
- // Negative numbers: -123 -> red
91
- .replace(/-\d+\.?\d*/g, (m) => chalk.red(m))
92
- // Symbols (futures contracts): NQ, ES, MNQ, MES, etc. -> yellow
93
- .replace(/\b(NQ|ES|MNQ|MES|RTY|M2K|YM|MYM|CL|GC|SI)[A-Z]\d\b/g, (m) => chalk.yellow.bold(m))
94
- // Percentages -> cyan
95
- .replace(/\d+\.?\d*%/g, (m) => chalk.cyan(m))
96
- // Prices (5 digits with decimal): 25688.50 -> white bold
97
- .replace(/\b\d{4,5}\.\d{2}\b/g, (m) => chalk.white.bold(m));
98
- };
99
-
100
- /**
101
- * Center text in width
102
- */
103
- const center = (text, width) => {
104
- const pad = Math.floor((width - text.length) / 2);
105
- return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
106
- };
107
-
108
- /**
109
- * Fit text to exact width (truncate or pad)
110
- */
111
- const fitToWidth = (text, width) => {
112
- const plain = stripAnsi(text);
113
- if (plain.length > width) {
114
- let count = 0, cut = 0;
115
- for (let i = 0; i < text.length && count < width - 3; i++) {
116
- if (text[i] === '\x1B') { while (i < text.length && text[i] !== 'm') i++; }
117
- else { count++; cut = i + 1; }
118
- }
119
- return text.substring(0, cut) + '...';
120
- }
121
- return text + ' '.repeat(width - plain.length);
122
- };
123
-
124
- /**
125
- * Build a labeled cell for grid - UPPERCASE BOLD style
126
- */
127
- const buildCell = (label, value, color, width) => {
128
- const upperLabel = label.toUpperCase();
129
- const upperValue = String(value).toUpperCase();
130
- const text = ` ${chalk.bold(upperLabel)}: ${color.bold(upperValue)}`;
131
- const plain = ` ${upperLabel}: ${upperValue}`;
132
- return { text, plain, padded: text + ' '.repeat(Math.max(0, width - plain.length)) };
133
- };
11
+ const { BOX, SPINNER, LOG_COLORS, LOG_ICONS, stripAnsi, colorizeMessage, center, fitToWidth, buildCell, checkMarketStatus } = require('./ui-constants');
12
+ const { renderSessionSummary, renderMultiSymbolSummary } = require('./ui-summary');
134
13
 
135
14
  /**
136
15
  * Create AlgoUI renderer
@@ -138,19 +17,17 @@ const buildCell = (label, value, color, width) => {
138
17
  class AlgoUI {
139
18
  constructor(config) {
140
19
  this.config = config;
141
- this.W = 96; // Fixed width
20
+ this.W = 96;
142
21
  this.logs = [];
143
- this.maxLogs = 40; // Fixed at 40 logs for one-account and copy-trading
22
+ this.maxLogs = 40;
144
23
  this.spinnerFrame = 0;
145
24
  this.firstDraw = true;
146
25
  this.isDrawing = false;
147
26
  this.lines = [];
148
27
  this.lastOutput = '';
149
- this.lastStatsHash = ''; // Track stats changes
150
- this.lastLogsHash = ''; // Track logs changes
151
- this.lastSpinnerUpdate = 0; // Rate limit spinner updates
152
-
153
- // Session log file
28
+ this.lastStatsHash = '';
29
+ this.lastLogsHash = '';
30
+ this.lastSpinnerUpdate = 0;
154
31
  this.logFile = null;
155
32
  this.logStream = null;
156
33
  this._initLogFile();
@@ -158,30 +35,22 @@ class AlgoUI {
158
35
 
159
36
  _initLogFile() {
160
37
  try {
161
- // Create logs directory in user home
162
38
  const logsDir = path.join(os.homedir(), '.hqx', 'logs');
163
39
  if (!fs.existsSync(logsDir)) {
164
40
  fs.mkdirSync(logsDir, { recursive: true });
165
41
  }
166
-
167
- // Create session log file with timestamp
168
42
  const now = new Date();
169
- const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
170
- const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS
43
+ const dateStr = now.toISOString().split('T')[0];
44
+ const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');
171
45
  const mode = this.config.mode || 'algo';
172
46
  this.logFile = path.join(logsDir, `session_${mode}_${dateStr}_${timeStr}.log`);
173
-
174
- // Open write stream
175
47
  this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
176
-
177
- // Write session header
178
48
  this.logStream.write(`\n${'='.repeat(80)}\n`);
179
49
  this.logStream.write(`HQX ALGO SESSION LOG\n`);
180
50
  this.logStream.write(`Mode: ${this.config.mode || 'unknown'}\n`);
181
51
  this.logStream.write(`Started: ${now.toISOString()}\n`);
182
52
  this.logStream.write(`${'='.repeat(80)}\n\n`);
183
53
  } catch (e) {
184
- // Silent fail - don't break UI if logging fails
185
54
  this.logStream = null;
186
55
  }
187
56
  }
@@ -190,8 +59,6 @@ class AlgoUI {
190
59
  const timestamp = new Date().toLocaleTimeString();
191
60
  this.logs.push({ timestamp, type, message });
192
61
  if (this.logs.length > this.maxLogs) this.logs.shift();
193
-
194
- // Write to log file
195
62
  if (this.logStream) {
196
63
  const isoTime = new Date().toISOString();
197
64
  const logLine = `[${isoTime}] [${type.toUpperCase().padEnd(8)}] ${message}\n`;
@@ -199,26 +66,18 @@ class AlgoUI {
199
66
  }
200
67
  }
201
68
 
202
- _line(text) {
203
- this.lines.push(text);
204
- }
69
+ _line(text) { this.lines.push(text); }
205
70
 
206
71
  _drawHeader() {
207
72
  const { W } = this;
208
73
  const version = require('../../../package.json').version;
209
-
210
- // Top border
211
74
  this._line(chalk.cyan(BOX.TOP + BOX.H.repeat(W) + BOX.TR));
212
-
213
- // Logo (compact)
214
75
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(BOX.V));
215
76
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(BOX.V));
216
77
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(BOX.V));
217
78
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(BOX.V));
218
79
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(BOX.V));
219
80
  this._line(chalk.cyan(BOX.V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(BOX.V));
220
-
221
- // Separator + title
222
81
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
223
82
  this._line(chalk.cyan(BOX.V) + chalk.white(center(`PROP FUTURES ALGO TRADING v${version}`, W)) + chalk.cyan(BOX.V));
224
83
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
@@ -226,206 +85,130 @@ class AlgoUI {
226
85
  }
227
86
 
228
87
  _drawStats(stats) {
229
- const { W } = this;
230
88
  const isCopyTrading = this.config.mode === 'copy-trading';
231
-
232
- const pnl = stats.pnl !== null && stats.pnl !== undefined ? stats.pnl : null;
233
- const pnlColor = pnl === null ? chalk.gray : (pnl >= 0 ? chalk.green : chalk.red);
234
- const pnlStr = pnl === null ? '--' : ((pnl >= 0 ? '+$' : '-$') + Math.abs(pnl).toFixed(2));
235
- const latencyColor = stats.latency < 100 ? chalk.green : (stats.latency < 300 ? chalk.yellow : chalk.red);
236
- const serverColor = stats.connected ? chalk.green : chalk.red;
237
-
238
- if (isCopyTrading) {
239
- this._drawCopyTradingStats(stats, pnlColor, pnlStr, latencyColor, serverColor);
240
- } else {
241
- this._drawOneAccountStats(stats, pnlColor, pnlStr, latencyColor, serverColor);
242
- }
89
+ if (isCopyTrading) this._drawCopyTradingStats(stats);
90
+ else this._drawOneAccountStats(stats);
243
91
  }
244
92
 
245
- _drawOneAccountStats(stats, pnlColor, pnlStr, latencyColor, serverColor) {
93
+ _drawOneAccountStats(stats) {
246
94
  const { W } = this;
247
95
  const colL = 48, colR = 47;
248
96
  const pad = (len) => ' '.repeat(Math.max(0, len));
249
-
250
97
  const GT = BOX.ML + BOX.H.repeat(colL) + BOX.TM + BOX.H.repeat(colR) + BOX.MR;
251
98
  const GM = BOX.ML + BOX.H.repeat(colL) + BOX.MM + BOX.H.repeat(colR) + BOX.MR;
252
99
  const GB = BOX.ML + BOX.H.repeat(colL) + BOX.BM + BOX.H.repeat(colR) + BOX.MR;
253
-
254
- const row = (c1, c2) => {
255
- this._line(chalk.cyan(BOX.V) + c1 + chalk.cyan(BOX.VS) + c2 + chalk.cyan(BOX.V));
256
- };
257
-
100
+ const row = (c1, c2) => { this._line(chalk.cyan(BOX.V) + c1 + chalk.cyan(BOX.VS) + c2 + chalk.cyan(BOX.V)); };
101
+
258
102
  this._line(chalk.cyan(GT));
259
-
260
- // Row 1: Account | Symbol
261
103
  const accountName = String(stats.accountName || 'N/A').substring(0, 40);
262
104
  const symbol = String(stats.symbol || 'N/A').substring(0, 35);
263
- const r1c1 = buildCell('Account', accountName, chalk.cyan, colL);
264
- const r1c2 = buildCell('Symbol', symbol, chalk.yellow, colR);
265
- row(r1c1.padded, r1c2.padded);
105
+ row(buildCell('Account', accountName, chalk.cyan, colL).padded, buildCell('Symbol', symbol, chalk.yellow, colR).padded);
266
106
 
267
107
  this._line(chalk.cyan(GM));
268
-
269
- // Row 2: Open P&L | Closed P&L (essential trading metrics)
270
108
  const openPnl = stats.openPnl;
271
109
  const closedPnl = stats.closedPnl;
272
- const openPnlStr = openPnl === null || openPnl === undefined
273
- ? '--'
274
- : (openPnl >= 0 ? `+$${openPnl.toFixed(2)}` : `-$${Math.abs(openPnl).toFixed(2)}`);
275
- const closedPnlStr = closedPnl === null || closedPnl === undefined
276
- ? '--'
277
- : (closedPnl >= 0 ? `+$${closedPnl.toFixed(2)}` : `-$${Math.abs(closedPnl).toFixed(2)}`);
110
+ const openPnlStr = openPnl === null || openPnl === undefined ? '--' : (openPnl >= 0 ? `+$${openPnl.toFixed(2)}` : `-$${Math.abs(openPnl).toFixed(2)}`);
111
+ const closedPnlStr = closedPnl === null || closedPnl === undefined ? '--' : (closedPnl >= 0 ? `+$${closedPnl.toFixed(2)}` : `-$${Math.abs(closedPnl).toFixed(2)}`);
278
112
  const openPnlColor = (openPnl || 0) >= 0 ? chalk.green : chalk.red;
279
113
  const closedPnlColor = (closedPnl || 0) >= 0 ? chalk.green : chalk.red;
280
- const r2c1 = buildCell('OPEN P&L', openPnlStr, openPnlColor, colL);
281
- const r2c2 = buildCell('CLOSED P&L', closedPnlStr, closedPnlColor, colR);
282
- row(r2c1.padded, r2c2.padded);
114
+ row(buildCell('OPEN P&L', openPnlStr, openPnlColor, colL).padded, buildCell('CLOSED P&L', closedPnlStr, closedPnlColor, colR).padded);
283
115
 
284
116
  this._line(chalk.cyan(GM));
285
-
286
- // Row 3: Target | Risk
287
117
  const targetStr = stats.target !== null && stats.target !== undefined ? '$' + stats.target.toFixed(2) : '--';
288
118
  const riskStr = stats.risk !== null && stats.risk !== undefined ? '$' + stats.risk.toFixed(2) : '--';
289
- const r3c1 = buildCell('TARGET', targetStr, chalk.green, colL);
290
- const r3c2 = buildCell('RISK', riskStr, chalk.red, colR);
291
- row(r3c1.padded, r3c2.padded);
119
+ row(buildCell('TARGET', targetStr, chalk.green, colL).padded, buildCell('RISK', riskStr, chalk.red, colR).padded);
292
120
 
293
121
  this._line(chalk.cyan(GM));
294
-
295
- // Row 4: Trades W/L | Propfirm
296
122
  const r4c1t = ` ${chalk.bold('TRADES')}: ${chalk.cyan.bold(stats.trades || 0)} ${chalk.bold('W/L')}: ${chalk.green.bold(stats.wins || 0)}/${chalk.red.bold(stats.losses || 0)}`;
297
123
  const r4c1p = ` TRADES: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
298
- const r4c2 = buildCell('PROPFIRM', stats.propfirm || 'N/A', chalk.cyan, colR);
299
- row(r4c1t + pad(colL - r4c1p.length), r4c2.padded);
124
+ row(r4c1t + pad(colL - r4c1p.length), buildCell('PROPFIRM', stats.propfirm || 'N/A', chalk.cyan, colR).padded);
300
125
 
301
126
  this._line(chalk.cyan(GM));
302
-
303
- // Row 5: Connection | Agents
304
127
  const connectionType = stats.platform || stats.connection || 'N/A';
305
128
  const connectionColor = connectionType.toLowerCase().includes('rithmic') ? chalk.green : chalk.yellow;
306
129
  const agentCount = stats.agentCount || 0;
307
130
  const agentStr = agentCount > 0 ? `${agentCount} agent${agentCount > 1 ? 's' : ''} active` : 'None';
308
131
  const agentColor = agentCount > 0 ? chalk.green : chalk.gray;
309
- const r5c1 = buildCell('CONNECTION', connectionType, connectionColor, colL);
310
- const r5c2 = buildCell('AGENTS', agentStr, agentColor, colR);
311
- row(r5c1.padded, r5c2.padded);
132
+ row(buildCell('CONNECTION', connectionType, connectionColor, colL).padded, buildCell('AGENTS', agentStr, agentColor, colR).padded);
312
133
 
313
134
  this._line(chalk.cyan(GB));
314
135
  }
315
136
 
316
- _drawCopyTradingStats(stats, pnlColor, pnlStr, latencyColor, serverColor) {
137
+ _drawCopyTradingStats(stats) {
317
138
  const { W } = this;
318
139
  const colL = 48, colR = 47;
319
140
  const pad = (len) => ' '.repeat(Math.max(0, len));
320
-
321
141
  const GT = BOX.ML + BOX.H.repeat(colL) + BOX.TM + BOX.H.repeat(colR) + BOX.MR;
322
142
  const GM = BOX.ML + BOX.H.repeat(colL) + BOX.MM + BOX.H.repeat(colR) + BOX.MR;
323
143
  const GB = BOX.ML + BOX.H.repeat(colL) + BOX.BM + BOX.H.repeat(colR) + BOX.MR;
324
-
325
- const row = (c1, c2) => {
326
- this._line(chalk.cyan(BOX.V) + c1 + chalk.cyan(BOX.VS) + c2 + chalk.cyan(BOX.V));
327
- };
328
-
144
+ const GF = BOX.ML + BOX.H.repeat(W) + BOX.MR;
145
+ const row = (c1, c2) => { this._line(chalk.cyan(BOX.V) + c1 + chalk.cyan(BOX.VS) + c2 + chalk.cyan(BOX.V)); };
146
+
329
147
  this._line(chalk.cyan(GT));
330
-
331
- // Row 1: Lead Account | Follower Account
332
148
  const leadName = (stats.leadName || 'N/A').substring(0, 40);
333
149
  const followerName = (stats.followerName || 'N/A').substring(0, 40);
334
- const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
335
- const r1c2 = buildCell('Follower', followerName, chalk.magenta, colR);
336
- row(r1c1.padded, r1c2.padded);
337
-
338
- // Full width separator
339
- const GF = BOX.ML + BOX.H.repeat(W) + BOX.MR;
150
+ row(buildCell('Lead', leadName, chalk.cyan, colL).padded, buildCell('Follower', followerName, chalk.magenta, colR).padded);
340
151
 
341
152
  this._line(chalk.cyan(GF));
342
-
343
- // Row 2: Symbol (centered, single row)
344
153
  const symbol = (stats.symbol || stats.leadSymbol || 'N/A').substring(0, 60);
345
154
  const symbolText = `SYMBOL: ${symbol}`;
346
- const symbolPadded = center(symbolText, W);
347
- this._line(chalk.cyan(BOX.V) + chalk.yellow(symbolPadded) + chalk.cyan(BOX.V));
155
+ this._line(chalk.cyan(BOX.V) + chalk.yellow(center(symbolText, W)) + chalk.cyan(BOX.V));
348
156
 
349
157
  this._line(chalk.cyan(GT));
350
-
351
- // Row 3: Lead Qty | Follower Qty
352
- const r3c1 = buildCell('QTY', (stats.leadQty || '1').toString(), chalk.cyan, colL);
353
- const r3c2 = buildCell('QTY', (stats.followerQty || '1').toString(), chalk.cyan, colR);
354
- row(r3c1.padded, r3c2.padded);
158
+ row(buildCell('QTY', (stats.leadQty || '1').toString(), chalk.cyan, colL).padded, buildCell('QTY', (stats.followerQty || '1').toString(), chalk.cyan, colR).padded);
355
159
 
356
160
  this._line(chalk.cyan(GM));
357
-
358
- // Row 4: Target | Risk
359
- const r4c1 = buildCell('TARGET', '$' + (stats.target || 0).toFixed(2), chalk.green, colL);
360
- const r4c2 = buildCell('RISK', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR);
361
- row(r4c1.padded, r4c2.padded);
161
+ row(buildCell('TARGET', '$' + (stats.target || 0).toFixed(2), chalk.green, colL).padded, buildCell('RISK', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR).padded);
362
162
 
363
163
  this._line(chalk.cyan(GM));
364
-
365
- // Row 5: P&L | Trades - UPPERCASE BOLD
366
- const r5c1 = buildCell('P&L', pnlStr, pnlColor, colL);
164
+ const pnl = stats.pnl !== null && stats.pnl !== undefined ? stats.pnl : null;
165
+ const pnlColor = pnl === null ? chalk.gray : (pnl >= 0 ? chalk.green : chalk.red);
166
+ const pnlStr = pnl === null ? '--' : ((pnl >= 0 ? '+$' : '-$') + Math.abs(pnl).toFixed(2));
367
167
  const r5c2t = ` ${chalk.bold('TRADES')}: ${chalk.cyan.bold(stats.trades || 0)} ${chalk.bold('W/L')}: ${chalk.green.bold(stats.wins || 0)}/${chalk.red.bold(stats.losses || 0)}`;
368
168
  const r5c2p = ` TRADES: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
369
- row(r5c1.padded, r5c2t + pad(colR - r5c2p.length));
169
+ row(buildCell('P&L', pnlStr, pnlColor, colL).padded, r5c2t + pad(colR - r5c2p.length));
370
170
 
371
171
  this._line(chalk.cyan(GB));
372
172
  }
373
173
 
374
174
  _drawLogs() {
375
175
  const { W, logs, maxLogs } = this;
376
-
377
- // Activity header with animated spinner
378
- // Date is cached on first draw to prevent changes
379
176
  if (!this.cachedDate) {
380
- const nowDate = new Date();
381
- this.cachedDate = nowDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
177
+ this.cachedDate = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
382
178
  }
383
179
  const dateStr = this.cachedDate;
384
-
385
- // Animated spinner - advances on each render
386
180
  const spinner = SPINNER[this.spinnerFrame % SPINNER.length];
387
181
  this.spinnerFrame++;
388
182
 
389
183
  const leftText = ` EXECUTION LOG ${spinner}`;
390
184
  const rightText = `[X] STOP `;
391
-
392
185
  const totalFixed = leftText.length + rightText.length;
393
186
  const centerSpace = W - totalFixed;
394
187
  const centerPadLeft = Math.floor((centerSpace - dateStr.length) / 2);
395
188
  const centerPadRight = centerSpace - dateStr.length - centerPadLeft;
396
189
 
397
190
  const left = ` ${chalk.bold('EXECUTION LOG')} ${chalk.green(spinner)}`;
398
- const center = ' '.repeat(Math.max(0, centerPadLeft)) + chalk.white.bold(dateStr) + ' '.repeat(Math.max(0, centerPadRight));
191
+ const ctr = ' '.repeat(Math.max(0, centerPadLeft)) + chalk.white.bold(dateStr) + ' '.repeat(Math.max(0, centerPadRight));
399
192
  const right = chalk.yellow.bold('[X] STOP') + ' ';
400
193
 
401
- this._line(chalk.cyan(BOX.V) + chalk.white(left) + center + right + chalk.cyan(BOX.V));
194
+ this._line(chalk.cyan(BOX.V) + chalk.white(left) + ctr + right + chalk.cyan(BOX.V));
402
195
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
403
196
 
404
- // Logs: newest at top
405
197
  const visible = logs.slice(-maxLogs).reverse();
406
-
407
198
  if (visible.length === 0) {
408
199
  this._line(chalk.cyan(BOX.V) + chalk.gray(fitToWidth(' Awaiting market signals...', W)) + chalk.cyan(BOX.V));
409
- for (let i = 0; i < maxLogs - 1; i++) {
410
- this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
411
- }
200
+ for (let i = 0; i < maxLogs - 1; i++) this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
412
201
  } else {
413
202
  visible.forEach(log => {
414
203
  const color = LOG_COLORS[log.type] || chalk.gray;
415
204
  const icon = LOG_ICONS[log.type] || '➡ INFO ';
416
- // HF style: TIME | COLORED TYPE | MESSAGE with colored numbers
417
205
  const coloredIcon = color.bold(icon);
418
- // Color numbers: positive=cyan, negative=red, symbols=yellow
419
206
  const coloredMessage = colorizeMessage(log.message);
420
207
  const line = ` ${chalk.gray(log.timestamp)} ${coloredIcon} ${coloredMessage}`;
421
208
  this._line(chalk.cyan(BOX.V) + fitToWidth(line, W) + chalk.cyan(BOX.V));
422
209
  });
423
- for (let i = visible.length; i < maxLogs; i++) {
424
- this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
425
- }
210
+ for (let i = visible.length; i < maxLogs; i++) this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
426
211
  }
427
-
428
- // Bottom border
429
212
  this._line(chalk.cyan(BOX.BOT + BOX.H.repeat(W) + BOX.BR));
430
213
  }
431
214
 
@@ -433,31 +216,16 @@ class AlgoUI {
433
216
  if (this.isDrawing) return;
434
217
  this.isDrawing = true;
435
218
 
436
- // Quick hash to detect meaningful changes
437
- const statsHash = JSON.stringify({
438
- pnl: stats.pnl,
439
- openPnl: stats.openPnl,
440
- closedPnl: stats.closedPnl,
441
- position: stats.position,
442
- trades: stats.trades,
443
- wins: stats.wins,
444
- losses: stats.losses,
445
- connected: stats.connected,
446
- });
219
+ const statsHash = JSON.stringify({ pnl: stats.pnl, openPnl: stats.openPnl, closedPnl: stats.closedPnl, position: stats.position, trades: stats.trades, wins: stats.wins, losses: stats.losses, connected: stats.connected });
447
220
  const logsHash = this.logs.length + (this.logs[this.logs.length - 1]?.message || '');
448
-
449
- // Check if anything meaningful changed
450
221
  const hasChanges = statsHash !== this.lastStatsHash || logsHash !== this.lastLogsHash;
451
222
 
452
- // First draw or changes detected
453
223
  if (this.firstDraw || hasChanges) {
454
224
  this.lastStatsHash = statsHash;
455
225
  this.lastLogsHash = logsHash;
456
-
457
226
  this.lines = [];
458
227
 
459
228
  if (this.firstDraw) {
460
- // Clear screen once, hide cursor
461
229
  console.clear();
462
230
  process.stdout.write('\x1B[?25l');
463
231
  this.firstDraw = false;
@@ -468,21 +236,15 @@ class AlgoUI {
468
236
  this._drawStats(stats);
469
237
  this._drawLogs();
470
238
 
471
- // Build output
472
239
  const output = this.lines.join('\n');
473
-
474
- // Only write if content changed
475
240
  if (output !== this.lastOutput) {
476
- // Write each line at fixed position (no full screen redraw)
477
241
  const lines = this.lines;
478
242
  for (let i = 0; i < lines.length; i++) {
479
- // Move to line i+1, column 1, then write line and clear to end
480
243
  process.stdout.write(`\x1B[${i + 1};1H${lines[i]}\x1B[K`);
481
244
  }
482
245
  this.lastOutput = output;
483
246
  }
484
247
  }
485
-
486
248
  this.isDrawing = false;
487
249
  }
488
250
 
@@ -491,9 +253,6 @@ class AlgoUI {
491
253
  console.clear();
492
254
  }
493
255
 
494
- /**
495
- * Close log file with session summary
496
- */
497
256
  closeLog(stats) {
498
257
  if (this.logStream) {
499
258
  try {
@@ -510,254 +269,11 @@ class AlgoUI {
510
269
  }
511
270
  this.logStream.write(`${'='.repeat(80)}\n`);
512
271
  this.logStream.end();
513
- } catch (e) {
514
- // Silent fail
515
- }
272
+ } catch (e) {}
516
273
  }
517
274
  }
518
275
 
519
- /**
520
- * Get log file path
521
- */
522
- getLogFile() {
523
- return this.logFile;
524
- }
276
+ getLogFile() { return this.logFile; }
525
277
  }
526
278
 
527
- /**
528
- * Check market hours
529
- */
530
- const checkMarketStatus = () => {
531
- const now = new Date();
532
- const utcDay = now.getUTCDay();
533
- const utcHour = now.getUTCHours();
534
- const isDST = now.getTimezoneOffset() < Math.max(
535
- new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
536
- new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
537
- );
538
- const ctOffset = isDST ? 5 : 6;
539
- const ctHour = (utcHour - ctOffset + 24) % 24;
540
- const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
541
-
542
- if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
543
- if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
544
- if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
545
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
546
- return { isOpen: true, message: 'Market OPEN' };
547
- };
548
-
549
- /**
550
- * Render Session Summary - Same style as dashboard
551
- */
552
- const renderSessionSummary = (stats, stopReason) => {
553
- const W = 96; // Same width as dashboard
554
- const colL = Math.floor(W / 2) - 1;
555
- const colR = W - colL - 1;
556
- const version = require('../../../package.json').version;
557
-
558
- console.clear();
559
- console.log();
560
-
561
- // Top border
562
- console.log(chalk.cyan(BOX.TOP + BOX.H.repeat(W) + BOX.TR));
563
-
564
- // Logo
565
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(BOX.V));
566
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(BOX.V));
567
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(BOX.V));
568
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(BOX.V));
569
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(BOX.V));
570
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(BOX.V));
571
-
572
- // Separator + title
573
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
574
- console.log(chalk.cyan(BOX.V) + chalk.white(center(`PROP FUTURES ALGO TRADING v${version}`, W)) + chalk.cyan(BOX.V));
575
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
576
- console.log(chalk.cyan(BOX.V) + chalk.yellow.bold(center('SESSION SUMMARY', W)) + chalk.cyan(BOX.V));
577
-
578
- // Grid separators
579
- const GT = BOX.ML + BOX.H.repeat(colL) + BOX.TM + BOX.H.repeat(colR) + BOX.MR;
580
- const GM = BOX.ML + BOX.H.repeat(colL) + BOX.MM + BOX.H.repeat(colR) + BOX.MR;
581
-
582
- const row = (label1, value1, color1, label2, value2, color2) => {
583
- const upperLabel1 = label1.toUpperCase();
584
- const upperLabel2 = label2.toUpperCase();
585
- const upperValue1 = String(value1).toUpperCase();
586
- const upperValue2 = String(value2).toUpperCase();
587
- const c1 = ` ${chalk.bold(upperLabel1)}: ${color1.bold(upperValue1)}`;
588
- const c2 = ` ${chalk.bold(upperLabel2)}: ${color2.bold(upperValue2)}`;
589
- const p1 = ` ${upperLabel1}: ${upperValue1}`;
590
- const p2 = ` ${upperLabel2}: ${upperValue2}`;
591
- const padded1 = c1 + ' '.repeat(Math.max(0, colL - p1.length));
592
- const padded2 = c2 + ' '.repeat(Math.max(0, colR - p2.length));
593
- console.log(chalk.cyan(BOX.V) + padded1 + chalk.cyan(BOX.VS) + padded2 + chalk.cyan(BOX.V));
594
- };
595
-
596
- console.log(chalk.cyan(GT));
597
-
598
- // Row 1: Stop Reason | Duration
599
- const duration = stats.duration || '--';
600
- const reasonColor = stopReason === 'target' ? chalk.green : stopReason === 'risk' ? chalk.red : chalk.yellow;
601
- row('Stop Reason', (stopReason || 'manual').toUpperCase(), reasonColor, 'Duration', duration, chalk.white);
602
-
603
- console.log(chalk.cyan(GM));
604
-
605
- // Row 2: Trades | Win Rate
606
- const winRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) + '%' : '0%';
607
- row('Trades', String(stats.trades || 0), chalk.white, 'Win Rate', winRate, stats.wins >= stats.losses ? chalk.green : chalk.red);
608
-
609
- console.log(chalk.cyan(GM));
610
-
611
- // Row 3: Wins | Losses
612
- row('Wins', String(stats.wins || 0), chalk.green, 'Losses', String(stats.losses || 0), chalk.red);
613
-
614
- console.log(chalk.cyan(GM));
615
-
616
- // Row 4: Session P&L | Target
617
- // Use sessionPnl (trades from this HQX session) if available, fallback to pnl
618
- const pnl = stats.sessionPnl !== undefined ? stats.sessionPnl : (stats.pnl || 0);
619
- const pnlStr = `${pnl >= 0 ? '+' : ''}$${Math.abs(pnl).toFixed(2)}`;
620
- const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
621
- const targetStr = `$${(stats.target || 0).toFixed(2)}`;
622
- row('Session P&L', pnlStr, pnlColor, 'TARGET', targetStr, chalk.cyan);
623
-
624
- // Bottom border
625
- console.log(chalk.cyan(BOX.BOT + BOX.H.repeat(W) + BOX.BR));
626
- console.log();
627
- };
628
-
629
- /**
630
- * Render Multi-Symbol Session Summary - Same style as single-symbol
631
- * All columns centered
632
- */
633
- const renderMultiSymbolSummary = (stats, stopReason, symbolStats) => {
634
- const W = 96;
635
- const version = require('../../../package.json').version;
636
-
637
- // Helper: center text in column width
638
- const centerCol = (text, width) => {
639
- const pad = Math.floor((width - text.length) / 2);
640
- return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
641
- };
642
-
643
- console.clear();
644
- console.log();
645
-
646
- // Top border
647
- console.log(chalk.cyan(BOX.TOP + BOX.H.repeat(W) + BOX.TR));
648
-
649
- // Logo
650
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(BOX.V));
651
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(BOX.V));
652
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(BOX.V));
653
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(BOX.V));
654
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(BOX.V));
655
- console.log(chalk.cyan(BOX.V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(BOX.V));
656
-
657
- // Separator + title
658
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
659
- console.log(chalk.cyan(BOX.V) + chalk.white(center(`PROP FUTURES ALGO TRADING v${version}`, W)) + chalk.cyan(BOX.V));
660
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
661
- console.log(chalk.cyan(BOX.V) + chalk.yellow.bold(center('MULTI-SYMBOL SESSION SUMMARY', W)) + chalk.cyan(BOX.V));
662
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
663
-
664
- // Column widths (total = 96 - 5 separators = 91)
665
- const colSymbol = 14;
666
- const colTrades = 12;
667
- const colWR = 14;
668
- const colWins = 12;
669
- const colLosses = 12;
670
- const colPnL = W - colSymbol - colTrades - colWR - colWins - colLosses - 5; // remaining
671
-
672
- // Header row - centered
673
- const headerSymbol = centerCol('SYMBOL', colSymbol);
674
- const headerTrades = centerCol('TRADES', colTrades);
675
- const headerWR = centerCol('WIN RATE', colWR);
676
- const headerWins = centerCol('WINS', colWins);
677
- const headerLosses = centerCol('LOSSES', colLosses);
678
- const headerPnL = centerCol('P&L', colPnL);
679
-
680
- console.log(chalk.cyan(BOX.V) + chalk.bold.white(headerSymbol) + chalk.cyan(BOX.VS) +
681
- chalk.bold.white(headerTrades) + chalk.cyan(BOX.VS) +
682
- chalk.bold.white(headerWR) + chalk.cyan(BOX.VS) +
683
- chalk.bold.white(headerWins) + chalk.cyan(BOX.VS) +
684
- chalk.bold.white(headerLosses) + chalk.cyan(BOX.VS) +
685
- chalk.bold.white(headerPnL) + chalk.cyan(BOX.V));
686
-
687
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
688
-
689
- // Per-symbol rows - centered
690
- for (const [symbol, symStats] of Object.entries(symbolStats)) {
691
- const winRate = symStats.trades > 0 ? ((symStats.wins / symStats.trades) * 100).toFixed(0) + '%' : '0%';
692
- const pnl = symStats.pnl || 0;
693
- const pnlStr = (pnl >= 0 ? '+$' : '-$') + Math.abs(pnl).toFixed(2);
694
- const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
695
- const wrColor = symStats.wins >= symStats.losses ? chalk.green : chalk.red;
696
-
697
- const cellSymbol = centerCol(symbol, colSymbol);
698
- const cellTrades = centerCol(String(symStats.trades || 0), colTrades);
699
- const cellWR = centerCol(winRate, colWR);
700
- const cellWins = centerCol(String(symStats.wins || 0), colWins);
701
- const cellLosses = centerCol(String(symStats.losses || 0), colLosses);
702
- const cellPnL = centerCol(pnlStr, colPnL);
703
-
704
- console.log(chalk.cyan(BOX.V) + chalk.yellow(cellSymbol) + chalk.cyan(BOX.VS) +
705
- chalk.white(cellTrades) + chalk.cyan(BOX.VS) +
706
- wrColor(cellWR) + chalk.cyan(BOX.VS) +
707
- chalk.green(cellWins) + chalk.cyan(BOX.VS) +
708
- chalk.red(cellLosses) + chalk.cyan(BOX.VS) +
709
- pnlColor.bold(cellPnL) + chalk.cyan(BOX.V));
710
- }
711
-
712
- // Separator before totals
713
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
714
-
715
- // Total row - centered
716
- const totalWinRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(0) + '%' : '0%';
717
- const totalPnl = stats.sessionPnl || 0;
718
- const totalPnlStr = (totalPnl >= 0 ? '+$' : '-$') + Math.abs(totalPnl).toFixed(2);
719
- const totalPnlColor = totalPnl >= 0 ? chalk.green : chalk.red;
720
- const totalWrColor = stats.wins >= stats.losses ? chalk.green : chalk.red;
721
-
722
- const totalCellSymbol = centerCol('TOTAL', colSymbol);
723
- const totalCellTrades = centerCol(String(stats.trades || 0), colTrades);
724
- const totalCellWR = centerCol(totalWinRate, colWR);
725
- const totalCellWins = centerCol(String(stats.wins || 0), colWins);
726
- const totalCellLosses = centerCol(String(stats.losses || 0), colLosses);
727
- const totalCellPnL = centerCol(totalPnlStr, colPnL);
728
-
729
- console.log(chalk.cyan(BOX.V) + chalk.bold.cyan(totalCellSymbol) + chalk.cyan(BOX.VS) +
730
- chalk.bold.white(totalCellTrades) + chalk.cyan(BOX.VS) +
731
- totalWrColor.bold(totalCellWR) + chalk.cyan(BOX.VS) +
732
- chalk.bold.green(totalCellWins) + chalk.cyan(BOX.VS) +
733
- chalk.bold.red(totalCellLosses) + chalk.cyan(BOX.VS) +
734
- totalPnlColor.bold(totalCellPnL) + chalk.cyan(BOX.V));
735
-
736
- // Separator
737
- console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
738
-
739
- // Stop Reason & Duration row - centered
740
- const duration = stats.duration || '--';
741
- const reasonColor = stopReason === 'target' ? chalk.green : stopReason === 'risk' ? chalk.red : chalk.yellow;
742
- const reasonStr = (stopReason || 'manual').toUpperCase();
743
- const infoPlain = `STOP: ${reasonStr} | DURATION: ${duration} | TARGET: $${(stats.target || 0).toFixed(2)} | RISK: $${(stats.risk || 0).toFixed(2)}`;
744
- const infoPadded = center(infoPlain, W);
745
-
746
- // Build colored version with same centering
747
- const padLeft = Math.floor((W - infoPlain.length) / 2);
748
- const padRight = W - infoPlain.length - padLeft;
749
- const infoColored = ' '.repeat(padLeft) +
750
- chalk.bold('STOP') + ': ' + reasonColor.bold(reasonStr) + ' | ' +
751
- chalk.bold('DURATION') + ': ' + chalk.white(duration) + ' | ' +
752
- chalk.bold('TARGET') + ': ' + chalk.cyan('$' + (stats.target || 0).toFixed(2)) + ' | ' +
753
- chalk.bold('RISK') + ': ' + chalk.red('$' + (stats.risk || 0).toFixed(2)) +
754
- ' '.repeat(padRight);
755
-
756
- console.log(chalk.cyan(BOX.V) + infoColored + chalk.cyan(BOX.V));
757
-
758
- // Bottom border
759
- console.log(chalk.cyan(BOX.BOT + BOX.H.repeat(W) + BOX.BR));
760
- console.log();
761
- };
762
-
763
279
  module.exports = { AlgoUI, checkMarketStatus, renderSessionSummary, renderMultiSymbolSummary, LOG_COLORS, LOG_ICONS, stripAnsi, center, fitToWidth };