hedgequantx 1.2.144 → 1.2.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.2.144",
3
+ "version": "1.2.146",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -689,13 +689,56 @@ const dashboardMenu = async (service) => {
689
689
  console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
690
690
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
691
691
 
692
- // Connection info - show all active connections
692
+ // Connection info - show all active connections in boxes (max 3 per row)
693
693
  const allConns = connections.getAll();
694
694
  if (allConns.length > 0) {
695
- allConns.forEach(c => {
696
- const connText = c.propfirm || c.type || 'Connected';
697
- console.log(chalk.cyan('║') + chalk.green(padLine(` ${connText}`, W)) + chalk.cyan('║'));
698
- });
695
+ const maxPerRow = 3;
696
+ const boxPadding = 2; // padding inside each mini-box
697
+ const gap = 2; // gap between boxes
698
+
699
+ // Calculate box width based on number of connections (max 3)
700
+ const numBoxes = Math.min(allConns.length, maxPerRow);
701
+ const totalGaps = (numBoxes - 1) * gap;
702
+ const boxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
703
+
704
+ // Process connections in rows of 3
705
+ for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
706
+ const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
707
+ const numInRow = rowConns.length;
708
+ const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
709
+
710
+ // Top border of boxes
711
+ let topLine = ' ';
712
+ for (let i = 0; i < numInRow; i++) {
713
+ topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
714
+ if (i < numInRow - 1) topLine += ' '.repeat(gap);
715
+ }
716
+ const topPad = W - topLine.length;
717
+ console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
718
+
719
+ // Content of boxes
720
+ let contentLine = ' ';
721
+ for (let i = 0; i < numInRow; i++) {
722
+ const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
723
+ const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
724
+ const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
725
+ const textPad = Math.floor((innerWidth - truncated.length) / 2);
726
+ const textPadRight = innerWidth - truncated.length - textPad;
727
+ contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
728
+ if (i < numInRow - 1) contentLine += ' '.repeat(gap);
729
+ }
730
+ const contentPad = W - contentLine.length;
731
+ console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
732
+
733
+ // Bottom border of boxes
734
+ let bottomLine = ' ';
735
+ for (let i = 0; i < numInRow; i++) {
736
+ bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
737
+ if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
738
+ }
739
+ const bottomPad = W - bottomLine.length;
740
+ console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
741
+ }
699
742
  }
700
743
 
701
744
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
@@ -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 };