hedgequantx 2.9.20 → 2.9.22
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 +64 -42
- package/src/menus/connect.js +17 -14
- package/src/menus/dashboard.js +76 -58
- package/src/pages/accounts.js +49 -38
- package/src/pages/ai-agents-ui.js +388 -0
- package/src/pages/ai-agents.js +494 -0
- package/src/pages/ai-models.js +389 -0
- package/src/pages/algo/algo-executor.js +307 -0
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +178 -546
- package/src/pages/algo/custom-strategy.js +313 -0
- package/src/pages/algo/index.js +75 -18
- package/src/pages/algo/one-account.js +57 -322
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +22 -19
- package/src/pages/positions.js +22 -19
- package/src/pages/stats/index.js +16 -15
- package/src/pages/user.js +11 -7
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +47 -35
- package/src/services/ai-supervision/index.js +359 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +256 -0
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +387 -0
- package/src/services/index.js +9 -1
- package/src/services/llmproxy/index.js +166 -0
- package/src/services/llmproxy/manager.js +411 -0
- package/src/services/rithmic/accounts.js +6 -8
- package/src/ui/box.js +5 -9
- package/src/ui/index.js +18 -5
- package/src/ui/menu.js +4 -4
|
@@ -1,49 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Copy Trading Mode - HQX Ultra Scalping
|
|
3
|
+
* Same as One Account but copies trades to multiple followers
|
|
4
|
+
* Supports multi-agent AI supervision
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const chalk = require('chalk');
|
|
7
8
|
const ora = require('ora');
|
|
8
|
-
const readline = require('readline');
|
|
9
9
|
|
|
10
10
|
const { connections } = require('../../services');
|
|
11
|
-
const {
|
|
12
|
-
const { logger, prompts } = require('../../utils');
|
|
11
|
+
const { prompts } = require('../../utils');
|
|
13
12
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const {
|
|
17
|
-
const { MarketDataFeed } = require('../../lib/data');
|
|
18
|
-
|
|
19
|
-
const log = logger.scope('CopyTrading');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Strategy Selection
|
|
24
|
-
* @returns {Promise<string|null>} Selected strategy ID or null
|
|
25
|
-
*/
|
|
26
|
-
const selectStrategy = async () => {
|
|
27
|
-
const strategies = getAvailableStrategies();
|
|
28
|
-
|
|
29
|
-
const options = strategies.map(s => ({
|
|
30
|
-
label: s.id === 'ultra-scalping' ? 'HQX Scalping' : 'HQX Sweep',
|
|
31
|
-
value: s.id
|
|
32
|
-
}));
|
|
33
|
-
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
34
|
-
|
|
35
|
-
const selected = await prompts.selectOption('Select Strategy:', options);
|
|
36
|
-
return selected === 'back' ? null : selected;
|
|
37
|
-
};
|
|
38
|
-
|
|
13
|
+
const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
|
|
14
|
+
const { launchCopyTrading } = require('./copy-executor');
|
|
15
|
+
const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
|
|
39
16
|
|
|
40
17
|
/**
|
|
41
18
|
* Copy Trading Menu
|
|
42
19
|
*/
|
|
43
20
|
const copyTradingMenu = async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Check market hours
|
|
21
|
+
// Check if market is open
|
|
47
22
|
const market = checkMarketHours();
|
|
48
23
|
if (!market.isOpen && !market.message.includes('early')) {
|
|
49
24
|
console.log();
|
|
@@ -53,565 +28,222 @@ const copyTradingMenu = async () => {
|
|
|
53
28
|
await prompts.waitForEnter();
|
|
54
29
|
return;
|
|
55
30
|
}
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
|
|
32
|
+
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
33
|
+
|
|
34
|
+
const allAccounts = await connections.getAllAccounts();
|
|
35
|
+
|
|
36
|
+
if (!allAccounts?.length) {
|
|
37
|
+
spinner.fail('No accounts found');
|
|
38
|
+
await prompts.waitForEnter();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
43
|
+
|
|
44
|
+
if (activeAccounts.length < 2) {
|
|
45
|
+
spinner.fail(`Need at least 2 active accounts (found: ${activeAccounts.length})`);
|
|
62
46
|
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
63
|
-
console.log();
|
|
64
47
|
await prompts.waitForEnter();
|
|
65
48
|
return;
|
|
66
49
|
}
|
|
67
|
-
|
|
50
|
+
|
|
51
|
+
spinner.succeed(`Found ${activeAccounts.length} active accounts`);
|
|
52
|
+
|
|
53
|
+
// Step 1: Select LEAD Account
|
|
68
54
|
console.log();
|
|
69
|
-
console.log(chalk.
|
|
55
|
+
console.log(chalk.cyan.bold(' STEP 1: SELECT LEAD ACCOUNT'));
|
|
56
|
+
const leadOptions = activeAccounts.map(acc => {
|
|
57
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
58
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
59
|
+
? ` - $${acc.balance.toLocaleString()}`
|
|
60
|
+
: '';
|
|
61
|
+
return {
|
|
62
|
+
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
63
|
+
value: acc
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
leadOptions.push({ label: '< Back', value: 'back' });
|
|
67
|
+
|
|
68
|
+
const leadAccount = await prompts.selectOption('Lead Account:', leadOptions);
|
|
69
|
+
if (!leadAccount || leadAccount === 'back') return;
|
|
70
|
+
|
|
71
|
+
// Step 2: Select FOLLOWER Account(s)
|
|
70
72
|
console.log();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
console.log(chalk.yellow.bold(' STEP 2: SELECT FOLLOWER ACCOUNT(S)'));
|
|
74
|
+
console.log(chalk.gray(' (Select accounts to copy trades to)'));
|
|
75
|
+
|
|
76
|
+
const followers = [];
|
|
77
|
+
const availableFollowers = activeAccounts.filter(a => a.accountId !== leadAccount.accountId);
|
|
78
|
+
|
|
79
|
+
while (availableFollowers.length > 0) {
|
|
80
|
+
const remaining = availableFollowers.filter(a => !followers.find(f => f.accountId === a.accountId));
|
|
81
|
+
if (remaining.length === 0) break;
|
|
82
|
+
|
|
83
|
+
const followerOptions = remaining.map(acc => {
|
|
84
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
85
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
86
|
+
? ` - $${acc.balance.toLocaleString()}`
|
|
87
|
+
: '';
|
|
88
|
+
return {
|
|
89
|
+
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
90
|
+
value: acc
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (followers.length > 0) {
|
|
95
|
+
followerOptions.push({ label: chalk.green('✓ Done selecting followers'), value: 'done' });
|
|
96
|
+
}
|
|
97
|
+
followerOptions.push({ label: '< Back', value: 'back' });
|
|
98
|
+
|
|
99
|
+
const msg = followers.length === 0 ? 'Select Follower:' : `Add another follower (${followers.length} selected):`;
|
|
100
|
+
const selected = await prompts.selectOption(msg, followerOptions);
|
|
101
|
+
|
|
102
|
+
if (!selected || selected === 'back') {
|
|
103
|
+
if (followers.length === 0) return;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (selected === 'done') break;
|
|
107
|
+
|
|
108
|
+
followers.push(selected);
|
|
109
|
+
console.log(chalk.green(` ✓ Added: ${selected.accountName || selected.accountId}`));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (followers.length === 0) {
|
|
113
|
+
console.log(chalk.red(' No followers selected'));
|
|
78
114
|
await prompts.waitForEnter();
|
|
79
115
|
return;
|
|
80
116
|
}
|
|
81
|
-
|
|
82
|
-
spinner.succeed(`Found ${allAccounts.length} active accounts`);
|
|
83
|
-
|
|
84
|
-
// Step 1: Select Lead Account
|
|
85
|
-
console.log(chalk.cyan(' Step 1: Select LEAD Account'));
|
|
86
|
-
const leadIdx = await selectAccount('Lead Account:', allAccounts, -1);
|
|
87
|
-
if (leadIdx === null || leadIdx === -1) return;
|
|
88
|
-
const lead = allAccounts[leadIdx];
|
|
89
|
-
|
|
90
|
-
// Step 2: Select Follower Account
|
|
91
|
-
console.log();
|
|
92
|
-
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
|
|
93
|
-
const followerIdx = await selectAccount('Follower Account:', allAccounts, leadIdx);
|
|
94
|
-
if (followerIdx === null || followerIdx === -1) return;
|
|
95
|
-
const follower = allAccounts[followerIdx];
|
|
96
|
-
|
|
117
|
+
|
|
97
118
|
// Step 3: Select Symbol
|
|
98
119
|
console.log();
|
|
99
|
-
console.log(chalk.
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
console.log(chalk.magenta.bold(' STEP 3: SELECT SYMBOL'));
|
|
121
|
+
const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
|
|
122
|
+
const contract = await selectSymbol(leadService);
|
|
123
|
+
if (!contract) return;
|
|
124
|
+
|
|
125
|
+
// Step 4: Configure Parameters
|
|
104
126
|
console.log();
|
|
105
|
-
console.log(chalk.cyan('
|
|
106
|
-
const strategyId = await selectStrategy();
|
|
107
|
-
if (!strategyId) return;
|
|
108
|
-
|
|
109
|
-
// Step 5: Configure Parameters
|
|
127
|
+
console.log(chalk.cyan.bold(' STEP 4: CONFIGURE PARAMETERS'));
|
|
110
128
|
console.log();
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
|
|
113
130
|
const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
|
|
114
131
|
if (leadContracts === null) return;
|
|
115
|
-
|
|
116
|
-
const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
|
|
132
|
+
|
|
133
|
+
const followerContracts = await prompts.numberInput('Follower contracts (each):', leadContracts, 1, 10);
|
|
117
134
|
if (followerContracts === null) return;
|
|
118
|
-
|
|
135
|
+
|
|
119
136
|
const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
|
|
120
137
|
if (dailyTarget === null) return;
|
|
121
|
-
|
|
138
|
+
|
|
122
139
|
const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
|
|
123
140
|
if (maxRisk === null) return;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const showNames = await prompts.selectOption('Account names:', [
|
|
127
|
-
{ label: 'Hide account names', value: false },
|
|
128
|
-
{ label: 'Show account names', value: true },
|
|
129
|
-
]);
|
|
141
|
+
|
|
142
|
+
const showNames = await prompts.confirmPrompt('Show account names?', false);
|
|
130
143
|
if (showNames === null) return;
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
const
|
|
144
|
+
|
|
145
|
+
// Check for AI Supervision
|
|
146
|
+
const agentCount = getActiveAgentCount();
|
|
147
|
+
let supervisionConfig = null;
|
|
148
|
+
|
|
149
|
+
if (agentCount > 0) {
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.cyan(` ${agentCount} AI Agent(s) available for supervision`));
|
|
152
|
+
const enableAI = await prompts.confirmPrompt('Enable AI Supervision?', true);
|
|
153
|
+
|
|
154
|
+
if (enableAI) {
|
|
155
|
+
// Run pre-flight check - ALL agents must pass
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(chalk.yellow(' Running AI pre-flight check...'));
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
const agents = getActiveAgents();
|
|
161
|
+
const preflightResults = await runPreflightCheck(agents);
|
|
162
|
+
|
|
163
|
+
// Display results
|
|
164
|
+
const lines = formatPreflightResults(preflightResults, 60);
|
|
165
|
+
for (const line of lines) console.log(line);
|
|
166
|
+
|
|
167
|
+
const summary = getPreflightSummary(preflightResults);
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(` ${summary.text}`);
|
|
170
|
+
console.log();
|
|
171
|
+
|
|
172
|
+
if (!preflightResults.success) {
|
|
173
|
+
console.log(chalk.red(' Cannot start algo - fix agent connections first.'));
|
|
174
|
+
await prompts.waitForEnter();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
supervisionConfig = getSupervisionConfig();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Summary
|
|
134
183
|
console.log();
|
|
135
|
-
console.log(chalk.white('
|
|
136
|
-
console.log(chalk.cyan(`
|
|
137
|
-
console.log(chalk.cyan(`
|
|
138
|
-
console.log(chalk.
|
|
139
|
-
|
|
184
|
+
console.log(chalk.white.bold(' SUMMARY:'));
|
|
185
|
+
console.log(chalk.cyan(` Symbol: ${contract.name}`));
|
|
186
|
+
console.log(chalk.cyan(` Lead: ${leadAccount.propfirm} x${leadContracts}`));
|
|
187
|
+
console.log(chalk.yellow(` Followers (${followers.length}):`));
|
|
188
|
+
for (const f of followers) {
|
|
189
|
+
console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
|
|
190
|
+
}
|
|
140
191
|
console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
192
|
+
if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
|
|
141
193
|
console.log();
|
|
142
|
-
|
|
194
|
+
|
|
143
195
|
const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
|
|
144
196
|
if (!confirm) return;
|
|
145
|
-
|
|
146
|
-
// Launch
|
|
197
|
+
|
|
147
198
|
await launchCopyTrading({
|
|
148
|
-
lead: {
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
lead: { account: leadAccount, contracts: leadContracts },
|
|
200
|
+
followers: followers.map(f => ({ account: f, contracts: followerContracts })),
|
|
201
|
+
contract,
|
|
151
202
|
dailyTarget,
|
|
152
203
|
maxRisk,
|
|
153
204
|
showNames,
|
|
205
|
+
supervisionConfig
|
|
154
206
|
});
|
|
155
207
|
};
|
|
156
208
|
|
|
157
209
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {Array} allConns - All connections
|
|
160
|
-
* @returns {Promise<Array>}
|
|
161
|
-
*/
|
|
162
|
-
const fetchAllAccounts = async (allConns) => {
|
|
163
|
-
const allAccounts = [];
|
|
164
|
-
|
|
165
|
-
for (const conn of allConns) {
|
|
166
|
-
try {
|
|
167
|
-
const result = await conn.service.getTradingAccounts();
|
|
168
|
-
if (result.success && result.accounts) {
|
|
169
|
-
const active = result.accounts.filter(a => a.status === 0);
|
|
170
|
-
for (const acc of active) {
|
|
171
|
-
allAccounts.push({
|
|
172
|
-
account: acc,
|
|
173
|
-
service: conn.service,
|
|
174
|
-
propfirm: conn.propfirm,
|
|
175
|
-
type: conn.type,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch (err) {
|
|
180
|
-
log.warn('Failed to get accounts', { type: conn.type, error: err.message });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return allAccounts;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Select account from list
|
|
189
|
-
* @param {string} message - Prompt message
|
|
190
|
-
* @param {Array} accounts - Available accounts
|
|
191
|
-
* @param {number} excludeIdx - Index to exclude
|
|
192
|
-
* @returns {Promise<number|null>}
|
|
193
|
-
*/
|
|
194
|
-
const selectAccount = async (message, accounts, excludeIdx) => {
|
|
195
|
-
const options = accounts
|
|
196
|
-
.map((a, i) => ({ a, i }))
|
|
197
|
-
.filter(x => x.i !== excludeIdx)
|
|
198
|
-
.map(x => {
|
|
199
|
-
const acc = x.a.account;
|
|
200
|
-
const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
|
|
201
|
-
return {
|
|
202
|
-
label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
|
|
203
|
-
value: x.i,
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
options.push({ label: '< Cancel', value: -1 });
|
|
208
|
-
return prompts.selectOption(message, options);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Select trading symbol
|
|
213
|
-
* @param {Object} service - Service instance
|
|
214
|
-
* @returns {Promise<Object|null>}
|
|
210
|
+
* Symbol selection - sorted with popular indices first
|
|
215
211
|
*/
|
|
216
212
|
const selectSymbol = async (service) => {
|
|
217
213
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// Fallback to service
|
|
224
|
-
if (!contracts && typeof service.getContracts === 'function') {
|
|
225
|
-
const result = await service.getContracts();
|
|
226
|
-
if (result.success && result.contracts?.length > 0) {
|
|
227
|
-
contracts = result.contracts;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!contracts || !contracts.length) {
|
|
232
|
-
spinner.fail('No contracts available');
|
|
233
|
-
await prompts.waitForEnter();
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
238
|
-
|
|
239
|
-
// Build options from RAW API data - no static mapping
|
|
240
|
-
const options = [];
|
|
241
|
-
let currentGroup = null;
|
|
242
|
-
|
|
243
|
-
for (const c of contracts) {
|
|
244
|
-
// Use RAW API field: contractGroup
|
|
245
|
-
if (c.contractGroup && c.contractGroup !== currentGroup) {
|
|
246
|
-
currentGroup = c.contractGroup;
|
|
247
|
-
options.push({
|
|
248
|
-
label: chalk.cyan.bold(`-- ${currentGroup} --`),
|
|
249
|
-
value: null,
|
|
250
|
-
disabled: true,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Use RAW API fields: symbol (trading symbol), name (product name), exchange
|
|
255
|
-
const label = ` ${c.symbol} - ${c.name} (${c.exchange})`;
|
|
256
|
-
options.push({ label, value: c });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
options.push({ label: '', value: null, disabled: true });
|
|
260
|
-
options.push({ label: chalk.gray('< Cancel'), value: null });
|
|
261
|
-
|
|
262
|
-
return prompts.selectOption('Trading Symbol:', options);
|
|
263
|
-
} catch (err) {
|
|
264
|
-
spinner.fail(`Error loading contracts: ${err.message}`);
|
|
265
|
-
await prompts.waitForEnter();
|
|
214
|
+
|
|
215
|
+
const contractsResult = await service.getContracts();
|
|
216
|
+
if (!contractsResult.success || !contractsResult.contracts?.length) {
|
|
217
|
+
spinner.fail('Failed to load contracts');
|
|
266
218
|
return null;
|
|
267
219
|
}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get contracts from Rithmic API - RAW data only
|
|
272
|
-
* @returns {Promise<Array|null>}
|
|
273
|
-
*/
|
|
274
|
-
const getContractsFromAPI = async () => {
|
|
275
|
-
const allConns = connections.getAll();
|
|
276
|
-
const rithmicConn = allConns.find(c => c.type === 'rithmic');
|
|
277
|
-
|
|
278
|
-
if (rithmicConn && typeof rithmicConn.service.getContracts === 'function') {
|
|
279
|
-
const result = await rithmicConn.service.getContracts();
|
|
280
|
-
if (result.success && result.contracts?.length > 0) {
|
|
281
|
-
// Return RAW API data - no mapping
|
|
282
|
-
return result.contracts;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return null;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Launch Copy Trading session with strategy
|
|
291
|
-
* @param {Object} config - Session configuration
|
|
292
|
-
*/
|
|
293
|
-
const launchCopyTrading = async (config) => {
|
|
294
|
-
const { lead, follower, strategyId, dailyTarget, maxRisk, showNames } = config;
|
|
295
|
-
|
|
296
|
-
// Load strategy dynamically
|
|
297
|
-
const strategyInfo = getStrategy(strategyId);
|
|
298
|
-
const strategyModule = loadStrategy(strategyId);
|
|
299
|
-
|
|
300
|
-
// Account names (masked for privacy)
|
|
301
|
-
const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
|
|
302
|
-
const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
|
|
303
|
-
|
|
304
|
-
const tickSize = lead.symbol.tickSize || 0.25;
|
|
305
|
-
const contractId = lead.symbol.id;
|
|
306
|
-
|
|
307
|
-
const ui = new AlgoUI({ subtitle: `${strategyInfo.name} - Copy Trading`, mode: 'copy-trading' });
|
|
308
|
-
|
|
309
|
-
const stats = {
|
|
310
|
-
leadName,
|
|
311
|
-
followerName,
|
|
312
|
-
leadSymbol: lead.symbol.name,
|
|
313
|
-
followerSymbol: follower.symbol.name,
|
|
314
|
-
leadQty: lead.contracts,
|
|
315
|
-
followerQty: follower.contracts,
|
|
316
|
-
target: dailyTarget,
|
|
317
|
-
risk: maxRisk,
|
|
318
|
-
pnl: 0,
|
|
319
|
-
trades: 0,
|
|
320
|
-
wins: 0,
|
|
321
|
-
losses: 0,
|
|
322
|
-
latency: 0,
|
|
323
|
-
connected: false,
|
|
324
|
-
platform: lead.account.platform || 'Rithmic',
|
|
325
|
-
startTime: Date.now(),
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
let running = true;
|
|
329
|
-
let stopReason = null;
|
|
330
|
-
let currentPosition = 0;
|
|
331
|
-
let pendingOrder = false;
|
|
332
|
-
let tickCount = 0;
|
|
333
|
-
|
|
334
|
-
// Initialize Strategy dynamically
|
|
335
|
-
const strategy = new strategyModule.M1({ tickSize });
|
|
336
|
-
strategy.initialize(contractId, tickSize);
|
|
337
|
-
|
|
338
|
-
// Initialize Market Data Feed
|
|
339
|
-
const marketFeed = new MarketDataFeed({ propfirm: lead.propfirm });
|
|
340
|
-
|
|
341
|
-
// Measure API latency (CLI <-> API)
|
|
342
|
-
const measureLatency = async () => {
|
|
343
|
-
try {
|
|
344
|
-
const start = Date.now();
|
|
345
|
-
await lead.service.getPositions(lead.account.accountId);
|
|
346
|
-
stats.latency = Date.now() - start;
|
|
347
|
-
} catch (e) {
|
|
348
|
-
stats.latency = 0;
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
// Log startup
|
|
353
|
-
ui.addLog('info', `Strategy: ${strategyInfo.name}`);
|
|
354
|
-
ui.addLog('info', `Lead: ${stats.leadName} -> Follower: ${stats.followerName}`);
|
|
355
|
-
ui.addLog('info', `Symbol: ${stats.leadSymbol} | Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
356
|
-
ui.addLog('info', `Params: ${strategyInfo.params.stopTicks}t stop, ${strategyInfo.params.targetTicks}t target (${strategyInfo.params.riskReward})`);
|
|
357
|
-
ui.addLog('info', 'Connecting to market data...');
|
|
358
|
-
|
|
359
|
-
// Handle strategy signals - execute on BOTH accounts
|
|
360
|
-
strategy.on('signal', async (signal) => {
|
|
361
|
-
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
362
|
-
|
|
363
|
-
const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
364
|
-
|
|
365
|
-
ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
366
|
-
|
|
367
|
-
pendingOrder = true;
|
|
368
|
-
try {
|
|
369
|
-
const orderSide = direction === 'long' ? 0 : 1;
|
|
370
|
-
|
|
371
|
-
// Place on LEAD account
|
|
372
|
-
const leadResult = await lead.service.placeOrder({
|
|
373
|
-
accountId: lead.account.accountId,
|
|
374
|
-
contractId: contractId,
|
|
375
|
-
type: 2,
|
|
376
|
-
side: orderSide,
|
|
377
|
-
size: lead.contracts
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
if (leadResult.success) {
|
|
381
|
-
ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x`);
|
|
382
|
-
|
|
383
|
-
// Place on FOLLOWER account
|
|
384
|
-
const followerResult = await follower.service.placeOrder({
|
|
385
|
-
accountId: follower.account.accountId,
|
|
386
|
-
contractId: contractId,
|
|
387
|
-
type: 2,
|
|
388
|
-
side: orderSide,
|
|
389
|
-
size: follower.contracts
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
if (followerResult.success) {
|
|
393
|
-
ui.addLog('trade', `FOLLOWER: ${direction.toUpperCase()} ${follower.contracts}x`);
|
|
394
|
-
currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
|
|
395
|
-
stats.trades++;
|
|
396
|
-
|
|
397
|
-
// Place bracket orders on both accounts
|
|
398
|
-
if (stopLoss && takeProfit) {
|
|
399
|
-
const exitSide = direction === 'long' ? 1 : 0;
|
|
400
|
-
|
|
401
|
-
// Lead SL/TP
|
|
402
|
-
await lead.service.placeOrder({
|
|
403
|
-
accountId: lead.account.accountId, contractId, type: 4, side: exitSide, size: lead.contracts, stopPrice: stopLoss
|
|
404
|
-
});
|
|
405
|
-
await lead.service.placeOrder({
|
|
406
|
-
accountId: lead.account.accountId, contractId, type: 1, side: exitSide, size: lead.contracts, limitPrice: takeProfit
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// Follower SL/TP
|
|
410
|
-
await follower.service.placeOrder({
|
|
411
|
-
accountId: follower.account.accountId, contractId, type: 4, side: exitSide, size: follower.contracts, stopPrice: stopLoss
|
|
412
|
-
});
|
|
413
|
-
await follower.service.placeOrder({
|
|
414
|
-
accountId: follower.account.accountId, contractId, type: 1, side: exitSide, size: follower.contracts, limitPrice: takeProfit
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
ui.addLog('error', `Follower order failed: ${followerResult.error}`);
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
ui.addLog('error', `Lead order failed: ${leadResult.error}`);
|
|
424
|
-
}
|
|
425
|
-
} catch (e) {
|
|
426
|
-
ui.addLog('error', `Order error: ${e.message}`);
|
|
427
|
-
}
|
|
428
|
-
pendingOrder = false;
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Handle market data ticks
|
|
432
|
-
marketFeed.on('tick', (tick) => {
|
|
433
|
-
tickCount++;
|
|
434
|
-
const latencyStart = Date.now();
|
|
435
|
-
|
|
436
|
-
strategy.processTick({
|
|
437
|
-
contractId: tick.contractId || contractId,
|
|
438
|
-
price: tick.price,
|
|
439
|
-
bid: tick.bid,
|
|
440
|
-
ask: tick.ask,
|
|
441
|
-
volume: tick.volume || 1,
|
|
442
|
-
side: tick.lastTradeSide || 'unknown',
|
|
443
|
-
timestamp: tick.timestamp || Date.now()
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
stats.latency = Date.now() - latencyStart;
|
|
447
|
-
|
|
448
|
-
if (tickCount % 100 === 0) {
|
|
449
|
-
ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
220
|
|
|
453
|
-
|
|
454
|
-
stats.connected = true;
|
|
455
|
-
ui.addLog('success', 'Market data connected!');
|
|
456
|
-
});
|
|
221
|
+
let contracts = contractsResult.contracts;
|
|
457
222
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
});
|
|
223
|
+
// Sort: Popular indices first
|
|
224
|
+
const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
461
225
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
226
|
+
contracts.sort((a, b) => {
|
|
227
|
+
const baseA = a.baseSymbol || a.symbol || '';
|
|
228
|
+
const baseB = b.baseSymbol || b.symbol || '';
|
|
229
|
+
const idxA = popularPrefixes.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
230
|
+
const idxB = popularPrefixes.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
231
|
+
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
232
|
+
if (idxA !== -1) return -1;
|
|
233
|
+
if (idxB !== -1) return 1;
|
|
234
|
+
return baseA.localeCompare(baseB);
|
|
465
235
|
});
|
|
466
236
|
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
const token = lead.service.token || lead.service.getToken?.();
|
|
470
|
-
const propfirmKey = (lead.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
471
|
-
await marketFeed.connect(token, propfirmKey, contractId);
|
|
472
|
-
await marketFeed.subscribe(lead.symbol.name, contractId);
|
|
473
|
-
} catch (e) {
|
|
474
|
-
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Poll combined P&L from both accounts
|
|
478
|
-
const pollPnL = async () => {
|
|
479
|
-
try {
|
|
480
|
-
let combinedPnL = 0;
|
|
481
|
-
|
|
482
|
-
// Lead P&L
|
|
483
|
-
const leadResult = await lead.service.getPositions(lead.account.accountId);
|
|
484
|
-
if (leadResult.success && leadResult.positions) {
|
|
485
|
-
const pos = leadResult.positions.find(p => {
|
|
486
|
-
const sym = p.contractId || p.symbol || '';
|
|
487
|
-
return sym.includes(lead.symbol.name) || sym.includes(contractId);
|
|
488
|
-
});
|
|
489
|
-
if (pos) combinedPnL += pos.profitAndLoss || 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Follower P&L
|
|
493
|
-
const followerResult = await follower.service.getPositions(follower.account.accountId);
|
|
494
|
-
if (followerResult.success && followerResult.positions) {
|
|
495
|
-
const pos = followerResult.positions.find(p => {
|
|
496
|
-
const sym = p.contractId || p.symbol || '';
|
|
497
|
-
return sym.includes(follower.symbol.name) || sym.includes(contractId);
|
|
498
|
-
});
|
|
499
|
-
if (pos) combinedPnL += pos.profitAndLoss || 0;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Update stats
|
|
503
|
-
if (combinedPnL !== stats.pnl) {
|
|
504
|
-
const diff = combinedPnL - stats.pnl;
|
|
505
|
-
if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
|
|
506
|
-
if (diff >= 0) stats.wins++;
|
|
507
|
-
else stats.losses++;
|
|
508
|
-
}
|
|
509
|
-
stats.pnl = combinedPnL;
|
|
510
|
-
|
|
511
|
-
if (stats.pnl !== 0) {
|
|
512
|
-
strategy.recordTradeResult(stats.pnl);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Check target/risk limits
|
|
517
|
-
if (stats.pnl >= dailyTarget) {
|
|
518
|
-
stopReason = 'target';
|
|
519
|
-
running = false;
|
|
520
|
-
ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
521
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
522
|
-
stopReason = 'risk';
|
|
523
|
-
running = false;
|
|
524
|
-
ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
525
|
-
}
|
|
526
|
-
} catch (e) {
|
|
527
|
-
// Silent fail - will retry
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
// UI refresh loop
|
|
532
|
-
const refreshInterval = setInterval(() => {
|
|
533
|
-
if (running) ui.render(stats);
|
|
534
|
-
}, 250);
|
|
535
|
-
|
|
536
|
-
// Measure API latency every 5 seconds
|
|
537
|
-
measureLatency();
|
|
538
|
-
const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
|
|
237
|
+
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
539
238
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const cleanupKeys = setupKeyboardHandler(() => {
|
|
546
|
-
running = false;
|
|
547
|
-
stopReason = 'manual';
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// Wait for stop
|
|
551
|
-
await new Promise((resolve) => {
|
|
552
|
-
const check = setInterval(() => {
|
|
553
|
-
if (!running) {
|
|
554
|
-
clearInterval(check);
|
|
555
|
-
resolve();
|
|
556
|
-
}
|
|
557
|
-
}, 100);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
// Cleanup
|
|
561
|
-
clearInterval(refreshInterval);
|
|
562
|
-
clearInterval(latencyInterval);
|
|
563
|
-
clearInterval(pnlInterval);
|
|
564
|
-
await marketFeed.disconnect();
|
|
565
|
-
if (cleanupKeys) cleanupKeys();
|
|
566
|
-
ui.cleanup();
|
|
239
|
+
const options = contracts.map(c => ({
|
|
240
|
+
label: `${c.symbol} - ${c.name} (${c.exchange})`,
|
|
241
|
+
value: c
|
|
242
|
+
}));
|
|
243
|
+
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
567
244
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
process.stdin.resume();
|
|
572
|
-
|
|
573
|
-
// Duration
|
|
574
|
-
const durationMs = Date.now() - stats.startTime;
|
|
575
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
576
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
577
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
578
|
-
stats.duration = hours > 0
|
|
579
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
580
|
-
: minutes > 0
|
|
581
|
-
? `${minutes}m ${seconds}s`
|
|
582
|
-
: `${seconds}s`;
|
|
583
|
-
|
|
584
|
-
// Show summary
|
|
585
|
-
renderSessionSummary(stats, stopReason);
|
|
586
|
-
await prompts.waitForEnter();
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Setup keyboard handler
|
|
591
|
-
* @param {Function} onStop - Stop callback
|
|
592
|
-
* @returns {Function|null} Cleanup function
|
|
593
|
-
*/
|
|
594
|
-
const setupKeyboardHandler = (onStop) => {
|
|
595
|
-
if (!process.stdin.isTTY) return null;
|
|
596
|
-
|
|
597
|
-
readline.emitKeypressEvents(process.stdin);
|
|
598
|
-
process.stdin.setRawMode(true);
|
|
599
|
-
process.stdin.resume();
|
|
600
|
-
|
|
601
|
-
const handler = (str, key) => {
|
|
602
|
-
if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
|
|
603
|
-
onStop();
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
process.stdin.on('keypress', handler);
|
|
608
|
-
|
|
609
|
-
return () => {
|
|
610
|
-
process.stdin.removeListener('keypress', handler);
|
|
611
|
-
if (process.stdin.isTTY) {
|
|
612
|
-
process.stdin.setRawMode(false);
|
|
613
|
-
}
|
|
614
|
-
};
|
|
245
|
+
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
246
|
+
return selected === 'back' || selected === null ? null : selected;
|
|
615
247
|
};
|
|
616
248
|
|
|
617
249
|
module.exports = { copyTradingMenu };
|