hedgequantx 1.8.49 → 2.3.0

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.
Files changed (103) hide show
  1. package/README.md +7 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Copy Trading Mode
2
+ * @fileoverview Copy Trading Mode
3
+ * @module pages/algo/copy-trading
3
4
  */
4
5
 
5
6
  const chalk = require('chalk');
@@ -19,8 +20,8 @@ const log = logger.scope('CopyTrading');
19
20
  */
20
21
  const copyTradingMenu = async () => {
21
22
  log.info('Copy Trading menu opened');
22
-
23
- // Check if market is open (skip early close check - market may still be open)
23
+
24
+ // Check market hours
24
25
  const market = checkMarketHours();
25
26
  if (!market.isOpen && !market.message.includes('early')) {
26
27
  console.log();
@@ -30,9 +31,9 @@ const copyTradingMenu = async () => {
30
31
  await prompts.waitForEnter();
31
32
  return;
32
33
  }
33
-
34
+
34
35
  const allConns = connections.getAll();
35
-
36
+
36
37
  if (allConns.length < 2) {
37
38
  console.log();
38
39
  console.log(chalk.yellow(` Copy Trading requires 2 connected accounts (found: ${allConns.length})`));
@@ -41,101 +42,65 @@ const copyTradingMenu = async () => {
41
42
  await prompts.waitForEnter();
42
43
  return;
43
44
  }
44
-
45
+
45
46
  console.log();
46
47
  console.log(chalk.yellow.bold(' Copy Trading Setup'));
47
48
  console.log();
48
-
49
+
50
+ // Fetch all accounts
49
51
  const spinner = ora({ text: 'Fetching accounts...', color: 'yellow' }).start();
50
-
51
- const allAccounts = [];
52
- for (const conn of allConns) {
53
- try {
54
- const result = await conn.service.getTradingAccounts();
55
- if (result.success && result.accounts) {
56
- const active = result.accounts.filter(a => a.status === 0);
57
- for (const acc of active) {
58
- allAccounts.push({ account: acc, service: conn.service, propfirm: conn.propfirm, type: conn.type });
59
- }
60
- }
61
- } catch (e) {}
62
- }
63
-
52
+ const allAccounts = await fetchAllAccounts(allConns);
53
+
64
54
  if (allAccounts.length < 2) {
65
55
  spinner.fail('Need at least 2 active accounts');
66
56
  await prompts.waitForEnter();
67
57
  return;
68
58
  }
69
-
59
+
70
60
  spinner.succeed(`Found ${allAccounts.length} active accounts`);
71
-
61
+
72
62
  // Step 1: Select Lead Account
73
63
  console.log(chalk.cyan(' Step 1: Select LEAD Account'));
74
- const leadOptions = allAccounts.map((a, i) => ({
75
- label: `${a.propfirm} - ${a.account.rithmicAccountId || a.account.name || a.account.accountId}${a.account.balance !== null ? ` ($${a.account.balance.toLocaleString()})` : ''}`,
76
- value: i
77
- }));
78
- leadOptions.push({ label: '< Cancel', value: -1 });
79
-
80
- const leadIdx = await prompts.selectOption('Lead Account:', leadOptions);
64
+ const leadIdx = await selectAccount('Lead Account:', allAccounts, -1);
81
65
  if (leadIdx === null || leadIdx === -1) return;
82
66
  const lead = allAccounts[leadIdx];
83
-
67
+
84
68
  // Step 2: Select Follower Account
85
69
  console.log();
86
70
  console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
87
- const followerOptions = allAccounts
88
- .map((a, i) => ({ a, i }))
89
- .filter(x => x.i !== leadIdx)
90
- .map(x => ({
91
- label: `${x.a.propfirm} - ${x.a.account.rithmicAccountId || x.a.account.name || x.a.account.accountId}${x.a.account.balance !== null ? ` ($${x.a.account.balance.toLocaleString()})` : ''}`,
92
- value: x.i
93
- }));
94
- followerOptions.push({ label: '< Cancel', value: -1 });
95
-
96
- const followerIdx = await prompts.selectOption('Follower Account:', followerOptions);
71
+ const followerIdx = await selectAccount('Follower Account:', allAccounts, leadIdx);
97
72
  if (followerIdx === null || followerIdx === -1) return;
98
73
  const follower = allAccounts[followerIdx];
99
-
100
- // Step 3: Select Trading Symbol
74
+
75
+ // Step 3: Select Symbol
101
76
  console.log();
102
77
  console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
103
- let symbol;
104
- try {
105
- symbol = await selectSymbol(lead.service, 'Trading');
106
- } catch (err) {
107
- console.log(chalk.red(` Error selecting symbol: ${err.message}`));
108
- await prompts.waitForEnter();
109
- return;
110
- }
111
- if (!symbol) {
112
- log.debug('No symbol selected');
113
- return;
114
- }
115
-
116
- // Step 4: Configure parameters
78
+ const symbol = await selectSymbol(lead.service);
79
+ if (!symbol) return;
80
+
81
+ // Step 4: Configure Parameters
117
82
  console.log();
118
83
  console.log(chalk.cyan(' Step 4: Configure Parameters'));
119
-
84
+
120
85
  const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
121
86
  if (leadContracts === null) return;
122
-
87
+
123
88
  const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
124
89
  if (followerContracts === null) return;
125
-
90
+
126
91
  const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
127
92
  if (dailyTarget === null) return;
128
-
93
+
129
94
  const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
130
95
  if (maxRisk === null) return;
131
-
96
+
132
97
  // Step 5: Privacy
133
98
  const showNames = await prompts.selectOption('Account names:', [
134
99
  { label: 'Hide account names', value: false },
135
- { label: 'Show account names', value: true }
100
+ { label: 'Show account names', value: true },
136
101
  ]);
137
102
  if (showNames === null) return;
138
-
103
+
139
104
  // Confirm
140
105
  console.log();
141
106
  console.log(chalk.white(' Summary:'));
@@ -144,240 +109,368 @@ const copyTradingMenu = async () => {
144
109
  console.log(chalk.cyan(` Follower: ${follower.propfirm} x${followerContracts}`));
145
110
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
146
111
  console.log();
147
-
112
+
148
113
  const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
149
114
  if (!confirm) return;
150
-
115
+
151
116
  // Launch
152
117
  await launchCopyTrading({
153
118
  lead: { ...lead, symbol, contracts: leadContracts },
154
119
  follower: { ...follower, symbol, contracts: followerContracts },
155
- dailyTarget, maxRisk, showNames
120
+ dailyTarget,
121
+ maxRisk,
122
+ showNames,
156
123
  });
157
124
  };
158
125
 
159
126
  /**
160
- * Get contracts from ProjectX API (shared for all services)
127
+ * Fetch all active accounts from connections
128
+ * @param {Array} allConns - All connections
129
+ * @returns {Promise<Array>}
161
130
  */
162
- const getContractsFromAPI = async () => {
163
- // Find a ProjectX connection to get contracts from API
164
- const allConns = connections.getAll();
165
- const projectxConn = allConns.find(c => c.type === 'projectx');
166
-
167
- if (projectxConn && typeof projectxConn.service.getContracts === 'function') {
168
- const result = await projectxConn.service.getContracts();
169
- if (result.success && result.contracts?.length > 0) {
170
- // Normalize contract structure - API returns { name: "ESH6", description: "E-mini S&P 500..." }
171
- return result.contracts.map(c => ({
172
- ...c,
173
- symbol: c.name || c.symbol,
174
- name: c.description || c.name || c.symbol
175
- }));
131
+ const fetchAllAccounts = async (allConns) => {
132
+ const allAccounts = [];
133
+
134
+ for (const conn of allConns) {
135
+ try {
136
+ const result = await conn.service.getTradingAccounts();
137
+ if (result.success && result.accounts) {
138
+ const active = result.accounts.filter(a => a.status === 0);
139
+ for (const acc of active) {
140
+ allAccounts.push({
141
+ account: acc,
142
+ service: conn.service,
143
+ propfirm: conn.propfirm,
144
+ type: conn.type,
145
+ });
146
+ }
147
+ }
148
+ } catch (err) {
149
+ log.warn('Failed to get accounts', { type: conn.type, error: err.message });
176
150
  }
177
151
  }
178
-
179
- return null;
152
+
153
+ return allAccounts;
154
+ };
155
+
156
+ /**
157
+ * Select account from list
158
+ * @param {string} message - Prompt message
159
+ * @param {Array} accounts - Available accounts
160
+ * @param {number} excludeIdx - Index to exclude
161
+ * @returns {Promise<number|null>}
162
+ */
163
+ const selectAccount = async (message, accounts, excludeIdx) => {
164
+ const options = accounts
165
+ .map((a, i) => ({ a, i }))
166
+ .filter(x => x.i !== excludeIdx)
167
+ .map(x => {
168
+ const acc = x.a.account;
169
+ const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
170
+ return {
171
+ label: `${x.a.propfirm} - ${acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
172
+ value: x.i,
173
+ };
174
+ });
175
+
176
+ options.push({ label: '< Cancel', value: -1 });
177
+ return prompts.selectOption(message, options);
180
178
  };
181
179
 
182
180
  /**
183
- * Symbol selection helper - grouped by category with indices first
181
+ * Select trading symbol
182
+ * @param {Object} service - Service instance
183
+ * @returns {Promise<Object|null>}
184
184
  */
185
- const selectSymbol = async (service, label) => {
185
+ const selectSymbol = async (service) => {
186
186
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
187
-
187
+
188
188
  try {
189
- // Always use contracts from ProjectX API for consistency
189
+ // Try ProjectX API first for consistency
190
190
  let contracts = await getContractsFromAPI();
191
-
192
- // Fallback to service's own contracts if no ProjectX available
191
+
192
+ // Fallback to service
193
193
  if (!contracts && typeof service.getContracts === 'function') {
194
194
  const result = await service.getContracts();
195
195
  if (result.success && result.contracts?.length > 0) {
196
- // Contracts from Rithmic are already categorized and sorted
197
196
  contracts = result.contracts;
198
197
  }
199
198
  }
200
-
201
- if (!contracts || contracts.length === 0) {
199
+
200
+ if (!contracts || !contracts.length) {
202
201
  spinner.fail('No contracts available');
203
202
  await prompts.waitForEnter();
204
203
  return null;
205
204
  }
206
-
205
+
207
206
  spinner.succeed(`Found ${contracts.length} contracts`);
208
-
209
- // Build options with category headers (if contracts have category info)
207
+
208
+ // Build options with category headers
210
209
  const options = [];
211
210
  let currentCategory = null;
212
-
211
+
213
212
  for (const c of contracts) {
214
- // Add category header when category changes (if available)
215
213
  if (c.categoryName && c.categoryName !== currentCategory) {
216
214
  currentCategory = c.categoryName;
217
- options.push({
218
- label: chalk.cyan.bold(`── ${currentCategory} ──`),
219
- value: null,
220
- disabled: true
215
+ options.push({
216
+ label: chalk.cyan.bold(`── ${currentCategory} ──`),
217
+ value: null,
218
+ disabled: true,
221
219
  });
222
220
  }
223
-
224
- // Format label based on available data
221
+
225
222
  const symbolDisplay = c.symbol || c.name;
226
223
  const nameDisplay = c.name || c.symbol;
227
224
  const exchangeDisplay = c.exchange ? ` (${c.exchange})` : '';
228
- const label = c.categoryName
225
+ const label = c.categoryName
229
226
  ? ` ${symbolDisplay} - ${nameDisplay}${exchangeDisplay}`
230
227
  : `${nameDisplay}${exchangeDisplay}`;
231
-
228
+
232
229
  options.push({ label, value: c });
233
230
  }
234
-
235
- options.push({ label: '', value: null, disabled: true }); // Spacer
231
+
232
+ options.push({ label: '', value: null, disabled: true });
236
233
  options.push({ label: chalk.gray('< Cancel'), value: null });
237
-
238
- return await prompts.selectOption(`${label} Symbol:`, options);
239
- } catch (e) {
240
- spinner.fail(`Error loading contracts: ${e.message}`);
234
+
235
+ return prompts.selectOption('Trading Symbol:', options);
236
+ } catch (err) {
237
+ spinner.fail(`Error loading contracts: ${err.message}`);
241
238
  await prompts.waitForEnter();
242
239
  return null;
243
240
  }
244
241
  };
245
242
 
246
243
  /**
247
- * Launch Copy Trading
244
+ * Get contracts from ProjectX API
245
+ * @returns {Promise<Array|null>}
246
+ */
247
+ const getContractsFromAPI = async () => {
248
+ const allConns = connections.getAll();
249
+ const projectxConn = allConns.find(c => c.type === 'projectx');
250
+
251
+ if (projectxConn && typeof projectxConn.service.getContracts === 'function') {
252
+ const result = await projectxConn.service.getContracts();
253
+ if (result.success && result.contracts?.length > 0) {
254
+ return result.contracts.map(c => ({
255
+ ...c,
256
+ symbol: c.name || c.symbol,
257
+ name: c.description || c.name || c.symbol,
258
+ }));
259
+ }
260
+ }
261
+
262
+ return null;
263
+ };
264
+
265
+ /**
266
+ * Launch Copy Trading session
267
+ * @param {Object} config - Session configuration
248
268
  */
249
269
  const launchCopyTrading = async (config) => {
250
270
  const { lead, follower, dailyTarget, maxRisk, showNames } = config;
251
-
252
- // NEVER show real name - only account ID or masked
271
+
272
+ // Account names (masked for privacy)
253
273
  const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
254
274
  const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
255
-
275
+
256
276
  const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
257
-
277
+
258
278
  const stats = {
259
- leadName, followerName,
279
+ leadName,
280
+ followerName,
260
281
  leadSymbol: lead.symbol.name,
261
282
  followerSymbol: follower.symbol.name,
262
283
  leadQty: lead.contracts,
263
284
  followerQty: follower.contracts,
264
- target: dailyTarget, risk: maxRisk,
265
- pnl: 0, trades: 0, wins: 0, losses: 0,
266
- latency: 0, connected: false
285
+ target: dailyTarget,
286
+ risk: maxRisk,
287
+ pnl: 0,
288
+ trades: 0,
289
+ wins: 0,
290
+ losses: 0,
291
+ latency: 0,
292
+ connected: false,
267
293
  };
268
-
294
+
269
295
  let running = true;
270
296
  let stopReason = null;
271
-
297
+
298
+ // Connect to HQX Server
272
299
  const hqx = new HQXServerService();
273
300
  const spinner = ora({ text: 'Connecting to HQX Server...', color: 'yellow' }).start();
274
-
301
+
275
302
  try {
276
- const auth = await hqx.authenticate(lead.account.accountId.toString(), lead.propfirm || 'topstep');
303
+ const auth = await hqx.authenticate(
304
+ lead.account.accountId.toString(),
305
+ lead.propfirm || 'topstep'
306
+ );
277
307
  if (!auth.success) throw new Error(auth.error);
278
-
308
+
279
309
  const conn = await hqx.connect();
280
310
  if (!conn.success) throw new Error('WebSocket failed');
281
-
311
+
282
312
  spinner.succeed('Connected');
283
313
  stats.connected = true;
284
314
  } catch (err) {
285
315
  spinner.warn('HQX Server unavailable');
316
+ log.warn('HQX connection failed', { error: err.message });
286
317
  }
287
-
318
+
288
319
  // Event handlers
289
- hqx.on('latency', (d) => { stats.latency = d.latency || 0; });
320
+ setupEventHandlers(hqx, ui, stats, lead, follower, showNames, () => {
321
+ running = false;
322
+ }, (reason) => {
323
+ stopReason = reason;
324
+ }, dailyTarget, maxRisk);
325
+
326
+ // Start algo on server
327
+ if (stats.connected) {
328
+ startCopyTradingOnServer(hqx, lead, follower, dailyTarget, maxRisk, ui);
329
+ }
330
+
331
+ // UI refresh loop
332
+ const refreshInterval = setInterval(() => {
333
+ if (running) ui.render(stats);
334
+ }, 250);
335
+
336
+ // Keyboard handling
337
+ const cleanupKeys = setupKeyboardHandler(() => {
338
+ running = false;
339
+ stopReason = 'manual';
340
+ });
341
+
342
+ // Wait for stop
343
+ await new Promise((resolve) => {
344
+ const check = setInterval(() => {
345
+ if (!running) {
346
+ clearInterval(check);
347
+ resolve();
348
+ }
349
+ }, 100);
350
+ });
351
+
352
+ // Cleanup
353
+ clearInterval(refreshInterval);
354
+ if (cleanupKeys) cleanupKeys();
355
+ if (stats.connected) {
356
+ hqx.stopAlgo();
357
+ hqx.disconnect();
358
+ }
359
+ ui.cleanup();
360
+
361
+ // Show summary
362
+ renderSessionSummary(stats, stopReason);
363
+ await prompts.waitForEnter();
364
+ };
365
+
366
+ /**
367
+ * Setup HQX event handlers
368
+ */
369
+ const setupEventHandlers = (hqx, ui, stats, lead, follower, showNames, stop, setReason, dailyTarget, maxRisk) => {
370
+ hqx.on('latency', (d) => {
371
+ stats.latency = d.latency || 0;
372
+ });
373
+
290
374
  hqx.on('log', (d) => {
291
375
  let msg = d.message;
292
376
  if (!showNames) {
293
- if (lead.account.accountName) msg = msg.replace(new RegExp(lead.account.accountName, 'gi'), 'Lead *****');
294
- if (follower.account.accountName) msg = msg.replace(new RegExp(follower.account.accountName, 'gi'), 'Follower *****');
377
+ if (lead.account.accountName) {
378
+ msg = msg.replace(new RegExp(lead.account.accountName, 'gi'), 'Lead *****');
379
+ }
380
+ if (follower.account.accountName) {
381
+ msg = msg.replace(new RegExp(follower.account.accountName, 'gi'), 'Follower *****');
382
+ }
295
383
  }
296
384
  ui.addLog(d.type || 'info', msg);
297
385
  });
386
+
298
387
  hqx.on('trade', (d) => {
299
388
  stats.trades++;
300
389
  stats.pnl += d.pnl || 0;
301
390
  d.pnl >= 0 ? stats.wins++ : stats.losses++;
302
391
  ui.addLog(d.pnl >= 0 ? 'trade' : 'loss', `${d.pnl >= 0 ? '+' : ''}$${d.pnl.toFixed(2)}`);
303
-
392
+
304
393
  if (stats.pnl >= dailyTarget) {
305
- stopReason = 'target'; running = false;
394
+ setReason('target');
395
+ stop();
306
396
  ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
307
397
  hqx.stopAlgo();
308
398
  } else if (stats.pnl <= -maxRisk) {
309
- stopReason = 'risk'; running = false;
399
+ setReason('risk');
400
+ stop();
310
401
  ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
311
402
  hqx.stopAlgo();
312
403
  }
313
404
  });
314
- hqx.on('copy', (d) => { ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x`); });
315
- hqx.on('error', (d) => { ui.addLog('error', d.message); });
316
- hqx.on('disconnected', () => { stats.connected = false; });
317
-
318
- // Start on server
319
- if (stats.connected) {
320
- ui.addLog('info', 'Starting Copy Trading...');
321
-
322
- let leadCreds = null, followerCreds = null;
323
- if (lead.service.getRithmicCredentials) leadCreds = lead.service.getRithmicCredentials();
324
- if (follower.service.getRithmicCredentials) followerCreds = follower.service.getRithmicCredentials();
325
-
326
- hqx.startCopyTrading({
327
- leadAccountId: lead.account.accountId,
328
- leadContractId: lead.symbol.id || lead.symbol.contractId,
329
- leadSymbol: lead.symbol.symbol || lead.symbol.name,
330
- leadContracts: lead.contracts,
331
- leadPropfirm: lead.propfirm,
332
- leadToken: lead.service.getToken?.() || null,
333
- leadRithmicCredentials: leadCreds,
334
- followerAccountId: follower.account.accountId,
335
- followerContractId: follower.symbol.id || follower.symbol.contractId,
336
- followerSymbol: follower.symbol.symbol || follower.symbol.name,
337
- followerContracts: follower.contracts,
338
- followerPropfirm: follower.propfirm,
339
- followerToken: follower.service.getToken?.() || null,
340
- followerRithmicCredentials: followerCreds,
341
- dailyTarget, maxRisk
342
- });
343
- }
344
-
345
- const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
346
-
347
- // Keyboard
348
- const setupKeys = () => {
349
- if (!process.stdin.isTTY) return null;
350
- readline.emitKeypressEvents(process.stdin);
351
- process.stdin.setRawMode(true);
352
- process.stdin.resume();
353
-
354
- const handler = (str, key) => {
355
- if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
356
- running = false; stopReason = 'manual';
357
- }
358
- };
359
- process.stdin.on('keypress', handler);
360
- return () => {
361
- process.stdin.removeListener('keypress', handler);
362
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
363
- };
364
- };
365
-
366
- const cleanupKeys = setupKeys();
367
-
368
- await new Promise(resolve => {
369
- const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
405
+
406
+ hqx.on('copy', (d) => {
407
+ ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x`);
370
408
  });
371
-
372
- clearInterval(refreshInterval);
373
- if (cleanupKeys) cleanupKeys();
374
- if (stats.connected) { hqx.stopAlgo(); hqx.disconnect(); }
375
- ui.cleanup();
376
-
377
- // Summary
378
- renderSessionSummary(stats, stopReason);
379
-
380
- await prompts.waitForEnter();
409
+
410
+ hqx.on('error', (d) => {
411
+ ui.addLog('error', d.message);
412
+ });
413
+
414
+ hqx.on('disconnected', () => {
415
+ stats.connected = false;
416
+ });
417
+ };
418
+
419
+ /**
420
+ * Start copy trading on HQX server
421
+ */
422
+ const startCopyTradingOnServer = (hqx, lead, follower, dailyTarget, maxRisk, ui) => {
423
+ ui.addLog('info', 'Starting Copy Trading...');
424
+
425
+ const leadCreds = lead.service.getRithmicCredentials?.() || null;
426
+ const followerCreds = follower.service.getRithmicCredentials?.() || null;
427
+
428
+ hqx.startCopyTrading({
429
+ leadAccountId: lead.account.accountId,
430
+ leadContractId: lead.symbol.id || lead.symbol.contractId,
431
+ leadSymbol: lead.symbol.symbol || lead.symbol.name,
432
+ leadContracts: lead.contracts,
433
+ leadPropfirm: lead.propfirm,
434
+ leadToken: lead.service.getToken?.() || null,
435
+ leadRithmicCredentials: leadCreds,
436
+ followerAccountId: follower.account.accountId,
437
+ followerContractId: follower.symbol.id || follower.symbol.contractId,
438
+ followerSymbol: follower.symbol.symbol || follower.symbol.name,
439
+ followerContracts: follower.contracts,
440
+ followerPropfirm: follower.propfirm,
441
+ followerToken: follower.service.getToken?.() || null,
442
+ followerRithmicCredentials: followerCreds,
443
+ dailyTarget,
444
+ maxRisk,
445
+ });
446
+ };
447
+
448
+ /**
449
+ * Setup keyboard handler
450
+ * @param {Function} onStop - Stop callback
451
+ * @returns {Function|null} Cleanup function
452
+ */
453
+ const setupKeyboardHandler = (onStop) => {
454
+ if (!process.stdin.isTTY) return null;
455
+
456
+ readline.emitKeypressEvents(process.stdin);
457
+ process.stdin.setRawMode(true);
458
+ process.stdin.resume();
459
+
460
+ const handler = (str, key) => {
461
+ if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
462
+ onStop();
463
+ }
464
+ };
465
+
466
+ process.stdin.on('keypress', handler);
467
+
468
+ return () => {
469
+ process.stdin.removeListener('keypress', handler);
470
+ if (process.stdin.isTTY) {
471
+ process.stdin.setRawMode(false);
472
+ }
473
+ };
381
474
  };
382
475
 
383
476
  module.exports = { copyTradingMenu };
@@ -132,7 +132,7 @@ const configureAlgo = async (account, contract) => {
132
132
  const maxRisk = await prompts.numberInput('Max risk ($):', 100, 1, 5000);
133
133
  if (maxRisk === null) return null;
134
134
 
135
- const showName = await prompts.confirmPrompt('Show account name?', true);
135
+ const showName = await prompts.confirmPrompt('Show account name?', false);
136
136
  if (showName === null) return null;
137
137
 
138
138
  const confirm = await prompts.confirmPrompt('Start algo trading?', true);