hedgequantx 1.2.145 → 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/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
package/src/pages/algo.js
CHANGED
|
@@ -1,2282 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Algo Trading Page
|
|
2
|
+
* Algo Trading Page - Lightweight Router
|
|
3
|
+
* Delegates to modular components
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const ora = require('ora');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
|
-
const readline = require('readline');
|
|
9
|
-
|
|
10
|
-
const { connections } = require('../services');
|
|
11
|
-
const { HQXServerService } = require('../services/hqx-server');
|
|
12
|
-
const { FUTURES_SYMBOLS } = require('../config');
|
|
13
|
-
const { getDevice, getSeparator } = require('../ui');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Algo Trading Menu
|
|
17
|
-
*/
|
|
18
|
-
const algoTradingMenu = async (service) => {
|
|
19
|
-
const device = getDevice();
|
|
20
|
-
console.log();
|
|
21
|
-
console.log(chalk.gray(getSeparator()));
|
|
22
|
-
console.log(chalk.magenta.bold(' Algo-Trading'));
|
|
23
|
-
console.log(chalk.gray(getSeparator()));
|
|
24
|
-
console.log();
|
|
25
|
-
|
|
26
|
-
const { action } = await inquirer.prompt([
|
|
27
|
-
{
|
|
28
|
-
type: 'list',
|
|
29
|
-
name: 'action',
|
|
30
|
-
message: chalk.white.bold('Select Mode:'),
|
|
31
|
-
choices: [
|
|
32
|
-
{ name: chalk.cyan('One Account'), value: 'one_account' },
|
|
33
|
-
{ name: chalk.green('Copy Trading'), value: 'copy_trading' },
|
|
34
|
-
new inquirer.Separator(),
|
|
35
|
-
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
36
|
-
],
|
|
37
|
-
pageSize: 10,
|
|
38
|
-
loop: false
|
|
39
|
-
}
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
switch (action) {
|
|
43
|
-
case 'one_account':
|
|
44
|
-
await oneAccountMenu(service);
|
|
45
|
-
break;
|
|
46
|
-
case 'copy_trading':
|
|
47
|
-
await copyTradingMenu();
|
|
48
|
-
break;
|
|
49
|
-
case 'back':
|
|
50
|
-
return 'back';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return action;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* One Account Menu - Select active account
|
|
58
|
-
*/
|
|
59
|
-
const oneAccountMenu = async (service) => {
|
|
60
|
-
const spinner = ora('Fetching active accounts...').start();
|
|
61
|
-
|
|
62
|
-
const result = await service.getTradingAccounts();
|
|
63
|
-
|
|
64
|
-
if (!result.success || !result.accounts || result.accounts.length === 0) {
|
|
65
|
-
spinner.fail('No accounts found');
|
|
66
|
-
console.log(chalk.yellow(' You need at least one trading account.'));
|
|
67
|
-
console.log();
|
|
68
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Filter only active accounts (status === 0)
|
|
73
|
-
const activeAccounts = result.accounts.filter(acc => acc.status === 0);
|
|
74
|
-
|
|
75
|
-
if (activeAccounts.length === 0) {
|
|
76
|
-
spinner.fail('No active accounts found');
|
|
77
|
-
console.log(chalk.yellow(' You need at least one active trading account (status: Active).'));
|
|
78
|
-
console.log();
|
|
79
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
|
|
84
|
-
console.log();
|
|
85
|
-
|
|
86
|
-
const accountChoices = activeAccounts.map(account => ({
|
|
87
|
-
name: chalk.cyan(`${account.accountName || account.name || 'Account #' + account.accountId} - Balance: $${account.balance.toLocaleString()}`),
|
|
88
|
-
value: account
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
accountChoices.push(new inquirer.Separator());
|
|
92
|
-
accountChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
|
|
93
|
-
|
|
94
|
-
const { selectedAccount } = await inquirer.prompt([
|
|
95
|
-
{
|
|
96
|
-
type: 'list',
|
|
97
|
-
name: 'selectedAccount',
|
|
98
|
-
message: chalk.white.bold('Select Account:'),
|
|
99
|
-
choices: accountChoices,
|
|
100
|
-
pageSize: 15,
|
|
101
|
-
loop: false
|
|
102
|
-
}
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
if (selectedAccount === 'back') {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Check market status
|
|
110
|
-
console.log();
|
|
111
|
-
const marketSpinner = ora('Checking market status...').start();
|
|
112
|
-
|
|
113
|
-
const marketHours = service.checkMarketHours();
|
|
114
|
-
const marketStatus = await service.getMarketStatus(selectedAccount.accountId);
|
|
115
|
-
|
|
116
|
-
if (!marketHours.isOpen) {
|
|
117
|
-
marketSpinner.fail('Market is CLOSED');
|
|
118
|
-
console.log();
|
|
119
|
-
console.log(chalk.red.bold(' [X] ' + marketHours.message));
|
|
120
|
-
console.log();
|
|
121
|
-
console.log(chalk.gray(' Futures markets (CME) trading hours:'));
|
|
122
|
-
console.log(chalk.gray(' Sunday 5:00 PM CT - Friday 4:00 PM CT'));
|
|
123
|
-
console.log(chalk.gray(' Daily maintenance: 4:00 PM - 5:00 PM CT'));
|
|
124
|
-
console.log();
|
|
125
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (marketStatus.success && !marketStatus.isOpen) {
|
|
130
|
-
marketSpinner.fail('Cannot trade on this account');
|
|
131
|
-
console.log();
|
|
132
|
-
console.log(chalk.red.bold(' [X] ' + marketStatus.message));
|
|
133
|
-
console.log();
|
|
134
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
marketSpinner.succeed('Market is OPEN - Ready to trade!');
|
|
139
|
-
|
|
140
|
-
await selectSymbolMenu(service, selectedAccount);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Symbol Selection Menu
|
|
145
|
-
*/
|
|
146
|
-
const selectSymbolMenu = async (service, account) => {
|
|
147
|
-
const device = getDevice();
|
|
148
|
-
const accountName = account.accountName || account.name || 'Account #' + account.accountId;
|
|
149
|
-
const propfirm = account.propfirm || 'projectx';
|
|
150
|
-
|
|
151
|
-
console.log();
|
|
152
|
-
console.log(chalk.gray(getSeparator()));
|
|
153
|
-
console.log(chalk.cyan.bold(` Account: ${accountName}`));
|
|
154
|
-
console.log(chalk.gray(getSeparator()));
|
|
155
|
-
console.log();
|
|
156
|
-
|
|
157
|
-
// Fetch available symbols from API
|
|
158
|
-
const spinner = ora('Loading available symbols...').start();
|
|
159
|
-
|
|
160
|
-
let availableSymbols = [];
|
|
161
|
-
|
|
162
|
-
// Search for common symbols to get available contracts (including micros)
|
|
163
|
-
// Use various search terms to find all contracts
|
|
164
|
-
const commonSearches = [
|
|
165
|
-
'NQ', 'ES', 'YM', 'RTY', // E-mini indices
|
|
166
|
-
'Micro', 'MNQ', 'MES', 'MYM', 'M2K', // Micro indices (try multiple search terms)
|
|
167
|
-
'CL', 'MCL', 'QM', // Crude Oil
|
|
168
|
-
'GC', 'MGC', // Gold
|
|
169
|
-
'SI', 'SIL', // Silver
|
|
170
|
-
'6E', 'M6E', '6B', '6J', '6A', '6C', // Currencies
|
|
171
|
-
'ZB', 'ZN', 'ZF', 'ZT', // Treasuries
|
|
172
|
-
'NG', 'QG', // Natural Gas
|
|
173
|
-
'HG', 'PL' // Copper, Platinum
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const seenIds = new Set();
|
|
178
|
-
|
|
179
|
-
for (const search of commonSearches) {
|
|
180
|
-
const result = await service.searchContracts(search, false);
|
|
181
|
-
if (result.success && result.contracts && result.contracts.length > 0) {
|
|
182
|
-
for (const contract of result.contracts) {
|
|
183
|
-
// Skip if already added (by contract ID)
|
|
184
|
-
const contractId = contract.id || '';
|
|
185
|
-
if (!contractId || seenIds.has(contractId)) continue;
|
|
186
|
-
seenIds.add(contractId);
|
|
187
|
-
|
|
188
|
-
// Add the raw contract data from API
|
|
189
|
-
availableSymbols.push(contract);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch (e) {
|
|
194
|
-
spinner.fail('Failed to load symbols from API: ' + e.message);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Only use REAL data from API - no mock/static data
|
|
199
|
-
if (availableSymbols.length === 0) {
|
|
200
|
-
spinner.fail('No contracts available from API');
|
|
201
|
-
console.log(chalk.red(' Please check your connection and try again'));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
spinner.succeed(`Found ${availableSymbols.length} available contracts`);
|
|
206
|
-
|
|
207
|
-
console.log();
|
|
208
|
-
|
|
209
|
-
// Format symbols for display - show ALL contracts from API (REAL DATA ONLY)
|
|
210
|
-
const symbolChoices = [];
|
|
211
|
-
|
|
212
|
-
for (const contract of availableSymbols) {
|
|
213
|
-
// Get symbol code and description directly from API
|
|
214
|
-
const symbolCode = contract.name || contract.id || 'Unknown';
|
|
215
|
-
const description = contract.description || symbolCode;
|
|
216
|
-
|
|
217
|
-
// Format: "NQH6 E-mini NASDAQ-100: March 2026"
|
|
218
|
-
symbolChoices.push({
|
|
219
|
-
name: chalk.yellow(symbolCode.padEnd(12)) + chalk.white(description),
|
|
220
|
-
value: contract
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Sort by category: E-mini indices first, then Micro E-mini, then others
|
|
225
|
-
const getSymbolPriority = (contract) => {
|
|
226
|
-
const name = (contract.name || contract.symbol || '').toUpperCase();
|
|
227
|
-
const desc = (contract.description || '').toLowerCase();
|
|
228
|
-
|
|
229
|
-
// E-mini indices (NQ, ES, YM, RTY) - highest priority
|
|
230
|
-
if (name.match(/^(NQ|ES|YM|RTY)[A-Z]\d/) && !name.startsWith('M')) {
|
|
231
|
-
if (name.startsWith('NQ')) return 10;
|
|
232
|
-
if (name.startsWith('ES')) return 11;
|
|
233
|
-
if (name.startsWith('YM')) return 12;
|
|
234
|
-
if (name.startsWith('RTY')) return 13;
|
|
235
|
-
return 15;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Micro E-mini indices (MNQ, MES, MYM, M2K)
|
|
239
|
-
if (name.match(/^(MNQ|MES|MYM|M2K)/)) {
|
|
240
|
-
if (name.startsWith('MNQ')) return 20;
|
|
241
|
-
if (name.startsWith('MES')) return 21;
|
|
242
|
-
if (name.startsWith('MYM')) return 22;
|
|
243
|
-
if (name.startsWith('M2K')) return 23;
|
|
244
|
-
return 25;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Energy (CL, MCL, NG)
|
|
248
|
-
if (name.match(/^(CL|MCL|NG|QG)/)) return 30;
|
|
249
|
-
|
|
250
|
-
// Metals (GC, MGC, SI)
|
|
251
|
-
if (name.match(/^(GC|MGC|SI|HG|PL)/)) return 40;
|
|
252
|
-
|
|
253
|
-
// Currencies (6E, 6B, etc)
|
|
254
|
-
if (name.match(/^(6E|6B|6J|6A|6C|M6E)/)) return 50;
|
|
255
|
-
|
|
256
|
-
// Treasuries (ZB, ZN, ZF, ZT)
|
|
257
|
-
if (name.match(/^(ZB|ZN|ZF|ZT)/)) return 60;
|
|
258
|
-
|
|
259
|
-
// Everything else
|
|
260
|
-
return 100;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
symbolChoices.sort((a, b) => {
|
|
264
|
-
const priorityA = getSymbolPriority(a.value);
|
|
265
|
-
const priorityB = getSymbolPriority(b.value);
|
|
266
|
-
|
|
267
|
-
if (priorityA !== priorityB) {
|
|
268
|
-
return priorityA - priorityB;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Same priority - sort alphabetically
|
|
272
|
-
const aCode = a.value.name || a.value.symbol || '';
|
|
273
|
-
const bCode = b.value.name || b.value.symbol || '';
|
|
274
|
-
return aCode.localeCompare(bCode);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
symbolChoices.push(new inquirer.Separator());
|
|
278
|
-
symbolChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
|
|
279
|
-
|
|
280
|
-
const { selectedSymbol } = await inquirer.prompt([
|
|
281
|
-
{
|
|
282
|
-
type: 'list',
|
|
283
|
-
name: 'selectedSymbol',
|
|
284
|
-
message: chalk.white.bold('Select Symbol:'),
|
|
285
|
-
choices: symbolChoices,
|
|
286
|
-
pageSize: 50,
|
|
287
|
-
loop: false
|
|
288
|
-
}
|
|
289
|
-
]);
|
|
290
|
-
|
|
291
|
-
if (selectedSymbol === 'back') {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Use the selected contract directly (already fetched from API)
|
|
296
|
-
let contract = selectedSymbol;
|
|
297
|
-
|
|
298
|
-
console.log();
|
|
299
|
-
console.log(chalk.green(` [OK] Selected: ${contract.name || contract.symbol}`));
|
|
300
|
-
if (contract.tickSize && contract.tickValue) {
|
|
301
|
-
console.log(chalk.gray(` Tick Size: ${contract.tickSize} | Tick Value: $${contract.tickValue}`));
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// If contract doesn't have full details, search again
|
|
305
|
-
if (!contract.id || !contract.tickSize) {
|
|
306
|
-
const searchSpinner = ora(`Getting contract details...`).start();
|
|
307
|
-
const contractResult = await service.searchContracts(contract.symbol || contract.searchText, false);
|
|
308
|
-
|
|
309
|
-
if (contractResult.success && contractResult.contracts && contractResult.contracts.length > 0) {
|
|
310
|
-
const found = contractResult.contracts.find(c => c.activeContract) || contractResult.contracts[0];
|
|
311
|
-
contract = { ...contract, ...found };
|
|
312
|
-
searchSpinner.succeed(`Contract: ${contract.name || contract.symbol}`);
|
|
313
|
-
} else {
|
|
314
|
-
searchSpinner.warn('Using basic contract info');
|
|
315
|
-
contract = {
|
|
316
|
-
id: contract.symbol || contract.id,
|
|
317
|
-
name: contract.name || contract.symbol,
|
|
318
|
-
symbol: contract.symbol || contract.id
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
console.log();
|
|
324
|
-
|
|
325
|
-
// Number of contracts
|
|
326
|
-
const { contracts } = await inquirer.prompt([
|
|
327
|
-
{
|
|
328
|
-
type: 'input',
|
|
329
|
-
name: 'contracts',
|
|
330
|
-
message: chalk.white.bold('Number of Contracts:'),
|
|
331
|
-
default: '1',
|
|
332
|
-
validate: (input) => {
|
|
333
|
-
const num = parseInt(input);
|
|
334
|
-
if (isNaN(num) || num <= 0 || num > 100) {
|
|
335
|
-
return 'Please enter a valid number between 1 and 100';
|
|
336
|
-
}
|
|
337
|
-
return true;
|
|
338
|
-
},
|
|
339
|
-
filter: (input) => parseInt(input)
|
|
340
|
-
}
|
|
341
|
-
]);
|
|
342
|
-
|
|
343
|
-
// Risk Management
|
|
344
|
-
console.log();
|
|
345
|
-
console.log(chalk.cyan.bold(' Risk Management'));
|
|
346
|
-
console.log(chalk.gray(' Set your daily target and maximum risk to auto-stop the algo.'));
|
|
347
|
-
console.log();
|
|
348
|
-
|
|
349
|
-
const { dailyTarget } = await inquirer.prompt([
|
|
350
|
-
{
|
|
351
|
-
type: 'input',
|
|
352
|
-
name: 'dailyTarget',
|
|
353
|
-
message: chalk.white.bold('Daily Target ($):'),
|
|
354
|
-
default: '500',
|
|
355
|
-
validate: (input) => {
|
|
356
|
-
const num = parseFloat(input);
|
|
357
|
-
if (isNaN(num) || num <= 0) {
|
|
358
|
-
return 'Please enter a valid amount greater than 0';
|
|
359
|
-
}
|
|
360
|
-
return true;
|
|
361
|
-
},
|
|
362
|
-
filter: (input) => parseFloat(input)
|
|
363
|
-
}
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
const { maxRisk } = await inquirer.prompt([
|
|
367
|
-
{
|
|
368
|
-
type: 'input',
|
|
369
|
-
name: 'maxRisk',
|
|
370
|
-
message: chalk.white.bold('Max Risk ($):'),
|
|
371
|
-
default: '200',
|
|
372
|
-
validate: (input) => {
|
|
373
|
-
const num = parseFloat(input);
|
|
374
|
-
if (isNaN(num) || num <= 0) {
|
|
375
|
-
return 'Please enter a valid amount greater than 0';
|
|
376
|
-
}
|
|
377
|
-
return true;
|
|
378
|
-
},
|
|
379
|
-
filter: (input) => parseFloat(input)
|
|
380
|
-
}
|
|
381
|
-
]);
|
|
382
|
-
|
|
383
|
-
// Privacy option - show or hide account name
|
|
384
|
-
console.log();
|
|
385
|
-
const { showAccountName } = await inquirer.prompt([
|
|
386
|
-
{
|
|
387
|
-
type: 'list',
|
|
388
|
-
name: 'showAccountName',
|
|
389
|
-
message: chalk.white.bold('Account name visibility:'),
|
|
390
|
-
choices: [
|
|
391
|
-
{ name: chalk.cyan('[>] Show account name'), value: true },
|
|
392
|
-
{ name: chalk.gray('[.] Hide account name'), value: false }
|
|
393
|
-
],
|
|
394
|
-
loop: false
|
|
395
|
-
}
|
|
396
|
-
]);
|
|
397
|
-
|
|
398
|
-
const displayAccountName = showAccountName ? accountName : 'HQX *****';
|
|
399
|
-
|
|
400
|
-
// Confirmation
|
|
401
|
-
console.log();
|
|
402
|
-
console.log(chalk.gray(getSeparator()));
|
|
403
|
-
console.log(chalk.white.bold(' Algo Configuration:'));
|
|
404
|
-
console.log(chalk.gray(getSeparator()));
|
|
405
|
-
console.log(chalk.white(` Account: ${chalk.cyan(displayAccountName)}`));
|
|
406
|
-
console.log(chalk.white(` Symbol: ${chalk.cyan(contract.name || selectedSymbol.value)}`));
|
|
407
|
-
console.log(chalk.white(` Contracts: ${chalk.cyan(contracts)}`));
|
|
408
|
-
console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
|
|
409
|
-
console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
|
|
410
|
-
console.log(chalk.gray(getSeparator()));
|
|
411
|
-
console.log();
|
|
412
|
-
|
|
413
|
-
const { launch } = await inquirer.prompt([
|
|
414
|
-
{
|
|
415
|
-
type: 'list',
|
|
416
|
-
name: 'launch',
|
|
417
|
-
message: chalk.white.bold('Ready to launch?'),
|
|
418
|
-
choices: [
|
|
419
|
-
{ name: chalk.green.bold('[>] Launch Algo'), value: 'launch' },
|
|
420
|
-
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
421
|
-
],
|
|
422
|
-
loop: false
|
|
423
|
-
}
|
|
424
|
-
]);
|
|
425
|
-
|
|
426
|
-
if (launch === 'back') {
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
await launchAlgo(service, account, contract, contracts, dailyTarget, maxRisk, showAccountName);
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Launch Algo with HQX Server Connection
|
|
435
|
-
*/
|
|
436
|
-
const launchAlgo = async (service, account, contract, numContracts, dailyTarget, maxRisk, showAccountName = true) => {
|
|
437
|
-
const realAccountName = account.accountName || account.name || 'Account #' + account.accountId;
|
|
438
|
-
const accountName = showAccountName ? realAccountName : 'HQX *****';
|
|
439
|
-
const symbolName = contract.name || contract.symbol || contract.id;
|
|
440
|
-
const symbol = contract.symbol || contract.id;
|
|
441
|
-
|
|
442
|
-
console.log();
|
|
443
|
-
console.log(chalk.green.bold(' [>] Launching HQX Algo...'));
|
|
444
|
-
console.log();
|
|
445
|
-
|
|
446
|
-
// Initialize HQX Server connection
|
|
447
|
-
const hqxServer = new HQXServerService();
|
|
448
|
-
let hqxConnected = false;
|
|
449
|
-
let algoRunning = false;
|
|
450
|
-
let stopReason = null;
|
|
451
|
-
let latency = 0;
|
|
452
|
-
let spinnerFrame = 0;
|
|
453
|
-
const spinnerChars = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
|
|
454
|
-
const sessionStartTime = Date.now();
|
|
455
|
-
|
|
456
|
-
// Stats
|
|
457
|
-
let stats = {
|
|
458
|
-
trades: 0,
|
|
459
|
-
wins: 0,
|
|
460
|
-
losses: 0,
|
|
461
|
-
pnl: 0,
|
|
462
|
-
signals: 0,
|
|
463
|
-
winRate: '0.0'
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// Logs buffer - newest first display, show many logs
|
|
467
|
-
const logs = [];
|
|
468
|
-
const MAX_LOGS = 50;
|
|
469
|
-
|
|
470
|
-
// Log colors
|
|
471
|
-
const typeColors = {
|
|
472
|
-
info: chalk.cyan,
|
|
473
|
-
success: chalk.green,
|
|
474
|
-
signal: chalk.yellow.bold,
|
|
475
|
-
trade: chalk.green.bold,
|
|
476
|
-
loss: chalk.magenta.bold,
|
|
477
|
-
error: chalk.red,
|
|
478
|
-
warning: chalk.yellow
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const getIcon = (type) => {
|
|
482
|
-
// Fixed width tags (10 chars) for alignment
|
|
483
|
-
switch(type) {
|
|
484
|
-
case 'signal': return '[SIGNAL] ';
|
|
485
|
-
case 'trade': return '[TRADE] ';
|
|
486
|
-
case 'order': return '[ORDER] ';
|
|
487
|
-
case 'position': return '[POSITION]';
|
|
488
|
-
case 'error': return '[ERROR] ';
|
|
489
|
-
case 'warning': return '[WARNING] ';
|
|
490
|
-
case 'success': return '[OK] ';
|
|
491
|
-
case 'analysis': return '[ANALYSIS]';
|
|
492
|
-
default: return '[INFO] ';
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
// Add log (oldest first, newest at bottom)
|
|
497
|
-
const addLog = (type, message) => {
|
|
498
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
499
|
-
logs.push({ timestamp, type, message }); // Add at end
|
|
500
|
-
if (logs.length > MAX_LOGS) logs.shift(); // Remove oldest from top
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
// Print log - just add to buffer, spinner interval will refresh display
|
|
504
|
-
// This prevents display flicker from multiple concurrent displayUI() calls
|
|
505
|
-
const printLog = (type, message) => {
|
|
506
|
-
addLog(type, message);
|
|
507
|
-
// Don't call displayUI() here - let the spinner interval handle it
|
|
508
|
-
// This prevents flickering when logs arrive rapidly
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
// Check market hours
|
|
512
|
-
const checkMarketStatus = () => {
|
|
513
|
-
const now = new Date();
|
|
514
|
-
const utcDay = now.getUTCDay();
|
|
515
|
-
const utcHour = now.getUTCHours();
|
|
516
|
-
const isDST = (() => {
|
|
517
|
-
const jan = new Date(now.getFullYear(), 0, 1);
|
|
518
|
-
const jul = new Date(now.getFullYear(), 6, 1);
|
|
519
|
-
return now.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
520
|
-
})();
|
|
521
|
-
const ctOffset = isDST ? 5 : 6;
|
|
522
|
-
const ctHour = (utcHour - ctOffset + 24) % 24;
|
|
523
|
-
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
524
|
-
|
|
525
|
-
if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
|
|
526
|
-
if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
|
|
527
|
-
if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
|
|
528
|
-
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
|
|
529
|
-
return { isOpen: true, message: 'Market OPEN' };
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
// Display full UI with logs (newest first at top)
|
|
533
|
-
let firstDraw = true;
|
|
534
|
-
let isDrawing = false; // Mutex to prevent concurrent draws
|
|
535
|
-
|
|
536
|
-
// Build entire screen as a single string buffer to write atomically
|
|
537
|
-
let screenBuffer = '';
|
|
538
|
-
|
|
539
|
-
const bufferLine = (text) => {
|
|
540
|
-
screenBuffer += text + '\x1B[K\n'; // Add text + clear to EOL + newline
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
// Legacy function for compatibility
|
|
544
|
-
const printLine = bufferLine;
|
|
545
|
-
|
|
546
|
-
const displayUI = () => {
|
|
547
|
-
// Prevent concurrent draws
|
|
548
|
-
if (isDrawing) return;
|
|
549
|
-
isDrawing = true;
|
|
550
|
-
|
|
551
|
-
// Reset buffer
|
|
552
|
-
screenBuffer = '';
|
|
553
|
-
|
|
554
|
-
if (firstDraw) {
|
|
555
|
-
// Switch to alternate screen buffer - isolates our display
|
|
556
|
-
screenBuffer += '\x1B[?1049h'; // Enter alternate screen
|
|
557
|
-
screenBuffer += '\x1B[?25l'; // Hide cursor
|
|
558
|
-
screenBuffer += '\x1B[2J'; // Clear screen
|
|
559
|
-
firstDraw = false;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Move cursor to home position
|
|
563
|
-
screenBuffer += '\x1B[H';
|
|
564
|
-
|
|
565
|
-
// Stats
|
|
566
|
-
const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
|
|
567
|
-
const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
|
|
568
|
-
// Always show latency in ms format
|
|
569
|
-
const latencyMs = latency > 0 ? latency : 0;
|
|
570
|
-
const latencyStr = `${latencyMs}ms`;
|
|
571
|
-
const latencyColor = latencyMs < 100 ? chalk.green : (latencyMs < 300 ? chalk.yellow : chalk.red);
|
|
572
|
-
const serverStatus = hqxConnected ? 'ON' : 'OFF';
|
|
573
|
-
const serverColor = hqxConnected ? chalk.green : chalk.red;
|
|
574
|
-
|
|
575
|
-
// Current date
|
|
576
|
-
const now = new Date();
|
|
577
|
-
const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
|
|
578
|
-
|
|
579
|
-
// Get package version
|
|
580
|
-
const version = require('../../package.json').version;
|
|
581
|
-
|
|
582
|
-
// Fixed width = 96 inner chars
|
|
583
|
-
const W = 96;
|
|
584
|
-
const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
|
|
585
|
-
const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
|
|
586
|
-
const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
|
|
587
|
-
const V = '\u2551';
|
|
588
|
-
|
|
589
|
-
// Center text helper
|
|
590
|
-
const center = (text, width) => {
|
|
591
|
-
const pad = Math.floor((width - text.length) / 2);
|
|
592
|
-
return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
// Pad text to exact width
|
|
596
|
-
const padRight = (text, width) => {
|
|
597
|
-
if (text.length >= width) return text.substring(0, width);
|
|
598
|
-
return text + ' '.repeat(width - text.length);
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
printLine('');
|
|
602
|
-
printLine(chalk.cyan(TOP));
|
|
603
|
-
// Logo = 87 chars cyan + 9 chars yellow = 96 total
|
|
604
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
|
|
605
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
|
|
606
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
|
|
607
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
|
|
608
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
|
|
609
|
-
printLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
|
|
610
|
-
printLine(chalk.cyan(MID));
|
|
611
|
-
|
|
612
|
-
// Centered title
|
|
613
|
-
const title1 = `Prop Futures Algo Trading v${version}`;
|
|
614
|
-
printLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
|
|
615
|
-
printLine(chalk.cyan(MID));
|
|
616
|
-
|
|
617
|
-
// Centered subtitle
|
|
618
|
-
const title2 = 'HQX Ultra-Scalping Algorithm';
|
|
619
|
-
printLine(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
|
|
620
|
-
|
|
621
|
-
// Grid layout for metrics - 2 columns per row, 4 rows
|
|
622
|
-
// Row 1: Account | Symbol + Qty
|
|
623
|
-
// Row 2: Target | Risk
|
|
624
|
-
// Row 3: P&L | Server
|
|
625
|
-
// Row 4: Trades + W/L | Latency
|
|
626
|
-
const VS = '\u2502'; // Vertical separator (thin)
|
|
627
|
-
|
|
628
|
-
// 2 columns: 48 + 47 + 1 separator = 96
|
|
629
|
-
const colL = 48, colR = 47;
|
|
630
|
-
|
|
631
|
-
// Safe padding function
|
|
632
|
-
const safePad = (len) => ' '.repeat(Math.max(0, len));
|
|
633
|
-
|
|
634
|
-
// Build cell helper
|
|
635
|
-
const buildCell = (label, value, valueColor, width) => {
|
|
636
|
-
const text = ` ${label}: ${valueColor(value)}`;
|
|
637
|
-
const plain = ` ${label}: ${value}`;
|
|
638
|
-
return { text, plain, padded: text + safePad(width - plain.length) };
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
// Row 1: Account | Symbol + Qty
|
|
642
|
-
const accVal = accountName.length > 35 ? accountName.substring(0, 35) : accountName;
|
|
643
|
-
const symVal = symbolName.length > 12 ? symbolName.substring(0, 12) : symbolName;
|
|
644
|
-
const r1c1 = buildCell('Account', accVal, chalk.cyan, colL);
|
|
645
|
-
const r1c2text = ` Symbol: ${chalk.yellow(symVal)} Qty: ${chalk.cyan(numContracts)}`;
|
|
646
|
-
const r1c2plain = ` Symbol: ${symVal} Qty: ${numContracts}`;
|
|
647
|
-
const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
|
|
648
|
-
|
|
649
|
-
// Row 2: Target | Risk
|
|
650
|
-
const r2c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
|
|
651
|
-
const r2c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
|
|
652
|
-
|
|
653
|
-
// Row 3: P&L | Server
|
|
654
|
-
const r3c1 = buildCell('P&L', pnlStr, pnlColor, colL);
|
|
655
|
-
const r3c2 = buildCell('Server', serverStatus, serverColor, colR);
|
|
656
|
-
|
|
657
|
-
// Row 4: Trades + W/L | Latency
|
|
658
|
-
const r4c1text = ` Trades: ${chalk.cyan(stats.trades)} W/L: ${chalk.green(stats.wins)}/${chalk.red(stats.losses)}`;
|
|
659
|
-
const r4c1plain = ` Trades: ${stats.trades} W/L: ${stats.wins}/${stats.losses}`;
|
|
660
|
-
const r4c1 = r4c1text + safePad(colL - r4c1plain.length);
|
|
661
|
-
const r4c2 = buildCell('Latency', latencyStr, latencyColor, colR);
|
|
662
|
-
|
|
663
|
-
// Grid separators
|
|
664
|
-
const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
|
|
665
|
-
const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
|
|
666
|
-
const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
|
|
667
|
-
|
|
668
|
-
// Print grid
|
|
669
|
-
printLine(chalk.cyan(GRID_TOP));
|
|
670
|
-
printLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
|
|
671
|
-
printLine(chalk.cyan(GRID_MID));
|
|
672
|
-
printLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2.padded + chalk.cyan(V));
|
|
673
|
-
printLine(chalk.cyan(GRID_MID));
|
|
674
|
-
printLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
|
|
675
|
-
printLine(chalk.cyan(GRID_MID));
|
|
676
|
-
printLine(chalk.cyan(V) + r4c1 + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
|
|
677
|
-
printLine(chalk.cyan(GRID_BOT));
|
|
678
|
-
|
|
679
|
-
// Activity log header with spinner and centered date
|
|
680
|
-
spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
|
|
681
|
-
const spinnerChar = spinnerChars[spinnerFrame];
|
|
682
|
-
const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
|
|
683
|
-
const actLeftPlain = ` Activity Log ${spinnerChar}`;
|
|
684
|
-
const actRight = 'Press X to stop ';
|
|
685
|
-
const dateCentered = `- ${dateStr} -`;
|
|
686
|
-
const leftLen = actLeftPlain.length;
|
|
687
|
-
const rightLen = actRight.length;
|
|
688
|
-
const midSpace = Math.max(0, W - leftLen - rightLen);
|
|
689
|
-
const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
|
|
690
|
-
const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
|
|
691
|
-
const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
|
|
692
|
-
bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
|
|
693
|
-
bufferLine(chalk.cyan(MID));
|
|
694
|
-
|
|
695
|
-
// Helper to strip ANSI codes for length calculation
|
|
696
|
-
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
697
|
-
|
|
698
|
-
// Helper to truncate and pad text to exact width W
|
|
699
|
-
const fitToWidth = (text, width) => {
|
|
700
|
-
const plainText = stripAnsi(text);
|
|
701
|
-
if (plainText.length > width) {
|
|
702
|
-
// Truncate - find where to cut in original string
|
|
703
|
-
let count = 0;
|
|
704
|
-
let cutIndex = 0;
|
|
705
|
-
for (let i = 0; i < text.length && count < width - 3; i++) {
|
|
706
|
-
if (text[i] === '\x1B') {
|
|
707
|
-
// Skip ANSI sequence
|
|
708
|
-
while (i < text.length && text[i] !== 'm') i++;
|
|
709
|
-
} else {
|
|
710
|
-
count++;
|
|
711
|
-
cutIndex = i + 1;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return text.substring(0, cutIndex) + '...';
|
|
715
|
-
}
|
|
716
|
-
return text + ' '.repeat(width - plainText.length);
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
// Logs inside the rectangle - newest first, max 50 lines
|
|
720
|
-
const MAX_VISIBLE_LOGS = 50;
|
|
721
|
-
|
|
722
|
-
if (logs.length === 0) {
|
|
723
|
-
const emptyLine = ' Waiting for activity...';
|
|
724
|
-
bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
|
|
725
|
-
// Fill remaining lines
|
|
726
|
-
for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
|
|
727
|
-
bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
|
|
728
|
-
}
|
|
729
|
-
} else {
|
|
730
|
-
// Show newest first (reverse), limited to MAX_VISIBLE_LOGS
|
|
731
|
-
const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
|
|
732
|
-
reversedLogs.forEach(log => {
|
|
733
|
-
const color = typeColors[log.type] || chalk.white;
|
|
734
|
-
const icon = getIcon(log.type);
|
|
735
|
-
// Build log line content (plain text, no color yet)
|
|
736
|
-
const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
|
|
737
|
-
// Fit to width then apply color
|
|
738
|
-
const fitted = fitToWidth(logContent, W);
|
|
739
|
-
bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
|
|
740
|
-
});
|
|
741
|
-
// Fill remaining lines with empty to keep fixed height
|
|
742
|
-
for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
|
|
743
|
-
bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Bottom border to close the rectangle
|
|
748
|
-
bufferLine(chalk.cyan(BOT));
|
|
749
|
-
|
|
750
|
-
// Write entire buffer atomically
|
|
751
|
-
process.stdout.write(screenBuffer);
|
|
752
|
-
|
|
753
|
-
isDrawing = false;
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
// Spinner interval to refresh UI - 250ms for stability
|
|
757
|
-
const spinnerInterval = setInterval(() => {
|
|
758
|
-
if (algoRunning && !isDrawing) {
|
|
759
|
-
displayUI();
|
|
760
|
-
}
|
|
761
|
-
}, 250);
|
|
762
|
-
|
|
763
|
-
// Connect to HQX Server
|
|
764
|
-
const spinner = ora('Authenticating with HQX Server...').start();
|
|
765
|
-
|
|
766
|
-
try {
|
|
767
|
-
// Authenticate
|
|
768
|
-
const authResult = await hqxServer.authenticate(account.accountId.toString(), account.propfirm || 'projectx');
|
|
769
|
-
|
|
770
|
-
if (!authResult.success) {
|
|
771
|
-
spinner.fail('Authentication failed: ' + (authResult.error || 'Unknown error'));
|
|
772
|
-
addLog('error', 'Authentication failed');
|
|
773
|
-
|
|
774
|
-
// Fallback to offline mode
|
|
775
|
-
console.log(chalk.yellow(' Running in offline demo mode...'));
|
|
776
|
-
console.log();
|
|
777
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
spinner.text = 'Connecting to WebSocket...';
|
|
782
|
-
|
|
783
|
-
// Connect WebSocket
|
|
784
|
-
const connectResult = await hqxServer.connect();
|
|
785
|
-
|
|
786
|
-
if (connectResult.success) {
|
|
787
|
-
spinner.succeed('Connected to HQX Server');
|
|
788
|
-
hqxConnected = true;
|
|
789
|
-
} else {
|
|
790
|
-
throw new Error('WebSocket connection failed');
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
} catch (error) {
|
|
794
|
-
spinner.warn('HQX Server unavailable - Running in offline mode');
|
|
795
|
-
hqxConnected = false;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Setup event handlers - logs scroll down naturally
|
|
799
|
-
hqxServer.on('latency', (data) => {
|
|
800
|
-
latency = data.latency || 0;
|
|
801
|
-
// Don't call displayUI() - spinner interval will refresh
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
hqxServer.on('log', (data) => {
|
|
805
|
-
let message = data.message;
|
|
806
|
-
// If account name is hidden, filter it from logs too
|
|
807
|
-
if (!showAccountName && realAccountName) {
|
|
808
|
-
message = message.replace(new RegExp(realAccountName, 'gi'), 'HQX *****');
|
|
809
|
-
}
|
|
810
|
-
printLog(data.type || 'info', message);
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
hqxServer.on('signal', (data) => {
|
|
814
|
-
stats.signals++;
|
|
815
|
-
const side = data.side === 'long' ? 'BUY' : 'SELL';
|
|
816
|
-
printLog('signal', `${side} Signal @ ${data.entry?.toFixed(2) || 'N/A'} | SL: ${data.stop?.toFixed(2) || 'N/A'} | TP: ${data.target?.toFixed(2) || 'N/A'}`);
|
|
817
|
-
|
|
818
|
-
// Execute order via PropFirm API if connected
|
|
819
|
-
if (hqxConnected && service) {
|
|
820
|
-
executeSignal(service, account, contract, numContracts, data);
|
|
821
|
-
}
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
hqxServer.on('trade', (data) => {
|
|
825
|
-
stats.trades++;
|
|
826
|
-
stats.pnl += data.pnl || 0;
|
|
827
|
-
if (data.pnl > 0) {
|
|
828
|
-
stats.wins++;
|
|
829
|
-
printLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
|
|
830
|
-
} else {
|
|
831
|
-
stats.losses++;
|
|
832
|
-
printLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
|
|
833
|
-
}
|
|
834
|
-
stats.winRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : '0.0';
|
|
835
|
-
|
|
836
|
-
// Print updated stats
|
|
837
|
-
const statsType = stats.pnl >= 0 ? 'info' : 'loss';
|
|
838
|
-
printLog(statsType, `Stats: Trades: ${stats.trades} | Wins: ${stats.wins} | P&L: $${stats.pnl.toFixed(2)}`);
|
|
839
|
-
|
|
840
|
-
// Check daily target
|
|
841
|
-
if (stats.pnl >= dailyTarget) {
|
|
842
|
-
stopReason = 'target';
|
|
843
|
-
printLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
|
|
844
|
-
algoRunning = false;
|
|
845
|
-
if (hqxConnected) {
|
|
846
|
-
hqxServer.stopAlgo();
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Check max risk
|
|
851
|
-
if (stats.pnl <= -maxRisk) {
|
|
852
|
-
stopReason = 'risk';
|
|
853
|
-
printLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
854
|
-
algoRunning = false;
|
|
855
|
-
if (hqxConnected) {
|
|
856
|
-
hqxServer.stopAlgo();
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
hqxServer.on('stats', (data) => {
|
|
862
|
-
// Update stats from server
|
|
863
|
-
stats.trades = data.trades || stats.trades;
|
|
864
|
-
stats.wins = data.wins || stats.wins;
|
|
865
|
-
stats.losses = data.losses || stats.losses;
|
|
866
|
-
stats.signals = data.signals || stats.signals;
|
|
867
|
-
stats.winRate = data.winRate || stats.winRate;
|
|
868
|
-
|
|
869
|
-
// P&L = realized P&L + unrealized P&L from open position
|
|
870
|
-
const realizedPnl = data.pnl || 0;
|
|
871
|
-
const unrealizedPnl = data.position?.pnl || 0;
|
|
872
|
-
stats.pnl = realizedPnl + unrealizedPnl;
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
hqxServer.on('error', (data) => {
|
|
876
|
-
printLog('error', data.message || 'Unknown error');
|
|
877
|
-
// Stop algo on connection error
|
|
878
|
-
if (!stopReason) {
|
|
879
|
-
stopReason = 'connection_error';
|
|
880
|
-
algoRunning = false;
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
hqxServer.on('disconnected', () => {
|
|
885
|
-
hqxConnected = false;
|
|
886
|
-
// Only log error if not intentionally stopped by user
|
|
887
|
-
if (!stopReason || stopReason === 'user') {
|
|
888
|
-
// Don't show error for user-initiated stop
|
|
889
|
-
if (!stopReason) {
|
|
890
|
-
printLog('error', 'Connection lost - Stopping algo');
|
|
891
|
-
stopReason = 'disconnected';
|
|
892
|
-
algoRunning = false;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Display header once
|
|
898
|
-
displayUI();
|
|
899
|
-
|
|
900
|
-
// Start algo
|
|
901
|
-
if (hqxConnected) {
|
|
902
|
-
printLog('info', 'Starting HQX Ultra-Scalping...');
|
|
903
|
-
printLog('info', `Target: $${dailyTarget.toFixed(2)} | Risk: $${maxRisk.toFixed(2)}`);
|
|
904
|
-
|
|
905
|
-
// Get propfirm token for real market data
|
|
906
|
-
const propfirmToken = service.getToken ? service.getToken() : null;
|
|
907
|
-
const propfirmId = service.getPropfirm ? service.getPropfirm() : (account.propfirm || 'topstep');
|
|
908
|
-
|
|
909
|
-
hqxServer.startAlgo({
|
|
910
|
-
accountId: account.accountId,
|
|
911
|
-
contractId: contract.id || contract.contractId,
|
|
912
|
-
symbol: symbol,
|
|
913
|
-
contracts: numContracts,
|
|
914
|
-
dailyTarget: dailyTarget,
|
|
915
|
-
maxRisk: maxRisk,
|
|
916
|
-
propfirm: propfirmId,
|
|
917
|
-
propfirmToken: propfirmToken
|
|
918
|
-
});
|
|
919
|
-
algoRunning = true;
|
|
920
|
-
} else {
|
|
921
|
-
printLog('warning', 'Running in offline demo mode');
|
|
922
|
-
printLog('info', 'No real trades will be executed');
|
|
923
|
-
algoRunning = true;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Wait for X key OR auto-stop (target/risk reached)
|
|
927
|
-
await new Promise((resolve) => {
|
|
928
|
-
let resolved = false;
|
|
929
|
-
|
|
930
|
-
const cleanup = () => {
|
|
931
|
-
if (resolved) return;
|
|
932
|
-
resolved = true;
|
|
933
|
-
clearInterval(checkInterval);
|
|
934
|
-
if (process.stdin.isTTY) {
|
|
935
|
-
try {
|
|
936
|
-
process.stdin.setRawMode(false);
|
|
937
|
-
process.stdin.removeAllListeners('keypress');
|
|
938
|
-
} catch (e) {}
|
|
939
|
-
}
|
|
940
|
-
resolve();
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
// Check for auto-stop every 500ms
|
|
944
|
-
const checkInterval = setInterval(() => {
|
|
945
|
-
if (!algoRunning || stopReason) {
|
|
946
|
-
cleanup();
|
|
947
|
-
}
|
|
948
|
-
}, 500);
|
|
949
|
-
|
|
950
|
-
// Listen for X key
|
|
951
|
-
if (process.stdin.isTTY) {
|
|
952
|
-
try {
|
|
953
|
-
readline.emitKeypressEvents(process.stdin);
|
|
954
|
-
process.stdin.setRawMode(true);
|
|
955
|
-
process.stdin.resume();
|
|
956
|
-
|
|
957
|
-
process.stdin.on('keypress', (str, key) => {
|
|
958
|
-
if (!key) return;
|
|
959
|
-
const keyName = key.name?.toLowerCase();
|
|
960
|
-
if (keyName === 'x' || (key.ctrl && keyName === 'c')) {
|
|
961
|
-
stopReason = 'user'; // Set stop reason before cleanup
|
|
962
|
-
cleanup();
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
} catch (e) {
|
|
966
|
-
// Fallback: just wait for auto-stop
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
// Clear spinner interval
|
|
972
|
-
clearInterval(spinnerInterval);
|
|
973
|
-
|
|
974
|
-
// Exit alternate screen buffer and show cursor
|
|
975
|
-
process.stdout.write('\x1B[?1049l'); // Exit alternate screen
|
|
976
|
-
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
977
|
-
|
|
978
|
-
// Stop algo
|
|
979
|
-
console.log();
|
|
980
|
-
if (!stopReason) {
|
|
981
|
-
printLog('warning', 'Stopping algo...');
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Cancel all pending orders and close positions
|
|
985
|
-
printLog('info', 'Cancelling pending orders...');
|
|
986
|
-
|
|
987
|
-
try {
|
|
988
|
-
// Cancel all orders
|
|
989
|
-
const cancelResult = await service.cancelAllOrders(account.accountId);
|
|
990
|
-
if (cancelResult.success) {
|
|
991
|
-
printLog('success', 'All pending orders cancelled');
|
|
992
|
-
} else {
|
|
993
|
-
printLog('warning', 'No pending orders to cancel');
|
|
994
|
-
}
|
|
995
|
-
} catch (e) {
|
|
996
|
-
printLog('warning', 'Could not cancel orders: ' + e.message);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Close all positions for this symbol
|
|
1000
|
-
printLog('info', 'Closing open positions...');
|
|
1001
|
-
|
|
1002
|
-
try {
|
|
1003
|
-
const positions = await service.getPositions(account.accountId);
|
|
1004
|
-
if (positions.success && positions.positions) {
|
|
1005
|
-
const symbolPos = positions.positions.find(p =>
|
|
1006
|
-
p.symbol === symbol ||
|
|
1007
|
-
p.contractId === (contract.id || contract.contractId)
|
|
1008
|
-
);
|
|
1009
|
-
|
|
1010
|
-
if (symbolPos && symbolPos.quantity !== 0) {
|
|
1011
|
-
const closeResult = await service.closePosition(account.accountId, symbolPos.contractId || symbolPos.symbol);
|
|
1012
|
-
if (closeResult.success) {
|
|
1013
|
-
printLog('success', `Position closed: ${Math.abs(symbolPos.quantity)} ${symbol}`);
|
|
1014
|
-
} else {
|
|
1015
|
-
printLog('error', 'Failed to close position: ' + (closeResult.error || 'Unknown'));
|
|
1016
|
-
}
|
|
1017
|
-
} else {
|
|
1018
|
-
printLog('info', 'No open position to close');
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
} catch (e) {
|
|
1022
|
-
printLog('warning', 'Could not close positions: ' + e.message);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (hqxConnected && algoRunning) {
|
|
1026
|
-
hqxServer.stopAlgo();
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
hqxServer.disconnect();
|
|
1030
|
-
algoRunning = false;
|
|
1031
|
-
|
|
1032
|
-
// Small delay to ensure all cleanup is done
|
|
1033
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1034
|
-
|
|
1035
|
-
// Show cursor again (don't clear screen - show summary below logs)
|
|
1036
|
-
process.stdout.write('\x1B[?25h');
|
|
1037
|
-
|
|
1038
|
-
// Print stop reason message
|
|
1039
|
-
console.log();
|
|
1040
|
-
console.log();
|
|
1041
|
-
if (stopReason === 'target') {
|
|
1042
|
-
console.log(chalk.green.bold(' [OK] Daily target reached! Algo stopped.'));
|
|
1043
|
-
} else if (stopReason === 'risk') {
|
|
1044
|
-
console.log(chalk.red.bold(' [X] Max risk reached! Algo stopped.'));
|
|
1045
|
-
} else if (stopReason === 'disconnected' || stopReason === 'connection_error') {
|
|
1046
|
-
console.log(chalk.red.bold(' [X] Connection lost! Algo stopped.'));
|
|
1047
|
-
} else if (stopReason === 'user') {
|
|
1048
|
-
console.log(chalk.yellow(' [OK] Algo stopped by user'));
|
|
1049
|
-
} else {
|
|
1050
|
-
console.log(chalk.yellow(' [OK] Algo stopped by user'));
|
|
1051
|
-
}
|
|
1052
|
-
console.log();
|
|
1053
|
-
|
|
1054
|
-
// Final stats in a grid box - must match main UI width of 96
|
|
1055
|
-
const summaryV = '\u2551';
|
|
1056
|
-
const summaryVS = '\u2502';
|
|
1057
|
-
const summaryH = '\u2550';
|
|
1058
|
-
const summaryW = 96; // Same as main UI
|
|
1059
|
-
|
|
1060
|
-
// Calculate session duration
|
|
1061
|
-
const sessionDuration = Date.now() - sessionStartTime;
|
|
1062
|
-
const durationSec = Math.floor(sessionDuration / 1000);
|
|
1063
|
-
const durationMin = Math.floor(durationSec / 60);
|
|
1064
|
-
const durationHr = Math.floor(durationMin / 60);
|
|
1065
|
-
const durationStr = durationHr > 0
|
|
1066
|
-
? `${durationHr}h ${durationMin % 60}m ${durationSec % 60}s`
|
|
1067
|
-
: durationMin > 0
|
|
1068
|
-
? `${durationMin}m ${durationSec % 60}s`
|
|
1069
|
-
: `${durationSec}s`;
|
|
1070
|
-
|
|
1071
|
-
// 4 cells + 3 separators = 96 inner chars
|
|
1072
|
-
// 96 - 3 separators = 93, divided by 4 = 23.25, so use 24+23+24+23 = 94... need 96
|
|
1073
|
-
// Let's use: 24 + 24 + 24 + 21 = 93 + 3 sep = 96
|
|
1074
|
-
const sc1 = 24, sc2 = 24, sc3 = 24, sc4 = 21;
|
|
1075
|
-
|
|
1076
|
-
const summaryCell = (label, value, width) => {
|
|
1077
|
-
const text = ` ${label}: ${value}`;
|
|
1078
|
-
const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1079
|
-
const padding = Math.max(0, width - stripped.length);
|
|
1080
|
-
return text + ' '.repeat(padding);
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
const centerSummaryTitle = (text, width) => {
|
|
1084
|
-
const pad = Math.floor((width - text.length) / 2);
|
|
1085
|
-
return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
|
-
const pnlValue = stats.pnl >= 0 ? chalk.green('+$' + stats.pnl.toFixed(2)) : chalk.red('-$' + Math.abs(stats.pnl).toFixed(2));
|
|
1089
|
-
|
|
1090
|
-
// Build separator lines
|
|
1091
|
-
const SUMMARY_TOP = '\u2554' + summaryH.repeat(summaryW) + '\u2557';
|
|
1092
|
-
const SUMMARY_GRID_TOP = '\u2560' + summaryH.repeat(sc1) + '\u2564' + summaryH.repeat(sc2) + '\u2564' + summaryH.repeat(sc3) + '\u2564' + summaryH.repeat(sc4) + '\u2563';
|
|
1093
|
-
const SUMMARY_GRID_MID = '\u2560' + summaryH.repeat(sc1) + '\u256A' + summaryH.repeat(sc2) + '\u256A' + summaryH.repeat(sc3) + '\u256A' + summaryH.repeat(sc4) + '\u2563';
|
|
1094
|
-
const SUMMARY_BOT = '\u255A' + summaryH.repeat(sc1) + '\u2567' + summaryH.repeat(sc2) + '\u2567' + summaryH.repeat(sc3) + '\u2567' + summaryH.repeat(sc4) + '\u255D';
|
|
1095
|
-
|
|
1096
|
-
console.log();
|
|
1097
|
-
console.log(chalk.cyan(SUMMARY_TOP));
|
|
1098
|
-
console.log(chalk.cyan(summaryV) + chalk.white.bold(centerSummaryTitle('Session Summary', summaryW)) + chalk.cyan(summaryV));
|
|
1099
|
-
console.log(chalk.cyan(SUMMARY_GRID_TOP));
|
|
1100
|
-
|
|
1101
|
-
// Row 1: Target | Risk | P&L | Win Rate
|
|
1102
|
-
const r1c1 = summaryCell('Target', chalk.green('$' + dailyTarget.toFixed(2)), sc1);
|
|
1103
|
-
const r1c2 = summaryCell('Risk', chalk.red('$' + maxRisk.toFixed(2)), sc2);
|
|
1104
|
-
const r1c3 = summaryCell('P&L', pnlValue, sc3);
|
|
1105
|
-
const r1c4 = summaryCell('Win Rate', chalk.yellow(stats.winRate + '%'), sc4);
|
|
1106
|
-
console.log(chalk.cyan(summaryV) + r1c1 + chalk.cyan(summaryVS) + r1c2 + chalk.cyan(summaryVS) + r1c3 + chalk.cyan(summaryVS) + r1c4 + chalk.cyan(summaryV));
|
|
1107
|
-
|
|
1108
|
-
console.log(chalk.cyan(SUMMARY_GRID_MID));
|
|
1109
|
-
|
|
1110
|
-
// Row 2: Trades | Wins | Losses | Duration
|
|
1111
|
-
const r2c1 = summaryCell('Trades', chalk.cyan(stats.trades.toString()), sc1);
|
|
1112
|
-
const r2c2 = summaryCell('Wins', chalk.green(stats.wins.toString()), sc2);
|
|
1113
|
-
const r2c3 = summaryCell('Losses', chalk.red(stats.losses.toString()), sc3);
|
|
1114
|
-
const r2c4 = summaryCell('Duration', chalk.white(durationStr), sc4);
|
|
1115
|
-
console.log(chalk.cyan(summaryV) + r2c1 + chalk.cyan(summaryVS) + r2c2 + chalk.cyan(summaryVS) + r2c3 + chalk.cyan(summaryVS) + r2c4 + chalk.cyan(summaryV));
|
|
1116
|
-
|
|
1117
|
-
console.log(chalk.cyan(SUMMARY_BOT));
|
|
1118
|
-
console.log();
|
|
1119
|
-
|
|
1120
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
1121
|
-
};
|
|
1122
|
-
|
|
1123
|
-
/**
|
|
1124
|
-
* Execute signal via PropFirm API
|
|
1125
|
-
*/
|
|
1126
|
-
const executeSignal = async (service, account, contract, numContracts, signal) => {
|
|
1127
|
-
try {
|
|
1128
|
-
const orderData = {
|
|
1129
|
-
accountId: account.accountId,
|
|
1130
|
-
contractId: contract.id || contract.contractId,
|
|
1131
|
-
type: 2, // Market order
|
|
1132
|
-
side: signal.side === 'long' ? 0 : 1, // 0=Buy, 1=Sell
|
|
1133
|
-
size: numContracts
|
|
1134
|
-
};
|
|
1135
|
-
|
|
1136
|
-
// Place order via ProjectX Gateway API
|
|
1137
|
-
const result = await service.placeOrder(orderData);
|
|
1138
|
-
|
|
1139
|
-
if (result.success) {
|
|
1140
|
-
console.log(chalk.green(` [OK] Order executed: ${signal.side.toUpperCase()} ${numContracts} contracts`));
|
|
1141
|
-
} else {
|
|
1142
|
-
console.log(chalk.red(` [X] Order failed: ${result.error || 'Unknown error'}`));
|
|
1143
|
-
}
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
console.log(chalk.red(` [X] Order error: ${error.message}`));
|
|
1146
|
-
}
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Copy Trading Menu
|
|
1151
|
-
*/
|
|
1152
|
-
const copyTradingMenu = async () => {
|
|
1153
|
-
console.log();
|
|
1154
|
-
console.log(chalk.gray(getSeparator()));
|
|
1155
|
-
console.log(chalk.green.bold(' Copy Trading Setup'));
|
|
1156
|
-
console.log(chalk.gray(getSeparator()));
|
|
1157
|
-
console.log();
|
|
1158
|
-
|
|
1159
|
-
// Check market status first
|
|
1160
|
-
const marketSpinner = ora('Checking market status...').start();
|
|
1161
|
-
|
|
1162
|
-
// Use a simple market hours check
|
|
1163
|
-
const now = new Date();
|
|
1164
|
-
const utcDay = now.getUTCDay();
|
|
1165
|
-
const utcHour = now.getUTCHours();
|
|
1166
|
-
const isDST = (() => {
|
|
1167
|
-
const jan = new Date(now.getFullYear(), 0, 1);
|
|
1168
|
-
const jul = new Date(now.getFullYear(), 6, 1);
|
|
1169
|
-
return now.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
1170
|
-
})();
|
|
1171
|
-
const ctOffset = isDST ? 5 : 6;
|
|
1172
|
-
const ctHour = (utcHour - ctOffset + 24) % 24;
|
|
1173
|
-
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
1174
|
-
|
|
1175
|
-
let marketClosed = false;
|
|
1176
|
-
let marketMessage = '';
|
|
1177
|
-
|
|
1178
|
-
if (ctDay === 6) {
|
|
1179
|
-
marketClosed = true;
|
|
1180
|
-
marketMessage = 'Market closed (Saturday)';
|
|
1181
|
-
} else if (ctDay === 0 && ctHour < 17) {
|
|
1182
|
-
marketClosed = true;
|
|
1183
|
-
marketMessage = 'Market opens Sunday 5:00 PM CT';
|
|
1184
|
-
} else if (ctDay === 5 && ctHour >= 16) {
|
|
1185
|
-
marketClosed = true;
|
|
1186
|
-
marketMessage = 'Market closed (Friday after 4PM CT)';
|
|
1187
|
-
} else if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
|
|
1188
|
-
marketClosed = true;
|
|
1189
|
-
marketMessage = 'Daily maintenance (4:00-5:00 PM CT)';
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
if (marketClosed) {
|
|
1193
|
-
marketSpinner.fail('Market is CLOSED');
|
|
1194
|
-
console.log();
|
|
1195
|
-
console.log(chalk.red.bold(' [X] ' + marketMessage));
|
|
1196
|
-
console.log();
|
|
1197
|
-
console.log(chalk.gray(' Futures markets (CME) trading hours:'));
|
|
1198
|
-
console.log(chalk.gray(' Sunday 5:00 PM CT - Friday 4:00 PM CT'));
|
|
1199
|
-
console.log(chalk.gray(' Daily maintenance: 4:00 PM - 5:00 PM CT'));
|
|
1200
|
-
console.log();
|
|
1201
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
marketSpinner.succeed('Market is OPEN - Ready to trade!');
|
|
1206
|
-
console.log();
|
|
1207
|
-
|
|
1208
|
-
// Get all active accounts from all connections
|
|
1209
|
-
const allAccounts = await connections.getAllAccounts();
|
|
1210
|
-
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
1211
|
-
|
|
1212
|
-
if (activeAccounts.length < 2) {
|
|
1213
|
-
console.log(chalk.red(' [X] You need at least 2 active accounts for copy trading.'));
|
|
1214
|
-
console.log(chalk.gray(' Connect more prop firm accounts first.'));
|
|
1215
|
-
console.log();
|
|
1216
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// Step 1: Risk Management Settings
|
|
1221
|
-
console.log(chalk.cyan.bold(' Step 1: Risk Management'));
|
|
1222
|
-
console.log(chalk.gray(' Set your daily target and maximum risk to auto-stop copy trading.'));
|
|
1223
|
-
console.log();
|
|
1224
|
-
|
|
1225
|
-
const { dailyTarget } = await inquirer.prompt([
|
|
1226
|
-
{
|
|
1227
|
-
type: 'input',
|
|
1228
|
-
name: 'dailyTarget',
|
|
1229
|
-
message: chalk.white.bold('Daily Target ($):'),
|
|
1230
|
-
default: '500',
|
|
1231
|
-
validate: (input) => {
|
|
1232
|
-
const num = parseFloat(input);
|
|
1233
|
-
if (isNaN(num) || num <= 0) {
|
|
1234
|
-
return 'Please enter a valid amount greater than 0';
|
|
1235
|
-
}
|
|
1236
|
-
return true;
|
|
1237
|
-
},
|
|
1238
|
-
filter: (input) => parseFloat(input)
|
|
1239
|
-
}
|
|
1240
|
-
]);
|
|
1241
|
-
|
|
1242
|
-
const { maxRisk } = await inquirer.prompt([
|
|
1243
|
-
{
|
|
1244
|
-
type: 'input',
|
|
1245
|
-
name: 'maxRisk',
|
|
1246
|
-
message: chalk.white.bold('Max Risk ($):'),
|
|
1247
|
-
default: '200',
|
|
1248
|
-
validate: (input) => {
|
|
1249
|
-
const num = parseFloat(input);
|
|
1250
|
-
if (isNaN(num) || num <= 0) {
|
|
1251
|
-
return 'Please enter a valid amount greater than 0';
|
|
1252
|
-
}
|
|
1253
|
-
return true;
|
|
1254
|
-
},
|
|
1255
|
-
filter: (input) => parseFloat(input)
|
|
1256
|
-
}
|
|
1257
|
-
]);
|
|
1258
|
-
|
|
1259
|
-
console.log();
|
|
1260
|
-
console.log(chalk.gray(' Daily Target: ') + chalk.green('$' + dailyTarget.toFixed(2)));
|
|
1261
|
-
console.log(chalk.gray(' Max Risk: ') + chalk.red('$' + maxRisk.toFixed(2)));
|
|
1262
|
-
console.log();
|
|
1263
|
-
|
|
1264
|
-
// Step 2: Select Lead Account
|
|
1265
|
-
console.log(chalk.cyan.bold(' Step 2: Select Lead Account'));
|
|
1266
|
-
console.log(chalk.gray(' The lead account is the master account whose trades will be copied.'));
|
|
1267
|
-
console.log();
|
|
1268
|
-
|
|
1269
|
-
const leadChoices = activeAccounts.map(acc => ({
|
|
1270
|
-
name: chalk.cyan(`${acc.accountName || acc.name} - ${acc.propfirm} - $${acc.balance.toLocaleString()}`),
|
|
1271
|
-
value: acc
|
|
1272
|
-
}));
|
|
1273
|
-
leadChoices.push(new inquirer.Separator());
|
|
1274
|
-
leadChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
|
|
1275
|
-
|
|
1276
|
-
const { leadAccount } = await inquirer.prompt([
|
|
1277
|
-
{
|
|
1278
|
-
type: 'list',
|
|
1279
|
-
name: 'leadAccount',
|
|
1280
|
-
message: chalk.white.bold('Lead Account:'),
|
|
1281
|
-
choices: leadChoices,
|
|
1282
|
-
pageSize: 15,
|
|
1283
|
-
loop: false
|
|
1284
|
-
}
|
|
1285
|
-
]);
|
|
1286
|
-
|
|
1287
|
-
if (leadAccount === 'back') return;
|
|
1288
|
-
|
|
1289
|
-
// Step 3: Select Follower Account
|
|
1290
|
-
console.log();
|
|
1291
|
-
console.log(chalk.cyan.bold(' Step 3: Select Follower Account'));
|
|
1292
|
-
console.log(chalk.gray(' The follower account will copy trades from the lead account.'));
|
|
1293
|
-
console.log();
|
|
1294
|
-
|
|
1295
|
-
const followerChoices = activeAccounts
|
|
1296
|
-
.filter(acc => acc.accountId !== leadAccount.accountId)
|
|
1297
|
-
.map(acc => ({
|
|
1298
|
-
name: chalk.cyan(`${acc.accountName || acc.name} - ${acc.propfirm} - $${acc.balance.toLocaleString()}`),
|
|
1299
|
-
value: acc
|
|
1300
|
-
}));
|
|
1301
|
-
followerChoices.push(new inquirer.Separator());
|
|
1302
|
-
followerChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
|
|
1303
|
-
|
|
1304
|
-
const { followerAccount } = await inquirer.prompt([
|
|
1305
|
-
{
|
|
1306
|
-
type: 'list',
|
|
1307
|
-
name: 'followerAccount',
|
|
1308
|
-
message: chalk.white.bold('Follower Account:'),
|
|
1309
|
-
choices: followerChoices,
|
|
1310
|
-
pageSize: 15,
|
|
1311
|
-
loop: false
|
|
1312
|
-
}
|
|
1313
|
-
]);
|
|
1314
|
-
|
|
1315
|
-
if (followerAccount === 'back') return;
|
|
1316
|
-
|
|
1317
|
-
// Step 4: Select Lead Symbol
|
|
1318
|
-
console.log();
|
|
1319
|
-
console.log(chalk.cyan.bold(' Step 4: Configure Lead Symbol'));
|
|
1320
|
-
console.log();
|
|
1321
|
-
|
|
1322
|
-
const { leadSymbol } = await inquirer.prompt([
|
|
1323
|
-
{
|
|
1324
|
-
type: 'list',
|
|
1325
|
-
name: 'leadSymbol',
|
|
1326
|
-
message: chalk.white.bold('Lead Symbol:'),
|
|
1327
|
-
choices: FUTURES_SYMBOLS.map(s => ({
|
|
1328
|
-
name: chalk.cyan(s.name),
|
|
1329
|
-
value: s
|
|
1330
|
-
})),
|
|
1331
|
-
pageSize: 15,
|
|
1332
|
-
loop: false
|
|
1333
|
-
}
|
|
1334
|
-
]);
|
|
1335
|
-
|
|
1336
|
-
const { leadContracts } = await inquirer.prompt([
|
|
1337
|
-
{
|
|
1338
|
-
type: 'number',
|
|
1339
|
-
name: 'leadContracts',
|
|
1340
|
-
message: chalk.white.bold('Lead Number of Contracts:'),
|
|
1341
|
-
default: 1,
|
|
1342
|
-
validate: (input) => {
|
|
1343
|
-
if (isNaN(input) || input <= 0 || input > 100) {
|
|
1344
|
-
return 'Please enter a valid number between 1 and 100';
|
|
1345
|
-
}
|
|
1346
|
-
return true;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
]);
|
|
1350
|
-
|
|
1351
|
-
// Step 5: Select Follower Symbol
|
|
1352
|
-
console.log();
|
|
1353
|
-
console.log(chalk.cyan.bold(' Step 5: Configure Follower Symbol'));
|
|
1354
|
-
console.log();
|
|
1355
|
-
|
|
1356
|
-
const { followerSymbol } = await inquirer.prompt([
|
|
1357
|
-
{
|
|
1358
|
-
type: 'list',
|
|
1359
|
-
name: 'followerSymbol',
|
|
1360
|
-
message: chalk.white.bold('Follower Symbol:'),
|
|
1361
|
-
choices: FUTURES_SYMBOLS.map(s => ({
|
|
1362
|
-
name: chalk.cyan(s.name),
|
|
1363
|
-
value: s
|
|
1364
|
-
})),
|
|
1365
|
-
pageSize: 15,
|
|
1366
|
-
loop: false
|
|
1367
|
-
}
|
|
1368
|
-
]);
|
|
1369
|
-
|
|
1370
|
-
const { followerContracts } = await inquirer.prompt([
|
|
1371
|
-
{
|
|
1372
|
-
type: 'number',
|
|
1373
|
-
name: 'followerContracts',
|
|
1374
|
-
message: chalk.white.bold('Follower Number of Contracts:'),
|
|
1375
|
-
default: 1,
|
|
1376
|
-
validate: (input) => {
|
|
1377
|
-
if (isNaN(input) || input <= 0 || input > 100) {
|
|
1378
|
-
return 'Please enter a valid number between 1 and 100';
|
|
1379
|
-
}
|
|
1380
|
-
return true;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
]);
|
|
1384
|
-
|
|
1385
|
-
// Privacy option - show or hide account names
|
|
1386
|
-
console.log();
|
|
1387
|
-
console.log(chalk.cyan.bold(' Step 6: Privacy Settings'));
|
|
1388
|
-
console.log();
|
|
1389
|
-
|
|
1390
|
-
const { showAccountNames } = await inquirer.prompt([
|
|
1391
|
-
{
|
|
1392
|
-
type: 'list',
|
|
1393
|
-
name: 'showAccountNames',
|
|
1394
|
-
message: chalk.white.bold('Account names visibility:'),
|
|
1395
|
-
choices: [
|
|
1396
|
-
{ name: chalk.cyan('[>] Show account names'), value: true },
|
|
1397
|
-
{ name: chalk.gray('[.] Hide account names'), value: false }
|
|
1398
|
-
],
|
|
1399
|
-
loop: false
|
|
1400
|
-
}
|
|
1401
|
-
]);
|
|
1402
|
-
|
|
1403
|
-
const displayLeadName = showAccountNames ? (leadAccount.accountName || leadAccount.name || 'N/A') : 'HQX Lead *****';
|
|
1404
|
-
const displayFollowerName = showAccountNames ? (followerAccount.accountName || followerAccount.name || 'N/A') : 'HQX Follower *****';
|
|
1405
|
-
|
|
1406
|
-
// Configuration Summary
|
|
1407
|
-
console.log();
|
|
1408
|
-
console.log(chalk.gray(getSeparator()));
|
|
1409
|
-
console.log(chalk.white.bold(' Copy Trading Configuration'));
|
|
1410
|
-
console.log(chalk.gray(getSeparator()));
|
|
1411
|
-
console.log();
|
|
1412
|
-
console.log(chalk.white(' RISK MANAGEMENT'));
|
|
1413
|
-
console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
|
|
1414
|
-
console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
|
|
1415
|
-
console.log();
|
|
1416
|
-
console.log(chalk.white(' LEAD ACCOUNT'));
|
|
1417
|
-
console.log(chalk.white(` Account: ${chalk.cyan(displayLeadName)}`));
|
|
1418
|
-
console.log(chalk.white(` PropFirm: ${chalk.magenta(leadAccount.propfirm || 'N/A')}`));
|
|
1419
|
-
console.log(chalk.white(` Symbol: ${chalk.cyan(leadSymbol.name || 'N/A')}`));
|
|
1420
|
-
console.log(chalk.white(` Contracts: ${chalk.cyan(leadContracts)}`));
|
|
1421
|
-
console.log();
|
|
1422
|
-
console.log(chalk.white(' FOLLOWER ACCOUNT'));
|
|
1423
|
-
console.log(chalk.white(` Account: ${chalk.cyan(displayFollowerName)}`));
|
|
1424
|
-
console.log(chalk.white(` PropFirm: ${chalk.magenta(followerAccount.propfirm || 'N/A')}`));
|
|
1425
|
-
console.log(chalk.white(` Symbol: ${chalk.cyan(followerSymbol.name || 'N/A')}`));
|
|
1426
|
-
console.log(chalk.white(` Contracts: ${chalk.cyan(followerContracts)}`));
|
|
1427
|
-
console.log();
|
|
1428
|
-
console.log(chalk.gray(getSeparator()));
|
|
1429
|
-
console.log();
|
|
1430
|
-
|
|
1431
|
-
const { launch } = await inquirer.prompt([
|
|
1432
|
-
{
|
|
1433
|
-
type: 'list',
|
|
1434
|
-
name: 'launch',
|
|
1435
|
-
message: chalk.white.bold('Ready to launch Copy Trading?'),
|
|
1436
|
-
choices: [
|
|
1437
|
-
{ name: chalk.green.bold('[>] Launch Copy Trading'), value: 'launch' },
|
|
1438
|
-
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
1439
|
-
],
|
|
1440
|
-
loop: false
|
|
1441
|
-
}
|
|
1442
|
-
]);
|
|
1443
|
-
|
|
1444
|
-
if (launch === 'back') return;
|
|
1445
|
-
|
|
1446
|
-
// Launch Copy Trading
|
|
1447
|
-
await launchCopyTrading({
|
|
1448
|
-
dailyTarget,
|
|
1449
|
-
maxRisk,
|
|
1450
|
-
lead: {
|
|
1451
|
-
account: leadAccount,
|
|
1452
|
-
symbol: leadSymbol,
|
|
1453
|
-
contracts: leadContracts,
|
|
1454
|
-
service: leadAccount.service,
|
|
1455
|
-
displayName: displayLeadName
|
|
1456
|
-
},
|
|
1457
|
-
follower: {
|
|
1458
|
-
account: followerAccount,
|
|
1459
|
-
symbol: followerSymbol,
|
|
1460
|
-
contracts: followerContracts,
|
|
1461
|
-
service: followerAccount.service,
|
|
1462
|
-
displayName: displayFollowerName
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
};
|
|
1466
|
-
|
|
1467
|
-
/**
|
|
1468
|
-
* Launch Copy Trading
|
|
1469
|
-
*/
|
|
1470
|
-
const launchCopyTrading = async (config) => {
|
|
1471
|
-
const { lead, follower, dailyTarget, maxRisk } = config;
|
|
1472
|
-
|
|
1473
|
-
console.log();
|
|
1474
|
-
console.log(chalk.green.bold(' [>] Launching Copy Trading...'));
|
|
1475
|
-
console.log();
|
|
1476
|
-
|
|
1477
|
-
let isRunning = true;
|
|
1478
|
-
let stopReason = null;
|
|
1479
|
-
let lastLeadPosition = null;
|
|
1480
|
-
const logs = [];
|
|
1481
|
-
const MAX_LOGS = 25;
|
|
1482
|
-
|
|
1483
|
-
const stats = {
|
|
1484
|
-
copiedTrades: 0,
|
|
1485
|
-
leadTrades: 0,
|
|
1486
|
-
followerTrades: 0,
|
|
1487
|
-
signals: 0,
|
|
1488
|
-
errors: 0,
|
|
1489
|
-
pnl: 0,
|
|
1490
|
-
trades: 0,
|
|
1491
|
-
wins: 0,
|
|
1492
|
-
losses: 0
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
// Log colors
|
|
1496
|
-
const typeColors = {
|
|
1497
|
-
info: chalk.cyan,
|
|
1498
|
-
success: chalk.green,
|
|
1499
|
-
trade: chalk.green.bold,
|
|
1500
|
-
copy: chalk.yellow.bold,
|
|
1501
|
-
signal: chalk.magenta.bold,
|
|
1502
|
-
loss: chalk.red.bold,
|
|
1503
|
-
error: chalk.red,
|
|
1504
|
-
warning: chalk.yellow
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
const getIcon = (type) => {
|
|
1508
|
-
switch(type) {
|
|
1509
|
-
case 'signal': return '[~]';
|
|
1510
|
-
case 'trade': return '[>]';
|
|
1511
|
-
case 'copy': return '[+]';
|
|
1512
|
-
case 'loss': return '[-]';
|
|
1513
|
-
case 'error': return '[X]';
|
|
1514
|
-
case 'success': return '[OK]';
|
|
1515
|
-
default: return '[.]';
|
|
1516
|
-
}
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
|
-
const addLog = (type, message) => {
|
|
1520
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
1521
|
-
logs.push({ timestamp, type, message });
|
|
1522
|
-
if (logs.length > MAX_LOGS) logs.shift();
|
|
1523
|
-
};
|
|
1524
|
-
|
|
1525
|
-
// Build entire screen as a single string buffer to write atomically
|
|
1526
|
-
let screenBuffer = '';
|
|
1527
|
-
let firstDraw = true;
|
|
1528
|
-
let isDrawing = false;
|
|
1529
|
-
let spinnerFrame = 0;
|
|
1530
|
-
const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
1531
|
-
|
|
1532
|
-
// HQX Server connection state (declared here so displayUI can access it)
|
|
1533
|
-
const hqxServer = new HQXServerService();
|
|
1534
|
-
let hqxConnected = false;
|
|
1535
|
-
let latency = 0;
|
|
1536
|
-
|
|
1537
|
-
const bufferLine = (text) => {
|
|
1538
|
-
screenBuffer += text + '\x1B[K\n';
|
|
1539
|
-
};
|
|
1540
|
-
|
|
1541
|
-
const displayUI = () => {
|
|
1542
|
-
// Prevent concurrent draws
|
|
1543
|
-
if (isDrawing) return;
|
|
1544
|
-
isDrawing = true;
|
|
1545
|
-
|
|
1546
|
-
// Reset buffer
|
|
1547
|
-
screenBuffer = '';
|
|
1548
|
-
|
|
1549
|
-
if (firstDraw) {
|
|
1550
|
-
screenBuffer += '\x1B[?1049h'; // Enter alternate screen
|
|
1551
|
-
screenBuffer += '\x1B[?25l'; // Hide cursor
|
|
1552
|
-
screenBuffer += '\x1B[2J'; // Clear screen
|
|
1553
|
-
firstDraw = false;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// Move cursor to home position
|
|
1557
|
-
screenBuffer += '\x1B[H';
|
|
1558
|
-
|
|
1559
|
-
// Stats
|
|
1560
|
-
const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
|
|
1561
|
-
const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
|
|
1562
|
-
|
|
1563
|
-
// Latency formatting
|
|
1564
|
-
const latencyMs = latency > 0 ? latency : 0;
|
|
1565
|
-
const latencyStr = `${latencyMs}ms`;
|
|
1566
|
-
const latencyColor = latencyMs < 100 ? chalk.green : (latencyMs < 300 ? chalk.yellow : chalk.red);
|
|
1567
|
-
|
|
1568
|
-
// Current date
|
|
1569
|
-
const now = new Date();
|
|
1570
|
-
const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
|
|
1571
|
-
|
|
1572
|
-
// Get package version
|
|
1573
|
-
const version = require('../../package.json').version;
|
|
1574
|
-
|
|
1575
|
-
// Fixed width = 96 inner chars
|
|
1576
|
-
const W = 96;
|
|
1577
|
-
const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
|
|
1578
|
-
const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
|
|
1579
|
-
const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
|
|
1580
|
-
const V = '\u2551';
|
|
1581
|
-
|
|
1582
|
-
// Center text helper
|
|
1583
|
-
const center = (text, width) => {
|
|
1584
|
-
const pad = Math.floor((width - text.length) / 2);
|
|
1585
|
-
return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
|
|
1586
|
-
};
|
|
1587
|
-
|
|
1588
|
-
// Safe padding function
|
|
1589
|
-
const safePad = (len) => ' '.repeat(Math.max(0, len));
|
|
1590
|
-
|
|
1591
|
-
// Build cell helper
|
|
1592
|
-
const buildCell = (label, value, valueColor, width) => {
|
|
1593
|
-
const text = ` ${label}: ${valueColor(value)}`;
|
|
1594
|
-
const plain = ` ${label}: ${value}`;
|
|
1595
|
-
return { text, plain, padded: text + safePad(width - plain.length) };
|
|
1596
|
-
};
|
|
1597
|
-
|
|
1598
|
-
bufferLine('');
|
|
1599
|
-
bufferLine(chalk.cyan(TOP));
|
|
1600
|
-
// Logo HEDGEQUANTX
|
|
1601
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
|
|
1602
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
|
|
1603
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
|
|
1604
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
|
|
1605
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
|
|
1606
|
-
bufferLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
|
|
1607
|
-
bufferLine(chalk.cyan(MID));
|
|
1608
|
-
|
|
1609
|
-
// Centered subtitle only
|
|
1610
|
-
const title2 = 'HQX Ultra-Scalping';
|
|
1611
|
-
bufferLine(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
|
|
1612
|
-
|
|
1613
|
-
// Grid layout - 2 columns
|
|
1614
|
-
const VS = '\u2502'; // Vertical separator (thin)
|
|
1615
|
-
const colL = 48, colR = 47;
|
|
1616
|
-
|
|
1617
|
-
// Row 1: Lead Account | Lead Symbol
|
|
1618
|
-
const leadName = (lead.displayName || lead.account.accountName || '').substring(0, 30);
|
|
1619
|
-
const leadSym = lead.symbol.value || lead.symbol.name || '';
|
|
1620
|
-
const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
|
|
1621
|
-
const r1c2text = ` Symbol: ${chalk.yellow(leadSym)} Qty: ${chalk.cyan(lead.contracts)}`;
|
|
1622
|
-
const r1c2plain = ` Symbol: ${leadSym} Qty: ${lead.contracts}`;
|
|
1623
|
-
const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
|
|
1624
|
-
|
|
1625
|
-
// Row 2: Follower Account | Follower Symbol
|
|
1626
|
-
const followerName = (follower.displayName || follower.account.accountName || '').substring(0, 30);
|
|
1627
|
-
const followerSym = follower.symbol.value || follower.symbol.name || '';
|
|
1628
|
-
const r2c1 = buildCell('Follower', followerName, chalk.magenta, colL);
|
|
1629
|
-
const r2c2text = ` Symbol: ${chalk.yellow(followerSym)} Qty: ${chalk.cyan(follower.contracts)}`;
|
|
1630
|
-
const r2c2plain = ` Symbol: ${followerSym} Qty: ${follower.contracts}`;
|
|
1631
|
-
const r2c2 = r2c2text + safePad(colR - r2c2plain.length);
|
|
1632
|
-
|
|
1633
|
-
// Row 3: Target | Risk
|
|
1634
|
-
const r3c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
|
|
1635
|
-
const r3c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
|
|
1636
|
-
|
|
1637
|
-
// Row 4: P&L | Server Status
|
|
1638
|
-
const r4c1 = buildCell('P&L', pnlStr, pnlColor, colL);
|
|
1639
|
-
const serverStr = hqxConnected ? 'HQX ON' : 'MONITOR';
|
|
1640
|
-
const serverColor = hqxConnected ? chalk.green : chalk.yellow;
|
|
1641
|
-
const r4c2 = buildCell('Server', serverStr, serverColor, colR);
|
|
1642
|
-
|
|
1643
|
-
// Row 5: Signals + Lead Trades | Copied + Errors
|
|
1644
|
-
const r5c1text = ` Signals: ${chalk.magenta(stats.signals || 0)} Lead: ${chalk.cyan(stats.leadTrades)}`;
|
|
1645
|
-
const r5c1plain = ` Signals: ${stats.signals || 0} Lead: ${stats.leadTrades}`;
|
|
1646
|
-
const r5c1 = r5c1text + safePad(colL - r5c1plain.length);
|
|
1647
|
-
const r5c2text = ` Copied: ${chalk.green(stats.copiedTrades)} Errors: ${chalk.red(stats.errors)}`;
|
|
1648
|
-
const r5c2plain = ` Copied: ${stats.copiedTrades} Errors: ${stats.errors}`;
|
|
1649
|
-
const r5c2 = r5c2text + safePad(colR - r5c2plain.length);
|
|
1650
|
-
|
|
1651
|
-
// Row 6: Trades + W/L | Latency
|
|
1652
|
-
const r6c1text = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
|
|
1653
|
-
const r6c1plain = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
|
|
1654
|
-
const r6c1 = r6c1text + safePad(colL - r6c1plain.length);
|
|
1655
|
-
const r6c2 = buildCell('Latency', latencyStr, latencyColor, colR);
|
|
1656
|
-
|
|
1657
|
-
// Grid separators
|
|
1658
|
-
const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
|
|
1659
|
-
const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
|
|
1660
|
-
const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
|
|
1661
|
-
|
|
1662
|
-
// Print grid
|
|
1663
|
-
bufferLine(chalk.cyan(GRID_TOP));
|
|
1664
|
-
bufferLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
|
|
1665
|
-
bufferLine(chalk.cyan(GRID_MID));
|
|
1666
|
-
bufferLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2 + chalk.cyan(V));
|
|
1667
|
-
bufferLine(chalk.cyan(GRID_MID));
|
|
1668
|
-
bufferLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
|
|
1669
|
-
bufferLine(chalk.cyan(GRID_MID));
|
|
1670
|
-
bufferLine(chalk.cyan(V) + r4c1.padded + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
|
|
1671
|
-
bufferLine(chalk.cyan(GRID_MID));
|
|
1672
|
-
bufferLine(chalk.cyan(V) + r5c1 + chalk.cyan(VS) + r5c2 + chalk.cyan(V));
|
|
1673
|
-
bufferLine(chalk.cyan(GRID_MID));
|
|
1674
|
-
bufferLine(chalk.cyan(V) + r6c1 + chalk.cyan(VS) + r6c2.padded + chalk.cyan(V));
|
|
1675
|
-
bufferLine(chalk.cyan(GRID_BOT));
|
|
1676
|
-
|
|
1677
|
-
// Activity log header with spinner and centered date
|
|
1678
|
-
spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
|
|
1679
|
-
const spinnerChar = spinnerChars[spinnerFrame];
|
|
1680
|
-
const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
|
|
1681
|
-
const actLeftPlain = ` Activity Log ${spinnerChar}`;
|
|
1682
|
-
const actRight = 'Press X to stop ';
|
|
1683
|
-
const dateCentered = `- ${dateStr} -`;
|
|
1684
|
-
const leftLen = actLeftPlain.length;
|
|
1685
|
-
const rightLen = actRight.length;
|
|
1686
|
-
const midSpace = Math.max(0, W - leftLen - rightLen);
|
|
1687
|
-
const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
|
|
1688
|
-
const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
|
|
1689
|
-
const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
|
|
1690
|
-
bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
|
|
1691
|
-
bufferLine(chalk.cyan(MID));
|
|
1692
|
-
|
|
1693
|
-
// Helper to strip ANSI codes for length calculation
|
|
1694
|
-
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
1695
|
-
|
|
1696
|
-
// Helper to truncate and pad text to exact width W
|
|
1697
|
-
const fitToWidth = (text, width) => {
|
|
1698
|
-
const plainText = stripAnsi(text);
|
|
1699
|
-
if (plainText.length > width) {
|
|
1700
|
-
let count = 0;
|
|
1701
|
-
let cutIndex = 0;
|
|
1702
|
-
for (let i = 0; i < text.length && count < width - 3; i++) {
|
|
1703
|
-
if (text[i] === '\x1B') {
|
|
1704
|
-
while (i < text.length && text[i] !== 'm') i++;
|
|
1705
|
-
} else {
|
|
1706
|
-
count++;
|
|
1707
|
-
cutIndex = i + 1;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
return text.substring(0, cutIndex) + '...';
|
|
1711
|
-
}
|
|
1712
|
-
return text + ' '.repeat(width - plainText.length);
|
|
1713
|
-
};
|
|
1714
|
-
|
|
1715
|
-
// Logs inside the rectangle - newest first, max 30 lines
|
|
1716
|
-
const MAX_VISIBLE_LOGS = 50;
|
|
1717
|
-
|
|
1718
|
-
if (logs.length === 0) {
|
|
1719
|
-
const emptyLine = ' Waiting for activity...';
|
|
1720
|
-
bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
|
|
1721
|
-
for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
|
|
1722
|
-
bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
|
|
1723
|
-
}
|
|
1724
|
-
} else {
|
|
1725
|
-
const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
|
|
1726
|
-
reversedLogs.forEach(log => {
|
|
1727
|
-
const color = typeColors[log.type] || chalk.white;
|
|
1728
|
-
const icon = getIcon(log.type);
|
|
1729
|
-
const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
|
|
1730
|
-
const fitted = fitToWidth(logContent, W);
|
|
1731
|
-
bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
|
|
1732
|
-
});
|
|
1733
|
-
for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
|
|
1734
|
-
bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Bottom border
|
|
1739
|
-
bufferLine(chalk.cyan(BOT));
|
|
1740
|
-
|
|
1741
|
-
// Write entire buffer atomically
|
|
1742
|
-
process.stdout.write(screenBuffer);
|
|
1743
|
-
isDrawing = false;
|
|
1744
|
-
};
|
|
1745
|
-
|
|
1746
|
-
// Spinner interval for animation
|
|
1747
|
-
const spinnerInterval = setInterval(() => {
|
|
1748
|
-
if (isRunning) displayUI();
|
|
1749
|
-
}, 250);
|
|
1750
|
-
|
|
1751
|
-
addLog('info', 'Copy trading initialized');
|
|
1752
|
-
addLog('info', 'Connecting to HQX Server...');
|
|
1753
|
-
displayUI();
|
|
1754
|
-
|
|
1755
|
-
// Authenticate with HQX Server
|
|
1756
|
-
displayUI();
|
|
1757
|
-
|
|
1758
|
-
try {
|
|
1759
|
-
const authResult = await hqxServer.authenticate(
|
|
1760
|
-
lead.account.accountId.toString(),
|
|
1761
|
-
lead.account.propfirm || 'projectx'
|
|
1762
|
-
);
|
|
1763
|
-
|
|
1764
|
-
if (authResult.success) {
|
|
1765
|
-
const connectResult = await hqxServer.connect();
|
|
1766
|
-
if (connectResult.success) {
|
|
1767
|
-
hqxConnected = true;
|
|
1768
|
-
addLog('success', 'Connected to HQX Server');
|
|
1769
|
-
} else {
|
|
1770
|
-
addLog('warning', 'HQX Server unavailable - Running in monitor mode');
|
|
1771
|
-
}
|
|
1772
|
-
} else {
|
|
1773
|
-
addLog('warning', 'HQX Auth failed - Running in monitor mode');
|
|
1774
|
-
}
|
|
1775
|
-
} catch (error) {
|
|
1776
|
-
addLog('warning', 'HQX Server unavailable - Running in monitor mode');
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
displayUI();
|
|
1780
|
-
|
|
1781
|
-
// Helper function to execute signal on both accounts
|
|
1782
|
-
const executeSignalOnBothAccounts = async (signal) => {
|
|
1783
|
-
const side = signal.side === 'long' ? 0 : 1; // 0=Buy, 1=Sell
|
|
1784
|
-
const sideStr = signal.side === 'long' ? 'LONG' : 'SHORT';
|
|
1785
|
-
|
|
1786
|
-
// Execute on Lead account
|
|
1787
|
-
try {
|
|
1788
|
-
const leadResult = await lead.service.placeOrder({
|
|
1789
|
-
accountId: lead.account.rithmicAccountId || lead.account.accountId,
|
|
1790
|
-
symbol: lead.symbol.value,
|
|
1791
|
-
exchange: 'CME',
|
|
1792
|
-
size: lead.contracts,
|
|
1793
|
-
side: side,
|
|
1794
|
-
type: 2 // Market
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
|
-
if (leadResult.success) {
|
|
1798
|
-
stats.leadTrades++;
|
|
1799
|
-
addLog('trade', `Lead: ${sideStr} ${lead.contracts} ${lead.symbol.value} @ MKT`);
|
|
1800
|
-
} else {
|
|
1801
|
-
throw new Error(leadResult.error || 'Lead order failed');
|
|
1802
|
-
}
|
|
1803
|
-
} catch (e) {
|
|
1804
|
-
stats.errors++;
|
|
1805
|
-
addLog('error', `Lead order failed: ${e.message}`);
|
|
1806
|
-
return; // Don't copy if lead fails
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// Execute on Follower account (copy)
|
|
1810
|
-
try {
|
|
1811
|
-
const followerResult = await follower.service.placeOrder({
|
|
1812
|
-
accountId: follower.account.rithmicAccountId || follower.account.accountId,
|
|
1813
|
-
symbol: follower.symbol.value,
|
|
1814
|
-
exchange: 'CME',
|
|
1815
|
-
size: follower.contracts,
|
|
1816
|
-
side: side,
|
|
1817
|
-
type: 2 // Market
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
|
-
if (followerResult.success) {
|
|
1821
|
-
stats.copiedTrades++;
|
|
1822
|
-
addLog('copy', `Follower: ${sideStr} ${follower.contracts} ${follower.symbol.value} @ MKT`);
|
|
1823
|
-
} else {
|
|
1824
|
-
throw new Error(followerResult.error || 'Follower order failed');
|
|
1825
|
-
}
|
|
1826
|
-
} catch (e) {
|
|
1827
|
-
stats.errors++;
|
|
1828
|
-
addLog('error', `Follower order failed: ${e.message}`);
|
|
1829
|
-
}
|
|
1830
|
-
};
|
|
1831
|
-
|
|
1832
|
-
// Helper function to close positions on both accounts
|
|
1833
|
-
const closePositionsOnBothAccounts = async (reason) => {
|
|
1834
|
-
// Close Lead position
|
|
1835
|
-
try {
|
|
1836
|
-
await lead.service.closePosition(
|
|
1837
|
-
lead.account.rithmicAccountId || lead.account.accountId,
|
|
1838
|
-
lead.symbol.value
|
|
1839
|
-
);
|
|
1840
|
-
addLog('trade', `Lead: Position closed (${reason})`);
|
|
1841
|
-
} catch (e) {
|
|
1842
|
-
// Position may already be closed
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// Close Follower position
|
|
1846
|
-
try {
|
|
1847
|
-
await follower.service.closePosition(
|
|
1848
|
-
follower.account.rithmicAccountId || follower.account.accountId,
|
|
1849
|
-
follower.symbol.value
|
|
1850
|
-
);
|
|
1851
|
-
addLog('copy', `Follower: Position closed (${reason})`);
|
|
1852
|
-
} catch (e) {
|
|
1853
|
-
// Position may already be closed
|
|
1854
|
-
}
|
|
1855
|
-
};
|
|
1856
|
-
|
|
1857
|
-
// Setup HQX Server event handlers (attach before connection, check hqxConnected inside)
|
|
1858
|
-
hqxServer.on('latency', (data) => {
|
|
1859
|
-
latency = data.latency || 0;
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
hqxServer.on('log', (data) => {
|
|
1863
|
-
addLog(data.type || 'info', data.message);
|
|
1864
|
-
});
|
|
1865
|
-
|
|
1866
|
-
hqxServer.on('signal', async (data) => {
|
|
1867
|
-
stats.signals = (stats.signals || 0) + 1;
|
|
1868
|
-
const side = data.side === 'long' ? 'BUY' : 'SELL';
|
|
1869
|
-
addLog('signal', `${side} Signal @ ${data.entry?.toFixed(2) || 'N/A'} | SL: ${data.stop?.toFixed(2) || 'N/A'} | TP: ${data.target?.toFixed(2) || 'N/A'}`);
|
|
1870
|
-
|
|
1871
|
-
// Execute on both accounts
|
|
1872
|
-
if (hqxConnected) {
|
|
1873
|
-
await executeSignalOnBothAccounts(data);
|
|
1874
|
-
}
|
|
1875
|
-
displayUI();
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
hqxServer.on('trade', async (data) => {
|
|
1879
|
-
stats.pnl += data.pnl || 0;
|
|
1880
|
-
if (data.pnl > 0) {
|
|
1881
|
-
stats.wins = (stats.wins || 0) + 1;
|
|
1882
|
-
addLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
|
|
1883
|
-
} else {
|
|
1884
|
-
stats.losses = (stats.losses || 0) + 1;
|
|
1885
|
-
addLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
|
|
1886
|
-
}
|
|
1887
|
-
stats.trades = (stats.trades || 0) + 1;
|
|
1888
|
-
|
|
1889
|
-
// Print updated stats like One Account
|
|
1890
|
-
const statsType = stats.pnl >= 0 ? 'info' : 'loss';
|
|
1891
|
-
addLog(statsType, `Stats: Trades: ${stats.trades} | Wins: ${stats.wins || 0} | P&L: $${stats.pnl.toFixed(2)}`);
|
|
1892
|
-
|
|
1893
|
-
// Check daily target
|
|
1894
|
-
if (stats.pnl >= dailyTarget) {
|
|
1895
|
-
stopReason = 'target';
|
|
1896
|
-
addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
|
|
1897
|
-
isRunning = false;
|
|
1898
|
-
if (hqxConnected) hqxServer.stopAlgo();
|
|
1899
|
-
await closePositionsOnBothAccounts('target');
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
// Check max risk
|
|
1903
|
-
if (stats.pnl <= -maxRisk) {
|
|
1904
|
-
stopReason = 'risk';
|
|
1905
|
-
addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
1906
|
-
isRunning = false;
|
|
1907
|
-
if (hqxConnected) hqxServer.stopAlgo();
|
|
1908
|
-
await closePositionsOnBothAccounts('risk');
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
displayUI();
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
hqxServer.on('stats', (data) => {
|
|
1915
|
-
const realizedPnl = data.pnl || 0;
|
|
1916
|
-
const unrealizedPnl = data.position?.pnl || 0;
|
|
1917
|
-
stats.pnl = realizedPnl + unrealizedPnl;
|
|
1918
|
-
stats.trades = data.trades || stats.trades;
|
|
1919
|
-
stats.wins = data.wins || stats.wins;
|
|
1920
|
-
stats.losses = data.losses || stats.losses;
|
|
1921
|
-
});
|
|
1922
|
-
|
|
1923
|
-
hqxServer.on('error', (data) => {
|
|
1924
|
-
const errorMsg = data.message || 'Unknown error';
|
|
1925
|
-
addLog('error', errorMsg);
|
|
1926
|
-
|
|
1927
|
-
// If algo failed to start, switch to monitor mode
|
|
1928
|
-
if (errorMsg.includes('Failed to start') || errorMsg.includes('WebSocket failed') || errorMsg.includes('Échec')) {
|
|
1929
|
-
if (hqxConnected) {
|
|
1930
|
-
hqxConnected = false;
|
|
1931
|
-
addLog('warning', 'Switching to Monitor Mode (watching Lead positions)');
|
|
1932
|
-
displayUI();
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
});
|
|
1936
|
-
|
|
1937
|
-
hqxServer.on('disconnected', () => {
|
|
1938
|
-
hqxConnected = false;
|
|
1939
|
-
if (!stopReason) {
|
|
1940
|
-
addLog('warning', 'HQX Server disconnected - Switching to Monitor Mode');
|
|
1941
|
-
}
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
|
-
// Start algo if connected
|
|
1945
|
-
if (hqxConnected) {
|
|
1946
|
-
|
|
1947
|
-
// Start the Ultra-Scalping algo
|
|
1948
|
-
addLog('info', 'Starting HQX Ultra-Scalping...');
|
|
1949
|
-
addLog('info', `Target: $${dailyTarget.toFixed(2)} | Risk: $${maxRisk.toFixed(2)}`);
|
|
1950
|
-
|
|
1951
|
-
const propfirmToken = lead.service.getToken ? lead.service.getToken() : null;
|
|
1952
|
-
const propfirmId = lead.service.getPropfirm ? lead.service.getPropfirm() : (lead.account.propfirm || 'topstep');
|
|
1953
|
-
|
|
1954
|
-
// Get Rithmic credentials if this is a Rithmic account
|
|
1955
|
-
let rithmicCredentials = null;
|
|
1956
|
-
if (lead.service.getRithmicCredentials) {
|
|
1957
|
-
rithmicCredentials = lead.service.getRithmicCredentials();
|
|
1958
|
-
} else if (lead.account.rithmicUserId && lead.account.rithmicPassword) {
|
|
1959
|
-
rithmicCredentials = {
|
|
1960
|
-
userId: lead.account.rithmicUserId,
|
|
1961
|
-
password: lead.account.rithmicPassword,
|
|
1962
|
-
systemName: lead.account.rithmicSystem || 'Apex',
|
|
1963
|
-
gateway: lead.account.rithmicGateway || 'wss://rprotocol.rithmic.com:443'
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
hqxServer.startAlgo({
|
|
1968
|
-
accountId: lead.account.accountId,
|
|
1969
|
-
contractId: lead.symbol.id || lead.symbol.contractId,
|
|
1970
|
-
symbol: lead.symbol.value,
|
|
1971
|
-
contracts: lead.contracts,
|
|
1972
|
-
dailyTarget: dailyTarget,
|
|
1973
|
-
maxRisk: maxRisk,
|
|
1974
|
-
propfirm: propfirmId,
|
|
1975
|
-
propfirmToken: propfirmToken,
|
|
1976
|
-
rithmicCredentials: rithmicCredentials,
|
|
1977
|
-
copyTrading: true, // Flag for copy trading mode
|
|
1978
|
-
followerSymbol: follower.symbol.value,
|
|
1979
|
-
followerContracts: follower.contracts
|
|
1980
|
-
});
|
|
1981
|
-
|
|
1982
|
-
displayUI();
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// Position monitoring loop (for P&L tracking and fallback copy)
|
|
1986
|
-
const monitorInterval = setInterval(async () => {
|
|
1987
|
-
if (!isRunning) return;
|
|
1988
|
-
|
|
1989
|
-
try {
|
|
1990
|
-
// Get positions from both accounts for P&L tracking
|
|
1991
|
-
const [leadPositions, followerPositions] = await Promise.all([
|
|
1992
|
-
lead.service.getPositions(lead.account.rithmicAccountId || lead.account.accountId),
|
|
1993
|
-
follower.service.getPositions(follower.account.rithmicAccountId || follower.account.accountId)
|
|
1994
|
-
]);
|
|
1995
|
-
|
|
1996
|
-
// Calculate combined P&L
|
|
1997
|
-
let leadPnl = 0, followerPnl = 0;
|
|
1998
|
-
|
|
1999
|
-
if (leadPositions.success && leadPositions.positions) {
|
|
2000
|
-
const leadPos = leadPositions.positions.find(p =>
|
|
2001
|
-
p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
|
|
2002
|
-
);
|
|
2003
|
-
if (leadPos && typeof leadPos.unrealizedPnl === 'number') {
|
|
2004
|
-
leadPnl = leadPos.unrealizedPnl;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
if (followerPositions.success && followerPositions.positions) {
|
|
2009
|
-
const followerPos = followerPositions.positions.find(p =>
|
|
2010
|
-
p.symbol === follower.symbol.value || p.symbol?.includes(follower.symbol.searchText)
|
|
2011
|
-
);
|
|
2012
|
-
if (followerPos && typeof followerPos.unrealizedPnl === 'number') {
|
|
2013
|
-
followerPnl = followerPos.unrealizedPnl;
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// Update combined P&L (or just follower if HQX handles lead)
|
|
2018
|
-
stats.pnl = leadPnl + followerPnl;
|
|
2019
|
-
|
|
2020
|
-
// Check if daily target reached
|
|
2021
|
-
if (stats.pnl >= dailyTarget && !stopReason) {
|
|
2022
|
-
isRunning = false;
|
|
2023
|
-
stopReason = 'target';
|
|
2024
|
-
addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
|
|
2025
|
-
|
|
2026
|
-
if (hqxConnected) hqxServer.stopAlgo();
|
|
2027
|
-
await closePositionsOnBothAccounts('target');
|
|
2028
|
-
displayUI();
|
|
2029
|
-
return;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// Check if max risk reached
|
|
2033
|
-
if (stats.pnl <= -maxRisk && !stopReason) {
|
|
2034
|
-
isRunning = false;
|
|
2035
|
-
stopReason = 'risk';
|
|
2036
|
-
addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
2037
|
-
|
|
2038
|
-
if (hqxConnected) hqxServer.stopAlgo();
|
|
2039
|
-
await closePositionsOnBothAccounts('risk');
|
|
2040
|
-
displayUI();
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
// Fallback: If HQX not connected, monitor lead and copy manually
|
|
2045
|
-
if (!hqxConnected) {
|
|
2046
|
-
let currentLeadPosition = null;
|
|
2047
|
-
if (leadPositions.success && leadPositions.positions) {
|
|
2048
|
-
currentLeadPosition = leadPositions.positions.find(p =>
|
|
2049
|
-
p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
|
|
2050
|
-
);
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
const hadPosition = lastLeadPosition && lastLeadPosition.quantity !== 0;
|
|
2054
|
-
const hasPosition = currentLeadPosition && currentLeadPosition.quantity !== 0;
|
|
2055
|
-
|
|
2056
|
-
if (!hadPosition && hasPosition) {
|
|
2057
|
-
stats.leadTrades++;
|
|
2058
|
-
const side = currentLeadPosition.quantity > 0 ? 'LONG' : 'SHORT';
|
|
2059
|
-
addLog('trade', `Lead opened ${side} ${Math.abs(currentLeadPosition.quantity)} @ ${currentLeadPosition.averagePrice || 'MKT'}`);
|
|
2060
|
-
await copyTradeToFollower(follower, currentLeadPosition, 'open');
|
|
2061
|
-
stats.copiedTrades++;
|
|
2062
|
-
displayUI();
|
|
2063
|
-
|
|
2064
|
-
} else if (hadPosition && !hasPosition) {
|
|
2065
|
-
addLog('trade', `Lead closed position`);
|
|
2066
|
-
await copyTradeToFollower(follower, lastLeadPosition, 'close');
|
|
2067
|
-
stats.copiedTrades++;
|
|
2068
|
-
displayUI();
|
|
2069
|
-
|
|
2070
|
-
} else if (hadPosition && hasPosition && lastLeadPosition.quantity !== currentLeadPosition.quantity) {
|
|
2071
|
-
const diff = currentLeadPosition.quantity - lastLeadPosition.quantity;
|
|
2072
|
-
const action = diff > 0 ? 'added' : 'reduced';
|
|
2073
|
-
addLog('trade', `Lead ${action} ${Math.abs(diff)} contracts`);
|
|
2074
|
-
await copyTradeToFollower(follower, { ...currentLeadPosition, quantityChange: diff }, 'adjust');
|
|
2075
|
-
stats.copiedTrades++;
|
|
2076
|
-
displayUI();
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
lastLeadPosition = currentLeadPosition ? { ...currentLeadPosition } : null;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
} catch (error) {
|
|
2083
|
-
stats.errors++;
|
|
2084
|
-
addLog('error', `Monitor error: ${error.message}`);
|
|
2085
|
-
displayUI();
|
|
2086
|
-
}
|
|
2087
|
-
}, 2000); // Check every 2 seconds
|
|
2088
|
-
|
|
2089
|
-
// Wait for X key OR auto-stop (target/risk reached)
|
|
2090
|
-
await new Promise((resolve) => {
|
|
2091
|
-
// Check for auto-stop every 500ms
|
|
2092
|
-
const checkInterval = setInterval(() => {
|
|
2093
|
-
if (!isRunning || stopReason) {
|
|
2094
|
-
clearInterval(checkInterval);
|
|
2095
|
-
clearInterval(monitorInterval);
|
|
2096
|
-
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
2097
|
-
process.stdin.setRawMode(false);
|
|
2098
|
-
}
|
|
2099
|
-
resolve();
|
|
2100
|
-
}
|
|
2101
|
-
}, 500);
|
|
2102
|
-
|
|
2103
|
-
// Also listen for X key
|
|
2104
|
-
if (process.stdin.isTTY) {
|
|
2105
|
-
try {
|
|
2106
|
-
readline.emitKeypressEvents(process.stdin);
|
|
2107
|
-
process.stdin.setRawMode(true);
|
|
2108
|
-
process.stdin.resume();
|
|
2109
|
-
|
|
2110
|
-
const onKeypress = (str, key) => {
|
|
2111
|
-
if (!key) return;
|
|
2112
|
-
const keyName = key.name?.toLowerCase();
|
|
2113
|
-
if (keyName === 'x' || (key.ctrl && keyName === 'c')) {
|
|
2114
|
-
clearInterval(checkInterval);
|
|
2115
|
-
clearInterval(monitorInterval);
|
|
2116
|
-
process.stdin.setRawMode(false);
|
|
2117
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
2118
|
-
resolve();
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2121
|
-
|
|
2122
|
-
process.stdin.on('keypress', onKeypress);
|
|
2123
|
-
} catch (e) {
|
|
2124
|
-
// Fallback: just wait for auto-stop
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
});
|
|
2128
|
-
|
|
2129
|
-
// Cleanup
|
|
2130
|
-
clearInterval(spinnerInterval);
|
|
2131
|
-
isRunning = false;
|
|
2132
|
-
|
|
2133
|
-
// Stop HQX Server and close positions
|
|
2134
|
-
if (hqxConnected) {
|
|
2135
|
-
hqxServer.stopAlgo();
|
|
2136
|
-
hqxServer.disconnect();
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
// Cancel all pending orders and close positions on both accounts
|
|
2140
|
-
try {
|
|
2141
|
-
await Promise.all([
|
|
2142
|
-
lead.service.cancelAllOrders(lead.account.rithmicAccountId || lead.account.accountId),
|
|
2143
|
-
follower.service.cancelAllOrders(follower.account.rithmicAccountId || follower.account.accountId)
|
|
2144
|
-
]);
|
|
2145
|
-
} catch (e) {
|
|
2146
|
-
// Ignore cancel errors
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
if (!stopReason) {
|
|
2150
|
-
// User stopped manually, close positions
|
|
2151
|
-
await closePositionsOnBothAccounts('user_stop');
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
// Restore stdin to normal mode
|
|
2155
|
-
try {
|
|
2156
|
-
if (process.stdin.isTTY) {
|
|
2157
|
-
process.stdin.setRawMode(false);
|
|
2158
|
-
}
|
|
2159
|
-
process.stdin.removeAllListeners('keypress');
|
|
2160
|
-
} catch (e) {
|
|
2161
|
-
// Ignore stdin restoration errors
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
// Exit alternate screen buffer and show cursor
|
|
2165
|
-
process.stdout.write('\x1B[?1049l');
|
|
2166
|
-
process.stdout.write('\x1B[?25h');
|
|
2167
|
-
|
|
2168
|
-
console.log();
|
|
2169
|
-
if (stopReason === 'target') {
|
|
2170
|
-
console.log(chalk.green.bold(' [OK] Daily target reached! Copy trading stopped.'));
|
|
2171
|
-
} else if (stopReason === 'risk') {
|
|
2172
|
-
console.log(chalk.red.bold(' [X] Max risk reached! Copy trading stopped.'));
|
|
2173
|
-
} else {
|
|
2174
|
-
console.log(chalk.yellow(' [OK] Copy trading stopped by user'));
|
|
2175
|
-
}
|
|
2176
|
-
console.log();
|
|
2177
|
-
console.log(chalk.gray(getSeparator()));
|
|
2178
|
-
console.log(chalk.white.bold(' Session Summary'));
|
|
2179
|
-
console.log(chalk.gray(getSeparator()));
|
|
2180
|
-
console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
|
|
2181
|
-
console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
|
|
2182
|
-
console.log(chalk.white(` Final P&L: ${stats.pnl >= 0 ? chalk.green('+$' + stats.pnl.toFixed(2)) : chalk.red('-$' + Math.abs(stats.pnl).toFixed(2))}`));
|
|
2183
|
-
console.log(chalk.gray(getSeparator()));
|
|
2184
|
-
console.log(chalk.white(` Lead Trades: ${chalk.cyan(stats.leadTrades)}`));
|
|
2185
|
-
console.log(chalk.white(` Copied Trades: ${chalk.green(stats.copiedTrades)}`));
|
|
2186
|
-
console.log(chalk.white(` Errors: ${chalk.red(stats.errors)}`));
|
|
2187
|
-
console.log(chalk.gray(getSeparator()));
|
|
2188
|
-
console.log();
|
|
2189
|
-
|
|
2190
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
2191
|
-
};
|
|
2192
|
-
|
|
2193
|
-
/**
|
|
2194
|
-
* Copy trade to follower account
|
|
2195
|
-
*/
|
|
2196
|
-
const copyTradeToFollower = async (follower, position, action) => {
|
|
2197
|
-
try {
|
|
2198
|
-
const service = follower.service;
|
|
2199
|
-
const accountId = follower.account.rithmicAccountId || follower.account.accountId;
|
|
2200
|
-
|
|
2201
|
-
if (action === 'open') {
|
|
2202
|
-
// Open new position
|
|
2203
|
-
const side = position.quantity > 0 ? 0 : 1; // 0=Buy, 1=Sell
|
|
2204
|
-
const result = await service.placeOrder({
|
|
2205
|
-
accountId: accountId,
|
|
2206
|
-
symbol: follower.symbol.value,
|
|
2207
|
-
exchange: 'CME',
|
|
2208
|
-
size: follower.contracts,
|
|
2209
|
-
side: side,
|
|
2210
|
-
type: 2 // Market
|
|
2211
|
-
});
|
|
2212
|
-
|
|
2213
|
-
if (result.success) {
|
|
2214
|
-
console.log(chalk.green(` [+] Follower: Opened ${side === 0 ? 'LONG' : 'SHORT'} ${follower.contracts} ${follower.symbol.value}`));
|
|
2215
|
-
} else {
|
|
2216
|
-
throw new Error(result.error || 'Order failed');
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
} else if (action === 'close') {
|
|
2220
|
-
// Close position
|
|
2221
|
-
const result = await service.closePosition(accountId, follower.symbol.value);
|
|
2222
|
-
|
|
2223
|
-
if (result.success) {
|
|
2224
|
-
console.log(chalk.green(` [+] Follower: Closed position`));
|
|
2225
|
-
} else {
|
|
2226
|
-
throw new Error(result.error || 'Close failed');
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
} else if (action === 'adjust') {
|
|
2230
|
-
// Adjust position size
|
|
2231
|
-
const side = position.quantityChange > 0 ? 0 : 1;
|
|
2232
|
-
const size = Math.abs(position.quantityChange);
|
|
2233
|
-
const adjustedSize = Math.round(size * (follower.contracts / Math.abs(position.quantity - position.quantityChange)));
|
|
2234
|
-
|
|
2235
|
-
if (adjustedSize > 0) {
|
|
2236
|
-
const result = await service.placeOrder({
|
|
2237
|
-
accountId: accountId,
|
|
2238
|
-
symbol: follower.symbol.value,
|
|
2239
|
-
exchange: 'CME',
|
|
2240
|
-
size: adjustedSize,
|
|
2241
|
-
side: side,
|
|
2242
|
-
type: 2
|
|
2243
|
-
});
|
|
2244
|
-
|
|
2245
|
-
if (result.success) {
|
|
2246
|
-
console.log(chalk.green(` [+] Follower: Adjusted by ${side === 0 ? '+' : '-'}${adjustedSize}`));
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
} catch (error) {
|
|
2252
|
-
console.log(chalk.red(` [X] Follower error: ${error.message}`));
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
|
|
2256
|
-
/**
|
|
2257
|
-
* Wait for X key to stop
|
|
2258
|
-
*/
|
|
2259
|
-
const waitForStopKey = () => {
|
|
2260
|
-
return new Promise((resolve) => {
|
|
2261
|
-
// Enable raw mode to capture keypresses
|
|
2262
|
-
if (process.stdin.isTTY) {
|
|
2263
|
-
readline.emitKeypressEvents(process.stdin);
|
|
2264
|
-
process.stdin.setRawMode(true);
|
|
2265
|
-
|
|
2266
|
-
const onKeypress = (str, key) => {
|
|
2267
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
2268
|
-
process.stdin.setRawMode(false);
|
|
2269
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
2270
|
-
resolve();
|
|
2271
|
-
}
|
|
2272
|
-
};
|
|
2273
|
-
|
|
2274
|
-
process.stdin.on('keypress', onKeypress);
|
|
2275
|
-
} else {
|
|
2276
|
-
// Fallback: wait 30 seconds in non-TTY mode
|
|
2277
|
-
setTimeout(resolve, 30000);
|
|
2278
|
-
}
|
|
2279
|
-
});
|
|
2280
|
-
};
|
|
6
|
+
const { algoTradingMenu } = require('./algo/index');
|
|
2281
7
|
|
|
2282
8
|
module.exports = { algoTradingMenu };
|