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.
- package/package.json +1 -1
- package/src/menus/ai-agent-connect.js +181 -0
- package/src/menus/ai-agent-models.js +219 -0
- package/src/menus/ai-agent-oauth.js +292 -0
- package/src/menus/ai-agent-ui.js +141 -0
- package/src/menus/ai-agent.js +88 -1489
- package/src/pages/algo/copy-engine.js +449 -0
- package/src/pages/algo/copy-trading.js +11 -543
- package/src/pages/algo/smart-logs-data.js +218 -0
- package/src/pages/algo/smart-logs.js +9 -214
- package/src/pages/algo/ui-constants.js +144 -0
- package/src/pages/algo/ui-summary.js +184 -0
- package/src/pages/algo/ui.js +42 -526
- package/src/pages/stats-calculations.js +191 -0
- package/src/pages/stats-ui.js +381 -0
- package/src/pages/stats.js +14 -507
- package/src/services/ai/client-analysis.js +194 -0
- package/src/services/ai/client-models.js +333 -0
- package/src/services/ai/client.js +6 -489
- package/src/services/ai/index.js +2 -257
- package/src/services/ai/proxy-install.js +249 -0
- package/src/services/ai/proxy-manager.js +29 -411
- package/src/services/ai/proxy-remote.js +161 -0
- package/src/services/ai/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -0
- package/src/services/ai/supervisor.js +50 -515
- package/src/services/ai/validation.js +250 -0
- package/src/services/hqx-server-events.js +110 -0
- package/src/services/hqx-server-handlers.js +217 -0
- package/src/services/hqx-server-latency.js +136 -0
- package/src/services/hqx-server.js +51 -403
- package/src/services/position-constants.js +28 -0
- package/src/services/position-manager.js +105 -554
- package/src/services/position-momentum.js +206 -0
- package/src/services/projectx/accounts.js +142 -0
- package/src/services/projectx/index.js +40 -289
- package/src/services/projectx/trading.js +180 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +32 -542
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
package/src/pages/algo/ui.js
CHANGED
|
@@ -8,129 +8,8 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const
|
|
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;
|
|
20
|
+
this.W = 96;
|
|
142
21
|
this.logs = [];
|
|
143
|
-
this.maxLogs = 40;
|
|
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 = '';
|
|
150
|
-
this.lastLogsHash = '';
|
|
151
|
-
this.lastSpinnerUpdate = 0;
|
|
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];
|
|
170
|
-
const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
|
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) +
|
|
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
|
-
|
|
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 };
|