hedgequantx 1.8.48 → 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.
- package/README.md +7 -6
- package/bin/cli.js +13 -7
- package/dist/algo/copy-engine.js +3 -0
- package/dist/algo/copy-engine.jsc +0 -0
- package/dist/algo/engine.js +3 -0
- package/dist/algo/engine.jsc +0 -0
- package/dist/algo/market-data-rithmic.js +3 -0
- package/dist/algo/market-data-rithmic.jsc +0 -0
- package/dist/algo/market-data.js +3 -0
- package/dist/algo/market-data.jsc +0 -0
- package/dist/algo/rithmic/connection.js +3 -0
- package/dist/algo/rithmic/connection.jsc +0 -0
- package/dist/algo/rithmic/constants.js +3 -0
- package/dist/algo/rithmic/constants.jsc +0 -0
- package/dist/algo/rithmic/index.js +3 -0
- package/dist/algo/rithmic/index.jsc +0 -0
- package/dist/algo/rithmic/market-data.js +3 -0
- package/dist/algo/rithmic/market-data.jsc +0 -0
- package/dist/algo/rithmic/pnl.js +3 -0
- package/dist/algo/rithmic/pnl.jsc +0 -0
- package/dist/algo/rithmic/pool.js +3 -0
- package/dist/algo/rithmic/pool.jsc +0 -0
- package/dist/algo/rithmic/trading.js +3 -0
- package/dist/algo/rithmic/trading.jsc +0 -0
- package/dist/algo/rithmic-decoder.js +3 -0
- package/dist/algo/rithmic-decoder.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
- package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping.js +3 -0
- package/dist/algo/strategies/ultra-scalping.jsc +0 -0
- package/dist/algo/trading-api-rithmic.js +3 -0
- package/dist/algo/trading-api-rithmic.jsc +0 -0
- package/dist/algo/trading-api.js +3 -0
- package/dist/algo/trading-api.jsc +0 -0
- package/dist/algo/utils/smart-logger.js +3 -0
- package/dist/algo/utils/smart-logger.jsc +0 -0
- package/dist/algo/utils/smart-logs.js +3 -0
- package/dist/algo/utils/smart-logs.jsc +0 -0
- package/package.json +33 -10
- package/protos/rithmic/account_pnl_position_update.proto +59 -0
- package/protos/rithmic/base.proto +7 -0
- package/protos/rithmic/best_bid_offer.proto +39 -0
- package/protos/rithmic/exchange_order_notification.proto +140 -0
- package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
- package/protos/rithmic/last_trade.proto +53 -0
- package/protos/rithmic/request_account_list.proto +20 -0
- package/protos/rithmic/request_cancel_all_orders.proto +15 -0
- package/protos/rithmic/request_front_month_contract.proto +10 -0
- package/protos/rithmic/request_heartbeat.proto +13 -0
- package/protos/rithmic/request_login.proto +28 -0
- package/protos/rithmic/request_login_info.proto +10 -0
- package/protos/rithmic/request_logout.proto +10 -0
- package/protos/rithmic/request_market_data_update.proto +42 -0
- package/protos/rithmic/request_new_order.proto +84 -0
- package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
- package/protos/rithmic/request_pnl_position_updates.proto +20 -0
- package/protos/rithmic/request_product_codes.proto +9 -0
- package/protos/rithmic/request_rithmic_system_info.proto +8 -0
- package/protos/rithmic/request_show_order_history.proto +16 -0
- package/protos/rithmic/request_show_order_history_dates.proto +10 -0
- package/protos/rithmic/request_show_order_history_summary.proto +14 -0
- package/protos/rithmic/request_show_orders.proto +14 -0
- package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
- package/protos/rithmic/request_tick_bar_replay.proto +48 -0
- package/protos/rithmic/request_trade_routes.proto +11 -0
- package/protos/rithmic/response_account_list.proto +18 -0
- package/protos/rithmic/response_front_month_contract.proto +13 -0
- package/protos/rithmic/response_heartbeat.proto +14 -0
- package/protos/rithmic/response_login.proto +18 -0
- package/protos/rithmic/response_login_info.proto +24 -0
- package/protos/rithmic/response_logout.proto +11 -0
- package/protos/rithmic/response_market_data_update.proto +9 -0
- package/protos/rithmic/response_new_order.proto +18 -0
- package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
- package/protos/rithmic/response_pnl_position_updates.proto +11 -0
- package/protos/rithmic/response_product_codes.proto +12 -0
- package/protos/rithmic/response_rithmic_system_info.proto +12 -0
- package/protos/rithmic/response_show_order_history.proto +11 -0
- package/protos/rithmic/response_show_order_history_dates.proto +13 -0
- package/protos/rithmic/response_show_order_history_summary.proto +11 -0
- package/protos/rithmic/response_show_orders.proto +11 -0
- package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
- package/protos/rithmic/response_tick_bar_replay.proto +40 -0
- package/protos/rithmic/response_trade_routes.proto +19 -0
- package/protos/rithmic/rithmic_order_notification.proto +124 -0
- package/src/app.js +136 -89
- package/src/config/index.js +27 -8
- package/src/config/settings.js +155 -0
- package/src/pages/accounts.js +2 -3
- package/src/pages/algo/copy-trading.js +293 -200
- package/src/pages/algo/one-account.js +1 -1
- package/src/security/encryption.js +81 -46
- package/src/security/index.js +12 -8
- package/src/security/rateLimit.js +68 -65
- package/src/security/validation.js +93 -79
- package/src/services/hqx-server.js +538 -206
- package/src/services/projectx/index.js +327 -204
- package/src/services/rithmic/index.js +288 -285
- package/src/services/session.js +184 -114
- package/src/services/tradovate/index.js +286 -297
- package/src/ui/index.js +53 -1
- package/src/utils/http.js +236 -0
- package/src/utils/index.js +11 -2
- package/src/utils/logger.js +64 -33
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
74
|
+
|
|
75
|
+
// Step 3: Select Symbol
|
|
101
76
|
console.log();
|
|
102
77
|
console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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,
|
|
120
|
+
dailyTarget,
|
|
121
|
+
maxRisk,
|
|
122
|
+
showNames,
|
|
156
123
|
});
|
|
157
124
|
};
|
|
158
125
|
|
|
159
126
|
/**
|
|
160
|
-
*
|
|
127
|
+
* Fetch all active accounts from connections
|
|
128
|
+
* @param {Array} allConns - All connections
|
|
129
|
+
* @returns {Promise<Array>}
|
|
161
130
|
*/
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
*
|
|
181
|
+
* Select trading symbol
|
|
182
|
+
* @param {Object} service - Service instance
|
|
183
|
+
* @returns {Promise<Object|null>}
|
|
184
184
|
*/
|
|
185
|
-
const selectSymbol = async (service
|
|
185
|
+
const selectSymbol = async (service) => {
|
|
186
186
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
try {
|
|
189
|
-
//
|
|
189
|
+
// Try ProjectX API first for consistency
|
|
190
190
|
let contracts = await getContractsFromAPI();
|
|
191
|
-
|
|
192
|
-
// Fallback to service
|
|
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
|
|
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
|
|
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 });
|
|
231
|
+
|
|
232
|
+
options.push({ label: '', value: null, disabled: true });
|
|
236
233
|
options.push({ label: chalk.gray('< Cancel'), value: null });
|
|
237
|
-
|
|
238
|
-
return
|
|
239
|
-
} catch (
|
|
240
|
-
spinner.fail(`Error loading contracts: ${
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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,
|
|
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,
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
|
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
|
|
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)
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
hqx.on('
|
|
316
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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?',
|
|
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);
|