hedgequantx 2.9.19 → 2.9.20
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 +42 -64
- package/src/lib/m/hqx-2b.js +7 -0
- package/src/lib/m/index.js +138 -0
- package/src/lib/m/ultra-scalping.js +7 -0
- package/src/menus/connect.js +14 -17
- package/src/menus/dashboard.js +58 -76
- package/src/pages/accounts.js +38 -49
- package/src/pages/algo/copy-trading.js +546 -178
- package/src/pages/algo/index.js +18 -75
- package/src/pages/algo/one-account.js +322 -57
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +19 -22
- package/src/pages/positions.js +19 -22
- package/src/pages/stats/index.js +15 -16
- package/src/pages/user.js +7 -11
- package/src/services/ai-supervision/health.js +35 -47
- package/src/services/index.js +1 -9
- package/src/services/rithmic/accounts.js +8 -6
- package/src/ui/box.js +9 -5
- package/src/ui/index.js +5 -18
- package/src/ui/menu.js +4 -4
- package/src/pages/ai-agents-ui.js +0 -388
- package/src/pages/ai-agents.js +0 -494
- package/src/pages/ai-models.js +0 -389
- package/src/pages/algo/algo-executor.js +0 -307
- package/src/pages/algo/copy-executor.js +0 -331
- package/src/pages/algo/custom-strategy.js +0 -313
- package/src/services/ai-supervision/consensus.js +0 -284
- package/src/services/ai-supervision/context.js +0 -275
- package/src/services/ai-supervision/directive.js +0 -167
- package/src/services/ai-supervision/index.js +0 -359
- package/src/services/ai-supervision/parser.js +0 -278
- package/src/services/ai-supervision/symbols.js +0 -259
- package/src/services/cliproxy/index.js +0 -256
- package/src/services/cliproxy/installer.js +0 -111
- package/src/services/cliproxy/manager.js +0 -387
- package/src/services/llmproxy/index.js +0 -166
- package/src/services/llmproxy/manager.js +0 -411
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copy Trading Mode
|
|
3
|
-
*
|
|
4
|
-
* Supports multi-agent AI supervision
|
|
2
|
+
* @fileoverview Copy Trading Mode with Strategy Selection
|
|
3
|
+
* @module pages/algo/copy-trading
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
const chalk = require('chalk');
|
|
8
7
|
const ora = require('ora');
|
|
8
|
+
const readline = require('readline');
|
|
9
9
|
|
|
10
10
|
const { connections } = require('../../services');
|
|
11
|
-
const {
|
|
11
|
+
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
12
|
+
const { logger, prompts } = require('../../utils');
|
|
12
13
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const {
|
|
14
|
+
|
|
15
|
+
// Strategy Registry & Market Data
|
|
16
|
+
const { getAvailableStrategies, loadStrategy, getStrategy } = require('../../lib/m');
|
|
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
|
+
|
|
16
39
|
|
|
17
40
|
/**
|
|
18
41
|
* Copy Trading Menu
|
|
19
42
|
*/
|
|
20
43
|
const copyTradingMenu = async () => {
|
|
21
|
-
|
|
44
|
+
log.info('Copy Trading menu opened');
|
|
45
|
+
|
|
46
|
+
// Check market hours
|
|
22
47
|
const market = checkMarketHours();
|
|
23
48
|
if (!market.isOpen && !market.message.includes('early')) {
|
|
24
49
|
console.log();
|
|
@@ -28,222 +53,565 @@ const copyTradingMenu = async () => {
|
|
|
28
53
|
await prompts.waitForEnter();
|
|
29
54
|
return;
|
|
30
55
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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})`);
|
|
56
|
+
|
|
57
|
+
const allConns = connections.getAll();
|
|
58
|
+
|
|
59
|
+
if (allConns.length < 2) {
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(chalk.yellow(` Copy Trading requires 2 connected accounts (found: ${allConns.length})`));
|
|
46
62
|
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
63
|
+
console.log();
|
|
47
64
|
await prompts.waitForEnter();
|
|
48
65
|
return;
|
|
49
66
|
}
|
|
50
|
-
|
|
51
|
-
spinner.succeed(`Found ${activeAccounts.length} active accounts`);
|
|
52
|
-
|
|
53
|
-
// Step 1: Select LEAD Account
|
|
67
|
+
|
|
54
68
|
console.log();
|
|
55
|
-
console.log(chalk.
|
|
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)
|
|
69
|
+
console.log(chalk.yellow.bold(' Copy Trading Setup'));
|
|
72
70
|
console.log();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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'));
|
|
71
|
+
|
|
72
|
+
// Fetch all accounts
|
|
73
|
+
const spinner = ora({ text: 'Fetching accounts...', color: 'yellow' }).start();
|
|
74
|
+
const allAccounts = await fetchAllAccounts(allConns);
|
|
75
|
+
|
|
76
|
+
if (allAccounts.length < 2) {
|
|
77
|
+
spinner.fail('Need at least 2 active accounts');
|
|
114
78
|
await prompts.waitForEnter();
|
|
115
79
|
return;
|
|
116
80
|
}
|
|
117
|
-
|
|
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
|
+
|
|
118
97
|
// Step 3: Select Symbol
|
|
119
98
|
console.log();
|
|
120
|
-
console.log(chalk.
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Step 4: Configure Parameters
|
|
99
|
+
console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
|
|
100
|
+
const symbol = await selectSymbol(lead.service);
|
|
101
|
+
if (!symbol) return;
|
|
102
|
+
|
|
103
|
+
// Step 4: Select Strategy
|
|
126
104
|
console.log();
|
|
127
|
-
console.log(chalk.cyan
|
|
105
|
+
console.log(chalk.cyan(' Step 4: Select Trading Strategy'));
|
|
106
|
+
const strategyId = await selectStrategy();
|
|
107
|
+
if (!strategyId) return;
|
|
108
|
+
|
|
109
|
+
// Step 5: Configure Parameters
|
|
128
110
|
console.log();
|
|
129
|
-
|
|
111
|
+
console.log(chalk.cyan(' Step 5: Configure Parameters'));
|
|
112
|
+
|
|
130
113
|
const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
|
|
131
114
|
if (leadContracts === null) return;
|
|
132
|
-
|
|
133
|
-
const followerContracts = await prompts.numberInput('Follower contracts
|
|
115
|
+
|
|
116
|
+
const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
|
|
134
117
|
if (followerContracts === null) return;
|
|
135
|
-
|
|
118
|
+
|
|
136
119
|
const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
|
|
137
120
|
if (dailyTarget === null) return;
|
|
138
|
-
|
|
121
|
+
|
|
139
122
|
const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
|
|
140
123
|
if (maxRisk === null) return;
|
|
141
|
-
|
|
142
|
-
|
|
124
|
+
|
|
125
|
+
// Step 6: Privacy
|
|
126
|
+
const showNames = await prompts.selectOption('Account names:', [
|
|
127
|
+
{ label: 'Hide account names', value: false },
|
|
128
|
+
{ label: 'Show account names', value: true },
|
|
129
|
+
]);
|
|
143
130
|
if (showNames === null) return;
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
const
|
|
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
|
|
131
|
+
|
|
132
|
+
// Confirm
|
|
133
|
+
const strategyInfo = getStrategy(strategyId);
|
|
183
134
|
console.log();
|
|
184
|
-
console.log(chalk.white
|
|
185
|
-
console.log(chalk.cyan(`
|
|
186
|
-
console.log(chalk.cyan(`
|
|
187
|
-
console.log(chalk.
|
|
188
|
-
|
|
189
|
-
console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
|
|
190
|
-
}
|
|
135
|
+
console.log(chalk.white(' Summary:'));
|
|
136
|
+
console.log(chalk.cyan(` Strategy: ${strategyInfo.name}`));
|
|
137
|
+
console.log(chalk.cyan(` Symbol: ${symbol.name}`));
|
|
138
|
+
console.log(chalk.cyan(` Lead: ${lead.propfirm} x${leadContracts}`));
|
|
139
|
+
console.log(chalk.cyan(` Follower: ${follower.propfirm} x${followerContracts}`));
|
|
191
140
|
console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
192
|
-
if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
|
|
193
141
|
console.log();
|
|
194
|
-
|
|
142
|
+
|
|
195
143
|
const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
|
|
196
144
|
if (!confirm) return;
|
|
197
|
-
|
|
145
|
+
|
|
146
|
+
// Launch
|
|
198
147
|
await launchCopyTrading({
|
|
199
|
-
lead: {
|
|
200
|
-
|
|
201
|
-
|
|
148
|
+
lead: { ...lead, symbol, contracts: leadContracts },
|
|
149
|
+
follower: { ...follower, symbol, contracts: followerContracts },
|
|
150
|
+
strategyId,
|
|
202
151
|
dailyTarget,
|
|
203
152
|
maxRisk,
|
|
204
153
|
showNames,
|
|
205
|
-
supervisionConfig
|
|
206
154
|
});
|
|
207
155
|
};
|
|
208
156
|
|
|
209
157
|
/**
|
|
210
|
-
*
|
|
158
|
+
* Fetch all active accounts from connections
|
|
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>}
|
|
211
215
|
*/
|
|
212
216
|
const selectSymbol = async (service) => {
|
|
213
217
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Try Rithmic API first for consistency
|
|
221
|
+
let contracts = await getContractsFromAPI();
|
|
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();
|
|
218
266
|
return null;
|
|
219
267
|
}
|
|
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
|
+
});
|
|
220
452
|
|
|
221
|
-
|
|
453
|
+
marketFeed.on('connected', () => {
|
|
454
|
+
stats.connected = true;
|
|
455
|
+
ui.addLog('success', 'Market data connected!');
|
|
456
|
+
});
|
|
222
457
|
|
|
223
|
-
|
|
224
|
-
|
|
458
|
+
marketFeed.on('error', (err) => {
|
|
459
|
+
ui.addLog('error', `Market: ${err.message}`);
|
|
460
|
+
});
|
|
225
461
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
462
|
+
marketFeed.on('disconnected', () => {
|
|
463
|
+
stats.connected = false;
|
|
464
|
+
ui.addLog('error', 'Market data disconnected');
|
|
235
465
|
});
|
|
236
466
|
|
|
237
|
-
|
|
467
|
+
// Connect to market data
|
|
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
|
+
}
|
|
238
476
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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);
|
|
539
|
+
|
|
540
|
+
// Poll P&L every 2 seconds
|
|
541
|
+
pollPnL();
|
|
542
|
+
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
543
|
+
|
|
544
|
+
// Keyboard handling
|
|
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();
|
|
244
567
|
|
|
245
|
-
|
|
246
|
-
|
|
568
|
+
if (process.stdin.isTTY) {
|
|
569
|
+
process.stdin.setRawMode(false);
|
|
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
|
+
};
|
|
247
615
|
};
|
|
248
616
|
|
|
249
617
|
module.exports = { copyTradingMenu };
|