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.
- package/package.json +1 -1
- package/src/app.js +48 -5
- package/src/pages/algo/copy-trading.js +404 -0
- package/src/pages/algo/index.js +51 -0
- package/src/pages/algo/one-account.js +352 -0
- package/src/pages/algo/ui.js +268 -0
- package/src/pages/algo.js +3 -2277
- package/src/services/hqx-server.js +27 -0
|
@@ -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 };
|