hedgequantx 1.2.145 → 1.2.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.js +10 -597
- package/src/menus/connect.js +402 -0
- package/src/menus/dashboard.js +214 -0
- package/src/menus/index.js +8 -0
- 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 +271 -0
- package/src/pages/algo.js +3 -2277
- package/src/services/hqx-server.js +27 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy Trading Mode - Mirror trades from Lead to Follower
|
|
3
|
+
* Lightweight - UI + HQX Server handles all execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
const { connections } = require('../../services');
|
|
12
|
+
const { HQXServerService } = require('../../services/hqx-server');
|
|
13
|
+
const { FUTURES_SYMBOLS } = require('../../config');
|
|
14
|
+
const { AlgoUI } = require('./ui');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copy Trading Menu
|
|
18
|
+
*/
|
|
19
|
+
const copyTradingMenu = async () => {
|
|
20
|
+
const allConns = connections.getAll();
|
|
21
|
+
|
|
22
|
+
if (allConns.length < 2) {
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.yellow(' Copy Trading requires 2 connected accounts'));
|
|
25
|
+
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
26
|
+
console.log();
|
|
27
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(chalk.magenta.bold(' Copy Trading Setup'));
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
// Get all active accounts from all connections
|
|
36
|
+
const allAccounts = [];
|
|
37
|
+
for (const conn of allConns) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await conn.service.getTradingAccounts();
|
|
40
|
+
if (result.success && result.accounts) {
|
|
41
|
+
const active = result.accounts.filter(a => a.status === 0);
|
|
42
|
+
for (const acc of active) {
|
|
43
|
+
allAccounts.push({
|
|
44
|
+
account: acc,
|
|
45
|
+
service: conn.service,
|
|
46
|
+
propfirm: conn.propfirm,
|
|
47
|
+
type: conn.type
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) { /* ignore */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (allAccounts.length < 2) {
|
|
55
|
+
console.log(chalk.yellow(' Need at least 2 active accounts'));
|
|
56
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 1: Select Lead Account
|
|
61
|
+
console.log(chalk.cyan(' Step 1: Select LEAD Account (source of trades)'));
|
|
62
|
+
const leadChoices = allAccounts.map((a, i) => ({
|
|
63
|
+
name: `${a.propfirm} - ${a.account.accountName || a.account.accountId} ($${a.account.balance.toLocaleString()})`,
|
|
64
|
+
value: i
|
|
65
|
+
}));
|
|
66
|
+
leadChoices.push({ name: chalk.yellow('< Cancel'), value: -1 });
|
|
67
|
+
|
|
68
|
+
const { leadIdx } = await inquirer.prompt([{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'leadIdx',
|
|
71
|
+
message: 'Lead Account:',
|
|
72
|
+
choices: leadChoices
|
|
73
|
+
}]);
|
|
74
|
+
|
|
75
|
+
if (leadIdx === -1) return;
|
|
76
|
+
const lead = allAccounts[leadIdx];
|
|
77
|
+
|
|
78
|
+
// Step 2: Select Follower Account
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account (copies trades)'));
|
|
81
|
+
const followerChoices = allAccounts
|
|
82
|
+
.map((a, i) => ({ a, i }))
|
|
83
|
+
.filter(x => x.i !== leadIdx)
|
|
84
|
+
.map(x => ({
|
|
85
|
+
name: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId} ($${x.a.account.balance.toLocaleString()})`,
|
|
86
|
+
value: x.i
|
|
87
|
+
}));
|
|
88
|
+
followerChoices.push({ name: chalk.yellow('< Cancel'), value: -1 });
|
|
89
|
+
|
|
90
|
+
const { followerIdx } = await inquirer.prompt([{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'followerIdx',
|
|
93
|
+
message: 'Follower Account:',
|
|
94
|
+
choices: followerChoices
|
|
95
|
+
}]);
|
|
96
|
+
|
|
97
|
+
if (followerIdx === -1) return;
|
|
98
|
+
const follower = allAccounts[followerIdx];
|
|
99
|
+
|
|
100
|
+
// Step 3: Select Symbol for Lead
|
|
101
|
+
console.log();
|
|
102
|
+
console.log(chalk.cyan(' Step 3: Select Symbol for LEAD'));
|
|
103
|
+
const leadSymbol = await selectSymbol(lead.service, 'Lead');
|
|
104
|
+
if (!leadSymbol) return;
|
|
105
|
+
|
|
106
|
+
// Step 4: Select Symbol for Follower
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(chalk.cyan(' Step 4: Select Symbol for FOLLOWER'));
|
|
109
|
+
const followerSymbol = await selectSymbol(follower.service, 'Follower');
|
|
110
|
+
if (!followerSymbol) return;
|
|
111
|
+
|
|
112
|
+
// Step 5: Configure parameters
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk.cyan(' Step 5: Configure Parameters'));
|
|
115
|
+
|
|
116
|
+
const { leadContracts } = await inquirer.prompt([{
|
|
117
|
+
type: 'number',
|
|
118
|
+
name: 'leadContracts',
|
|
119
|
+
message: 'Lead contracts:',
|
|
120
|
+
default: 1
|
|
121
|
+
}]);
|
|
122
|
+
|
|
123
|
+
const { followerContracts } = await inquirer.prompt([{
|
|
124
|
+
type: 'number',
|
|
125
|
+
name: 'followerContracts',
|
|
126
|
+
message: 'Follower contracts:',
|
|
127
|
+
default: leadContracts
|
|
128
|
+
}]);
|
|
129
|
+
|
|
130
|
+
const { dailyTarget } = await inquirer.prompt([{
|
|
131
|
+
type: 'number',
|
|
132
|
+
name: 'dailyTarget',
|
|
133
|
+
message: 'Daily target ($):',
|
|
134
|
+
default: 400
|
|
135
|
+
}]);
|
|
136
|
+
|
|
137
|
+
const { maxRisk } = await inquirer.prompt([{
|
|
138
|
+
type: 'number',
|
|
139
|
+
name: 'maxRisk',
|
|
140
|
+
message: 'Max risk ($):',
|
|
141
|
+
default: 200
|
|
142
|
+
}]);
|
|
143
|
+
|
|
144
|
+
// Step 6: Privacy
|
|
145
|
+
const { showNames } = await inquirer.prompt([{
|
|
146
|
+
type: 'confirm',
|
|
147
|
+
name: 'showNames',
|
|
148
|
+
message: 'Show account names?',
|
|
149
|
+
default: false
|
|
150
|
+
}]);
|
|
151
|
+
|
|
152
|
+
// Confirm
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(chalk.white(' Summary:'));
|
|
155
|
+
console.log(chalk.gray(` Lead: ${lead.propfirm} -> ${leadSymbol.name} x${leadContracts}`));
|
|
156
|
+
console.log(chalk.gray(` Follower: ${follower.propfirm} -> ${followerSymbol.name} x${followerContracts}`));
|
|
157
|
+
console.log(chalk.gray(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
const { confirm } = await inquirer.prompt([{
|
|
161
|
+
type: 'confirm',
|
|
162
|
+
name: 'confirm',
|
|
163
|
+
message: chalk.yellow('Start Copy Trading?'),
|
|
164
|
+
default: true
|
|
165
|
+
}]);
|
|
166
|
+
|
|
167
|
+
if (!confirm) return;
|
|
168
|
+
|
|
169
|
+
// Launch
|
|
170
|
+
await launchCopyTrading({
|
|
171
|
+
lead: { ...lead, symbol: leadSymbol, contracts: leadContracts },
|
|
172
|
+
follower: { ...follower, symbol: followerSymbol, contracts: followerContracts },
|
|
173
|
+
dailyTarget,
|
|
174
|
+
maxRisk,
|
|
175
|
+
showNames
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Symbol selection helper
|
|
181
|
+
*/
|
|
182
|
+
const selectSymbol = async (service, label) => {
|
|
183
|
+
try {
|
|
184
|
+
const result = await service.getContracts();
|
|
185
|
+
if (!result.success) return null;
|
|
186
|
+
|
|
187
|
+
const choices = [];
|
|
188
|
+
const cats = {};
|
|
189
|
+
|
|
190
|
+
for (const c of result.contracts) {
|
|
191
|
+
const cat = c.group || 'Other';
|
|
192
|
+
if (!cats[cat]) cats[cat] = [];
|
|
193
|
+
cats[cat].push(c);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [cat, list] of Object.entries(cats)) {
|
|
197
|
+
choices.push(new inquirer.Separator(chalk.gray(`--- ${cat} ---`)));
|
|
198
|
+
for (const c of list.slice(0, 8)) {
|
|
199
|
+
choices.push({ name: c.name || c.symbol, value: c });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
choices.push(new inquirer.Separator());
|
|
203
|
+
choices.push({ name: chalk.yellow('< Cancel'), value: null });
|
|
204
|
+
|
|
205
|
+
const { symbol } = await inquirer.prompt([{
|
|
206
|
+
type: 'list',
|
|
207
|
+
name: 'symbol',
|
|
208
|
+
message: `${label} Symbol:`,
|
|
209
|
+
choices,
|
|
210
|
+
pageSize: 15
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
return symbol;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Launch Copy Trading
|
|
221
|
+
*/
|
|
222
|
+
const launchCopyTrading = async (config) => {
|
|
223
|
+
const { lead, follower, dailyTarget, maxRisk, showNames } = config;
|
|
224
|
+
|
|
225
|
+
const leadName = showNames ? (lead.account.accountName || lead.account.accountId) : 'HQX Lead *****';
|
|
226
|
+
const followerName = showNames ? (follower.account.accountName || follower.account.accountId) : 'HQX Follower *****';
|
|
227
|
+
|
|
228
|
+
// UI with copy trading subtitle
|
|
229
|
+
const ui = new AlgoUI({ subtitle: 'HQX Copy Trading' });
|
|
230
|
+
|
|
231
|
+
// Combined stats
|
|
232
|
+
const stats = {
|
|
233
|
+
accountName: `${leadName} -> ${followerName}`,
|
|
234
|
+
symbol: `${lead.symbol.name} / ${follower.symbol.name}`,
|
|
235
|
+
contracts: `${lead.contracts}/${follower.contracts}`,
|
|
236
|
+
target: dailyTarget,
|
|
237
|
+
risk: maxRisk,
|
|
238
|
+
pnl: 0,
|
|
239
|
+
trades: 0,
|
|
240
|
+
wins: 0,
|
|
241
|
+
losses: 0,
|
|
242
|
+
latency: 0,
|
|
243
|
+
connected: false
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let running = true;
|
|
247
|
+
let stopReason = null;
|
|
248
|
+
|
|
249
|
+
// Connect to HQX Server
|
|
250
|
+
const hqx = new HQXServerService();
|
|
251
|
+
|
|
252
|
+
const spinner = ora('Connecting to HQX Server...').start();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const auth = await hqx.authenticate(lead.account.accountId.toString(), lead.propfirm || 'topstep');
|
|
256
|
+
if (!auth.success) throw new Error(auth.error);
|
|
257
|
+
|
|
258
|
+
const conn = await hqx.connect();
|
|
259
|
+
if (!conn.success) throw new Error('WebSocket failed');
|
|
260
|
+
|
|
261
|
+
spinner.succeed('Connected');
|
|
262
|
+
stats.connected = true;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
spinner.warn('HQX Server unavailable');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Event handlers
|
|
268
|
+
hqx.on('latency', (d) => { stats.latency = d.latency || 0; });
|
|
269
|
+
|
|
270
|
+
hqx.on('log', (d) => {
|
|
271
|
+
let msg = d.message;
|
|
272
|
+
if (!showNames) {
|
|
273
|
+
if (lead.account.accountName) msg = msg.replace(new RegExp(lead.account.accountName, 'gi'), 'Lead *****');
|
|
274
|
+
if (follower.account.accountName) msg = msg.replace(new RegExp(follower.account.accountName, 'gi'), 'Follower *****');
|
|
275
|
+
}
|
|
276
|
+
ui.addLog(d.type || 'info', msg);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
hqx.on('trade', (d) => {
|
|
280
|
+
stats.trades++;
|
|
281
|
+
stats.pnl += d.pnl || 0;
|
|
282
|
+
d.pnl >= 0 ? stats.wins++ : stats.losses++;
|
|
283
|
+
ui.addLog(d.pnl >= 0 ? 'trade' : 'loss', `${d.pnl >= 0 ? '+' : ''}$${d.pnl.toFixed(2)}`);
|
|
284
|
+
|
|
285
|
+
if (stats.pnl >= dailyTarget) {
|
|
286
|
+
stopReason = 'target';
|
|
287
|
+
running = false;
|
|
288
|
+
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
289
|
+
hqx.stopAlgo();
|
|
290
|
+
} else if (stats.pnl <= -maxRisk) {
|
|
291
|
+
stopReason = 'risk';
|
|
292
|
+
running = false;
|
|
293
|
+
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
294
|
+
hqx.stopAlgo();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
hqx.on('copy', (d) => {
|
|
299
|
+
ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x to Follower`);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
hqx.on('error', (d) => { ui.addLog('error', d.message); });
|
|
303
|
+
hqx.on('disconnected', () => { stats.connected = false; });
|
|
304
|
+
|
|
305
|
+
// Start copy trading on server
|
|
306
|
+
if (stats.connected) {
|
|
307
|
+
ui.addLog('info', 'Starting Copy Trading...');
|
|
308
|
+
|
|
309
|
+
// Get credentials
|
|
310
|
+
let leadCreds = null, followerCreds = null;
|
|
311
|
+
|
|
312
|
+
if (lead.service.getRithmicCredentials) {
|
|
313
|
+
leadCreds = lead.service.getRithmicCredentials();
|
|
314
|
+
}
|
|
315
|
+
if (follower.service.getRithmicCredentials) {
|
|
316
|
+
followerCreds = follower.service.getRithmicCredentials();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
hqx.startCopyTrading({
|
|
320
|
+
// Lead config
|
|
321
|
+
leadAccountId: lead.account.accountId,
|
|
322
|
+
leadContractId: lead.symbol.id || lead.symbol.contractId,
|
|
323
|
+
leadSymbol: lead.symbol.symbol || lead.symbol.name,
|
|
324
|
+
leadContracts: lead.contracts,
|
|
325
|
+
leadPropfirm: lead.propfirm,
|
|
326
|
+
leadToken: lead.service.getToken ? lead.service.getToken() : null,
|
|
327
|
+
leadRithmicCredentials: leadCreds,
|
|
328
|
+
|
|
329
|
+
// Follower config
|
|
330
|
+
followerAccountId: follower.account.accountId,
|
|
331
|
+
followerContractId: follower.symbol.id || follower.symbol.contractId,
|
|
332
|
+
followerSymbol: follower.symbol.symbol || follower.symbol.name,
|
|
333
|
+
followerContracts: follower.contracts,
|
|
334
|
+
followerPropfirm: follower.propfirm,
|
|
335
|
+
followerToken: follower.service.getToken ? follower.service.getToken() : null,
|
|
336
|
+
followerRithmicCredentials: followerCreds,
|
|
337
|
+
|
|
338
|
+
// Targets
|
|
339
|
+
dailyTarget,
|
|
340
|
+
maxRisk
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// UI refresh
|
|
345
|
+
const refreshInterval = setInterval(() => {
|
|
346
|
+
if (running) ui.render(stats);
|
|
347
|
+
}, 250);
|
|
348
|
+
|
|
349
|
+
// Keyboard
|
|
350
|
+
const setupKeys = () => {
|
|
351
|
+
if (!process.stdin.isTTY) return null;
|
|
352
|
+
readline.emitKeypressEvents(process.stdin);
|
|
353
|
+
process.stdin.setRawMode(true);
|
|
354
|
+
process.stdin.resume();
|
|
355
|
+
|
|
356
|
+
const handler = (str, key) => {
|
|
357
|
+
if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
|
|
358
|
+
running = false;
|
|
359
|
+
stopReason = 'manual';
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
process.stdin.on('keypress', handler);
|
|
363
|
+
|
|
364
|
+
return () => {
|
|
365
|
+
process.stdin.removeListener('keypress', handler);
|
|
366
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const cleanupKeys = setupKeys();
|
|
371
|
+
|
|
372
|
+
// Wait
|
|
373
|
+
await new Promise(resolve => {
|
|
374
|
+
const check = setInterval(() => {
|
|
375
|
+
if (!running) { clearInterval(check); resolve(); }
|
|
376
|
+
}, 100);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Cleanup
|
|
380
|
+
clearInterval(refreshInterval);
|
|
381
|
+
if (cleanupKeys) cleanupKeys();
|
|
382
|
+
|
|
383
|
+
if (stats.connected) {
|
|
384
|
+
hqx.stopAlgo();
|
|
385
|
+
hqx.disconnect();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ui.cleanup();
|
|
389
|
+
|
|
390
|
+
// Summary
|
|
391
|
+
console.clear();
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(chalk.cyan(' === Copy Trading Summary ==='));
|
|
394
|
+
console.log();
|
|
395
|
+
console.log(chalk.white(` Stop: ${stopReason || 'unknown'}`));
|
|
396
|
+
console.log(chalk.white(` Trades: ${stats.trades} (W: ${stats.wins} / L: ${stats.losses})`));
|
|
397
|
+
const c = stats.pnl >= 0 ? chalk.green : chalk.red;
|
|
398
|
+
console.log(c(` P&L: ${stats.pnl >= 0 ? '+' : ''}$${stats.pnl.toFixed(2)}`));
|
|
399
|
+
console.log();
|
|
400
|
+
|
|
401
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
module.exports = { copyTradingMenu };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algo Trading - Main Menu
|
|
3
|
+
* Lightweight entry point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const { getSeparator } = require('../../ui');
|
|
9
|
+
|
|
10
|
+
const { oneAccountMenu } = require('./one-account');
|
|
11
|
+
const { copyTradingMenu } = require('./copy-trading');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Algo Trading Menu
|
|
15
|
+
*/
|
|
16
|
+
const algoTradingMenu = async (service) => {
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(chalk.gray(getSeparator()));
|
|
19
|
+
console.log(chalk.magenta.bold(' Algo-Trading'));
|
|
20
|
+
console.log(chalk.gray(getSeparator()));
|
|
21
|
+
console.log();
|
|
22
|
+
|
|
23
|
+
const { action } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'action',
|
|
27
|
+
message: chalk.white.bold('Select Mode:'),
|
|
28
|
+
choices: [
|
|
29
|
+
{ name: chalk.cyan('One Account'), value: 'one_account' },
|
|
30
|
+
{ name: chalk.green('Copy Trading'), value: 'copy_trading' },
|
|
31
|
+
new inquirer.Separator(),
|
|
32
|
+
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
33
|
+
],
|
|
34
|
+
pageSize: 10,
|
|
35
|
+
loop: false
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
switch (action) {
|
|
40
|
+
case 'one_account':
|
|
41
|
+
await oneAccountMenu(service);
|
|
42
|
+
break;
|
|
43
|
+
case 'copy_trading':
|
|
44
|
+
await copyTradingMenu();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return action;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { algoTradingMenu };
|