hedgequantx 1.2.144 → 1.2.146

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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * One Account Mode - Single account algo trading
3
+ * Lightweight - UI + HQX Server connection only
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const inquirer = require('inquirer');
9
+ const readline = require('readline');
10
+
11
+ const { connections } = require('../../services');
12
+ const { HQXServerService } = require('../../services/hqx-server');
13
+ const { FUTURES_SYMBOLS } = require('../../config');
14
+ const { AlgoUI, checkMarketStatus } = require('./ui');
15
+
16
+ /**
17
+ * One Account Menu - Select account and launch
18
+ */
19
+ const oneAccountMenu = async (service) => {
20
+ const spinner = ora('Fetching active accounts...').start();
21
+
22
+ const result = await service.getTradingAccounts();
23
+
24
+ if (!result.success || !result.accounts?.length) {
25
+ spinner.fail('No accounts found');
26
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
27
+ return;
28
+ }
29
+
30
+ const activeAccounts = result.accounts.filter(acc => acc.status === 0);
31
+
32
+ if (!activeAccounts.length) {
33
+ spinner.fail('No active accounts');
34
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
35
+ return;
36
+ }
37
+
38
+ spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
39
+
40
+ // Select account
41
+ const { selectedAccount } = await inquirer.prompt([{
42
+ type: 'list',
43
+ name: 'selectedAccount',
44
+ message: 'Select Account:',
45
+ choices: [
46
+ ...activeAccounts.map(acc => ({
47
+ name: chalk.cyan(`${acc.accountName || acc.accountId} - $${acc.balance.toLocaleString()}`),
48
+ value: acc
49
+ })),
50
+ new inquirer.Separator(),
51
+ { name: chalk.yellow('< Back'), value: 'back' }
52
+ ]
53
+ }]);
54
+
55
+ if (selectedAccount === 'back') return;
56
+
57
+ // Select symbol
58
+ const contract = await selectSymbol(service, selectedAccount);
59
+ if (!contract) return;
60
+
61
+ // Configure algo
62
+ const config = await configureAlgo(selectedAccount, contract);
63
+ if (!config) return;
64
+
65
+ // Launch
66
+ await launchAlgo(service, selectedAccount, contract, config);
67
+ };
68
+
69
+ /**
70
+ * Symbol selection
71
+ */
72
+ const selectSymbol = async (service, account) => {
73
+ const spinner = ora('Loading contracts...').start();
74
+
75
+ const contractsResult = await service.getContracts();
76
+ if (!contractsResult.success) {
77
+ spinner.fail('Failed to load contracts');
78
+ return null;
79
+ }
80
+
81
+ spinner.succeed('Contracts loaded');
82
+
83
+ // Group by category
84
+ const categories = {};
85
+ for (const c of contractsResult.contracts) {
86
+ const cat = c.group || 'Other';
87
+ if (!categories[cat]) categories[cat] = [];
88
+ categories[cat].push(c);
89
+ }
90
+
91
+ // Build choices
92
+ const choices = [];
93
+ for (const [cat, contracts] of Object.entries(categories)) {
94
+ choices.push(new inquirer.Separator(chalk.gray(`--- ${cat} ---`)));
95
+ for (const c of contracts.slice(0, 10)) {
96
+ choices.push({ name: chalk.white(c.name || c.symbol), value: c });
97
+ }
98
+ }
99
+ choices.push(new inquirer.Separator());
100
+ choices.push({ name: chalk.yellow('< Back'), value: 'back' });
101
+
102
+ const { contract } = await inquirer.prompt([{
103
+ type: 'list',
104
+ name: 'contract',
105
+ message: 'Select Symbol:',
106
+ choices,
107
+ pageSize: 20
108
+ }]);
109
+
110
+ return contract === 'back' ? null : contract;
111
+ };
112
+
113
+ /**
114
+ * Configure algo parameters
115
+ */
116
+ const configureAlgo = async (account, contract) => {
117
+ console.log();
118
+ console.log(chalk.cyan(' Configure Algo Parameters'));
119
+ console.log();
120
+
121
+ const { contracts } = await inquirer.prompt([{
122
+ type: 'number',
123
+ name: 'contracts',
124
+ message: 'Number of contracts:',
125
+ default: 1,
126
+ validate: v => v > 0 && v <= 10 ? true : 'Enter 1-10'
127
+ }]);
128
+
129
+ const { dailyTarget } = await inquirer.prompt([{
130
+ type: 'number',
131
+ name: 'dailyTarget',
132
+ message: 'Daily target ($):',
133
+ default: 200,
134
+ validate: v => v > 0 ? true : 'Must be positive'
135
+ }]);
136
+
137
+ const { maxRisk } = await inquirer.prompt([{
138
+ type: 'number',
139
+ name: 'maxRisk',
140
+ message: 'Max risk ($):',
141
+ default: 100,
142
+ validate: v => v > 0 ? true : 'Must be positive'
143
+ }]);
144
+
145
+ const { showName } = await inquirer.prompt([{
146
+ type: 'confirm',
147
+ name: 'showName',
148
+ message: 'Show account name?',
149
+ default: true
150
+ }]);
151
+
152
+ const { confirm } = await inquirer.prompt([{
153
+ type: 'confirm',
154
+ name: 'confirm',
155
+ message: chalk.yellow('Start algo trading?'),
156
+ default: true
157
+ }]);
158
+
159
+ if (!confirm) return null;
160
+
161
+ return { contracts, dailyTarget, maxRisk, showName };
162
+ };
163
+
164
+ /**
165
+ * Launch algo trading
166
+ */
167
+ const launchAlgo = async (service, account, contract, config) => {
168
+ const { contracts, dailyTarget, maxRisk, showName } = config;
169
+ const accountName = showName ? (account.accountName || account.accountId) : 'HQX *****';
170
+ const symbolName = contract.name || contract.symbol;
171
+
172
+ // Initialize UI
173
+ const ui = new AlgoUI({ subtitle: 'HQX Ultra-Scalping' });
174
+
175
+ // Stats state
176
+ const stats = {
177
+ accountName,
178
+ symbol: symbolName,
179
+ contracts,
180
+ target: dailyTarget,
181
+ risk: maxRisk,
182
+ pnl: 0,
183
+ trades: 0,
184
+ wins: 0,
185
+ losses: 0,
186
+ latency: 0,
187
+ connected: false
188
+ };
189
+
190
+ let running = true;
191
+ let stopReason = null;
192
+
193
+ // Connect to HQX Server
194
+ const hqx = new HQXServerService();
195
+
196
+ const spinner = ora('Connecting to HQX Server...').start();
197
+
198
+ try {
199
+ const auth = await hqx.authenticate(account.accountId.toString(), account.propfirm || 'topstep');
200
+ if (!auth.success) throw new Error(auth.error || 'Auth failed');
201
+
202
+ spinner.text = 'Connecting WebSocket...';
203
+ const conn = await hqx.connect();
204
+ if (!conn.success) throw new Error('WebSocket failed');
205
+
206
+ spinner.succeed('Connected to HQX Server');
207
+ stats.connected = true;
208
+
209
+ } catch (err) {
210
+ spinner.warn('HQX Server unavailable - offline mode');
211
+ stats.connected = false;
212
+ }
213
+
214
+ // Event handlers
215
+ hqx.on('latency', (data) => { stats.latency = data.latency || 0; });
216
+
217
+ hqx.on('log', (data) => {
218
+ let msg = data.message;
219
+ if (!showName && account.accountName) {
220
+ msg = msg.replace(new RegExp(account.accountName, 'gi'), 'HQX *****');
221
+ }
222
+ ui.addLog(data.type || 'info', msg);
223
+ });
224
+
225
+ hqx.on('signal', (data) => {
226
+ stats.signals = (stats.signals || 0) + 1;
227
+ const side = data.side === 'long' ? 'BUY' : 'SELL';
228
+ ui.addLog('signal', `${side} @ ${data.entry?.toFixed(2) || 'MKT'}`);
229
+ });
230
+
231
+ hqx.on('trade', (data) => {
232
+ stats.trades++;
233
+ stats.pnl += data.pnl || 0;
234
+ if (data.pnl >= 0) {
235
+ stats.wins++;
236
+ ui.addLog('trade', `+$${data.pnl.toFixed(2)}`);
237
+ } else {
238
+ stats.losses++;
239
+ ui.addLog('loss', `-$${Math.abs(data.pnl).toFixed(2)}`);
240
+ }
241
+
242
+ // Check targets
243
+ if (stats.pnl >= dailyTarget) {
244
+ stopReason = 'target';
245
+ running = false;
246
+ ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
247
+ hqx.stopAlgo();
248
+ } else if (stats.pnl <= -maxRisk) {
249
+ stopReason = 'risk';
250
+ running = false;
251
+ ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
252
+ hqx.stopAlgo();
253
+ }
254
+ });
255
+
256
+ hqx.on('error', (data) => {
257
+ ui.addLog('error', data.message || 'Error');
258
+ });
259
+
260
+ hqx.on('disconnected', () => {
261
+ stats.connected = false;
262
+ ui.addLog('warning', 'Server disconnected');
263
+ });
264
+
265
+ // Start algo on server
266
+ if (stats.connected) {
267
+ ui.addLog('info', 'Starting algo...');
268
+
269
+ // Get credentials if Rithmic
270
+ let rithmicCreds = null;
271
+ if (service.getRithmicCredentials) {
272
+ rithmicCreds = service.getRithmicCredentials();
273
+ }
274
+
275
+ hqx.startAlgo({
276
+ accountId: account.accountId,
277
+ contractId: contract.id || contract.contractId,
278
+ symbol: contract.symbol || contract.name,
279
+ contracts,
280
+ dailyTarget,
281
+ maxRisk,
282
+ propfirm: account.propfirm || 'topstep',
283
+ propfirmToken: service.getToken ? service.getToken() : null,
284
+ rithmicCredentials: rithmicCreds
285
+ });
286
+ }
287
+
288
+ // UI refresh interval
289
+ const refreshInterval = setInterval(() => {
290
+ if (running) ui.render(stats);
291
+ }, 250);
292
+
293
+ // Keyboard handler
294
+ const setupKeyHandler = () => {
295
+ if (!process.stdin.isTTY) return;
296
+
297
+ readline.emitKeypressEvents(process.stdin);
298
+ process.stdin.setRawMode(true);
299
+ process.stdin.resume();
300
+
301
+ const onKey = (str, key) => {
302
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
303
+ running = false;
304
+ stopReason = 'manual';
305
+ }
306
+ };
307
+
308
+ process.stdin.on('keypress', onKey);
309
+ return () => {
310
+ process.stdin.removeListener('keypress', onKey);
311
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
312
+ };
313
+ };
314
+
315
+ const cleanupKeys = setupKeyHandler();
316
+
317
+ // Wait for stop
318
+ await new Promise(resolve => {
319
+ const check = setInterval(() => {
320
+ if (!running) {
321
+ clearInterval(check);
322
+ resolve();
323
+ }
324
+ }, 100);
325
+ });
326
+
327
+ // Cleanup
328
+ clearInterval(refreshInterval);
329
+ if (cleanupKeys) cleanupKeys();
330
+
331
+ if (stats.connected) {
332
+ hqx.stopAlgo();
333
+ hqx.disconnect();
334
+ }
335
+
336
+ ui.cleanup();
337
+
338
+ // Final summary
339
+ console.clear();
340
+ console.log();
341
+ console.log(chalk.cyan(' === Session Summary ==='));
342
+ console.log();
343
+ console.log(chalk.white(` Stop Reason: ${stopReason || 'unknown'}`));
344
+ console.log(chalk.white(` Trades: ${stats.trades} (W: ${stats.wins} / L: ${stats.losses})`));
345
+ const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
346
+ console.log(pnlColor(` P&L: ${stats.pnl >= 0 ? '+' : ''}$${stats.pnl.toFixed(2)}`));
347
+ console.log();
348
+
349
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
350
+ };
351
+
352
+ module.exports = { oneAccountMenu };
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Algo Trading - Shared UI Components
3
+ * Lightweight UI renderer for algo trading modes
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+
8
+ // Box drawing characters
9
+ const BOX = {
10
+ TOP: '\u2554', BOT: '\u255A', V: '\u2551', H: '\u2550',
11
+ TR: '\u2557', BR: '\u255D', ML: '\u2560', MR: '\u2563',
12
+ TM: '\u2564', BM: '\u2567', MM: '\u256A', VS: '\u2502'
13
+ };
14
+
15
+ // Spinner characters
16
+ const SPINNER = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
17
+
18
+ // Log type colors
19
+ const LOG_COLORS = {
20
+ info: chalk.cyan,
21
+ success: chalk.green,
22
+ signal: chalk.yellow.bold,
23
+ trade: chalk.green.bold,
24
+ loss: chalk.magenta.bold,
25
+ error: chalk.red,
26
+ warning: chalk.yellow
27
+ };
28
+
29
+ // Log type icons (fixed 10 chars for alignment)
30
+ const LOG_ICONS = {
31
+ signal: '[SIGNAL] ',
32
+ trade: '[TRADE] ',
33
+ order: '[ORDER] ',
34
+ position: '[POSITION]',
35
+ error: '[ERROR] ',
36
+ warning: '[WARNING] ',
37
+ success: '[OK] ',
38
+ analysis: '[ANALYSIS]',
39
+ info: '[INFO] '
40
+ };
41
+
42
+ /**
43
+ * Strip ANSI codes from string
44
+ */
45
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
46
+
47
+ /**
48
+ * Center text in width
49
+ */
50
+ const center = (text, width) => {
51
+ const pad = Math.floor((width - text.length) / 2);
52
+ return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
53
+ };
54
+
55
+ /**
56
+ * Fit text to exact width (truncate or pad)
57
+ */
58
+ const fitToWidth = (text, width) => {
59
+ const plain = stripAnsi(text);
60
+ if (plain.length > width) {
61
+ let count = 0, cut = 0;
62
+ for (let i = 0; i < text.length && count < width - 3; i++) {
63
+ if (text[i] === '\x1B') { while (i < text.length && text[i] !== 'm') i++; }
64
+ else { count++; cut = i + 1; }
65
+ }
66
+ return text.substring(0, cut) + '...';
67
+ }
68
+ return text + ' '.repeat(width - plain.length);
69
+ };
70
+
71
+ /**
72
+ * Build a labeled cell for grid
73
+ */
74
+ const buildCell = (label, value, color, width) => {
75
+ const text = ` ${label}: ${color(value)}`;
76
+ const plain = ` ${label}: ${value}`;
77
+ return { text, plain, padded: text + ' '.repeat(Math.max(0, width - plain.length)) };
78
+ };
79
+
80
+ /**
81
+ * Create AlgoUI renderer
82
+ */
83
+ class AlgoUI {
84
+ constructor(config) {
85
+ this.config = config;
86
+ this.W = 96; // Fixed width
87
+ this.logs = [];
88
+ this.maxLogs = 50;
89
+ this.spinnerFrame = 0;
90
+ this.firstDraw = true;
91
+ this.isDrawing = false;
92
+ this.buffer = '';
93
+ }
94
+
95
+ addLog(type, message) {
96
+ const timestamp = new Date().toLocaleTimeString();
97
+ this.logs.push({ timestamp, type, message });
98
+ if (this.logs.length > this.maxLogs) this.logs.shift();
99
+ }
100
+
101
+ _line(text) {
102
+ this.buffer += text + '\x1B[K\n';
103
+ }
104
+
105
+ _drawHeader() {
106
+ const { W } = this;
107
+ const version = require('../../../package.json').version;
108
+
109
+ // Top border
110
+ this._line(chalk.cyan(BOX.TOP + BOX.H.repeat(W) + BOX.TR));
111
+
112
+ // Logo (compact)
113
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(BOX.V));
114
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(BOX.V));
115
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(BOX.V));
116
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(BOX.V));
117
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(BOX.V));
118
+ this._line(chalk.cyan(BOX.V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(BOX.V));
119
+
120
+ // Separator + title
121
+ this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
122
+ this._line(chalk.cyan(BOX.V) + chalk.white(center(`Prop Futures Algo Trading v${version}`, W)) + chalk.cyan(BOX.V));
123
+ this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
124
+ this._line(chalk.cyan(BOX.V) + chalk.yellow(center(this.config.subtitle || 'HQX Ultra-Scalping', W)) + chalk.cyan(BOX.V));
125
+ }
126
+
127
+ _drawStats(stats) {
128
+ const { W } = this;
129
+ const colL = 48, colR = 47;
130
+ const pad = (len) => ' '.repeat(Math.max(0, len));
131
+
132
+ const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
133
+ const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
134
+ const latencyColor = stats.latency < 100 ? chalk.green : (stats.latency < 300 ? chalk.yellow : chalk.red);
135
+ const serverColor = stats.connected ? chalk.green : chalk.red;
136
+
137
+ // Grid borders
138
+ const GT = BOX.ML + BOX.H.repeat(colL) + BOX.TM + BOX.H.repeat(colR) + BOX.MR;
139
+ const GM = BOX.ML + BOX.H.repeat(colL) + BOX.MM + BOX.H.repeat(colR) + BOX.MR;
140
+ const GB = BOX.ML + BOX.H.repeat(colL) + BOX.BM + BOX.H.repeat(colR) + BOX.MR;
141
+
142
+ // Row builders
143
+ const row = (c1, c2) => {
144
+ this._line(chalk.cyan(BOX.V) + c1 + chalk.cyan(BOX.VS) + c2 + chalk.cyan(BOX.V));
145
+ };
146
+
147
+ this._line(chalk.cyan(GT));
148
+
149
+ // Row 1: Account | Symbol
150
+ const r1c1 = buildCell('Account', stats.accountName || 'N/A', chalk.cyan, colL);
151
+ const r1c2t = ` Symbol: ${chalk.yellow(stats.symbol || 'N/A')} Qty: ${chalk.cyan(stats.contracts || 1)}`;
152
+ const r1c2p = ` Symbol: ${stats.symbol || 'N/A'} Qty: ${stats.contracts || 1}`;
153
+ row(r1c1.padded, r1c2t + pad(colR - r1c2p.length));
154
+
155
+ this._line(chalk.cyan(GM));
156
+
157
+ // Row 2: Target | Risk
158
+ const r2c1 = buildCell('Target', '$' + (stats.target || 0).toFixed(2), chalk.green, colL);
159
+ const r2c2 = buildCell('Risk', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR);
160
+ row(r2c1.padded, r2c2.padded);
161
+
162
+ this._line(chalk.cyan(GM));
163
+
164
+ // Row 3: P&L | Server
165
+ const r3c1 = buildCell('P&L', pnlStr, pnlColor, colL);
166
+ const r3c2 = buildCell('Server', stats.connected ? 'ON' : 'OFF', serverColor, colR);
167
+ row(r3c1.padded, r3c2.padded);
168
+
169
+ this._line(chalk.cyan(GM));
170
+
171
+ // Row 4: Trades | Latency
172
+ const r4c1t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
173
+ const r4c1p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
174
+ const r4c2 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colR);
175
+ row(r4c1t + pad(colL - r4c1p.length), r4c2.padded);
176
+
177
+ this._line(chalk.cyan(GB));
178
+ }
179
+
180
+ _drawLogs() {
181
+ const { W, logs, maxLogs } = this;
182
+
183
+ // Activity header
184
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
185
+ const spinner = SPINNER[this.spinnerFrame];
186
+ const dateStr = new Date().toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
187
+ const left = ` Activity Log ${chalk.yellow(spinner)}`;
188
+ const right = 'Press X to stop ';
189
+ const mid = `- ${dateStr} -`;
190
+ const space = W - stripAnsi(left).length - right.length;
191
+ const midPad = Math.floor((space - mid.length) / 2);
192
+
193
+ this._line(chalk.cyan(BOX.V) + chalk.white(left) + ' '.repeat(midPad) + chalk.cyan(mid) + ' '.repeat(space - midPad - mid.length) + chalk.yellow(right) + chalk.cyan(BOX.V));
194
+ this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
195
+
196
+ // Logs (newest first)
197
+ const visible = [...logs].reverse().slice(0, maxLogs);
198
+
199
+ if (visible.length === 0) {
200
+ this._line(chalk.cyan(BOX.V) + chalk.gray(fitToWidth(' Waiting for activity...', W)) + chalk.cyan(BOX.V));
201
+ for (let i = 0; i < maxLogs - 1; i++) {
202
+ this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
203
+ }
204
+ } else {
205
+ visible.forEach(log => {
206
+ const color = LOG_COLORS[log.type] || chalk.white;
207
+ const icon = LOG_ICONS[log.type] || LOG_ICONS.info;
208
+ const line = ` [${log.timestamp}] ${icon} ${log.message}`;
209
+ this._line(chalk.cyan(BOX.V) + color(fitToWidth(line, W)) + chalk.cyan(BOX.V));
210
+ });
211
+ for (let i = visible.length; i < maxLogs; i++) {
212
+ this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
213
+ }
214
+ }
215
+
216
+ // Bottom border
217
+ this._line(chalk.cyan(BOX.BOT + BOX.H.repeat(W) + BOX.BR));
218
+ }
219
+
220
+ render(stats) {
221
+ if (this.isDrawing) return;
222
+ this.isDrawing = true;
223
+
224
+ this.buffer = '';
225
+
226
+ if (this.firstDraw) {
227
+ this.buffer += '\x1B[?1049h\x1B[?25l\x1B[2J';
228
+ this.firstDraw = false;
229
+ }
230
+
231
+ this.buffer += '\x1B[H';
232
+ this._line('');
233
+ this._drawHeader();
234
+ this._drawStats(stats);
235
+ this._drawLogs();
236
+
237
+ process.stdout.write(this.buffer);
238
+ this.isDrawing = false;
239
+ }
240
+
241
+ cleanup() {
242
+ process.stdout.write('\x1B[?1049l\x1B[?25h');
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Check market hours
248
+ */
249
+ const checkMarketStatus = () => {
250
+ const now = new Date();
251
+ const utcDay = now.getUTCDay();
252
+ const utcHour = now.getUTCHours();
253
+ const isDST = now.getTimezoneOffset() < Math.max(
254
+ new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
255
+ new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
256
+ );
257
+ const ctOffset = isDST ? 5 : 6;
258
+ const ctHour = (utcHour - ctOffset + 24) % 24;
259
+ const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
260
+
261
+ if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
262
+ if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
263
+ if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
264
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
265
+ return { isOpen: true, message: 'Market OPEN' };
266
+ };
267
+
268
+ module.exports = { AlgoUI, checkMarketStatus, LOG_COLORS, LOG_ICONS, stripAnsi, center, fitToWidth };