hedgequantx 2.7.86 → 2.7.87
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/pages/algo/copy-trading.js +354 -322
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Copy Trading Mode - HQX Ultra Scalping
|
|
3
|
+
* Same as One Account but copies trades to multiple followers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const chalk = require('chalk');
|
|
@@ -9,18 +9,18 @@ const readline = require('readline');
|
|
|
9
9
|
|
|
10
10
|
const { connections } = require('../../services');
|
|
11
11
|
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
12
|
-
const {
|
|
12
|
+
const { prompts } = require('../../utils');
|
|
13
13
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Strategy & Market Data
|
|
16
|
+
const { M1 } = require('../../lib/m/s1');
|
|
17
|
+
const { MarketDataFeed } = require('../../lib/data');
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Copy Trading Menu
|
|
19
21
|
*/
|
|
20
22
|
const copyTradingMenu = async () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Check market hours
|
|
23
|
+
// Check if market is open
|
|
24
24
|
const market = checkMarketHours();
|
|
25
25
|
if (!market.isOpen && !market.message.includes('early')) {
|
|
26
26
|
console.log();
|
|
@@ -30,326 +30,372 @@ const copyTradingMenu = async () => {
|
|
|
30
30
|
await prompts.waitForEnter();
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
|
|
34
|
+
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
35
|
+
|
|
36
|
+
const allAccounts = await connections.getAllAccounts();
|
|
37
|
+
|
|
38
|
+
if (!allAccounts?.length) {
|
|
39
|
+
spinner.fail('No accounts found');
|
|
40
|
+
await prompts.waitForEnter();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
45
|
+
|
|
46
|
+
if (activeAccounts.length < 2) {
|
|
47
|
+
spinner.fail(`Need at least 2 active accounts (found: ${activeAccounts.length})`);
|
|
39
48
|
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
40
|
-
console.log();
|
|
41
49
|
await prompts.waitForEnter();
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
spinner.succeed(`Found ${activeAccounts.length} active accounts`);
|
|
54
|
+
|
|
55
|
+
// Step 1: Select LEAD Account
|
|
45
56
|
console.log();
|
|
46
|
-
console.log(chalk.
|
|
57
|
+
console.log(chalk.cyan.bold(' STEP 1: SELECT LEAD ACCOUNT'));
|
|
58
|
+
const leadOptions = activeAccounts.map(acc => {
|
|
59
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
60
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
61
|
+
? ` - $${acc.balance.toLocaleString()}`
|
|
62
|
+
: '';
|
|
63
|
+
return {
|
|
64
|
+
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
65
|
+
value: acc
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
leadOptions.push({ label: '< Back', value: 'back' });
|
|
69
|
+
|
|
70
|
+
const leadAccount = await prompts.selectOption('Lead Account:', leadOptions);
|
|
71
|
+
if (!leadAccount || leadAccount === 'back') return;
|
|
72
|
+
|
|
73
|
+
// Step 2: Select FOLLOWER Account(s)
|
|
47
74
|
console.log();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
console.log(chalk.yellow.bold(' STEP 2: SELECT FOLLOWER ACCOUNT(S)'));
|
|
76
|
+
console.log(chalk.gray(' (Select accounts to copy trades to)'));
|
|
77
|
+
|
|
78
|
+
const followers = [];
|
|
79
|
+
const availableFollowers = activeAccounts.filter(a => a.accountId !== leadAccount.accountId);
|
|
80
|
+
|
|
81
|
+
while (availableFollowers.length > 0) {
|
|
82
|
+
const remaining = availableFollowers.filter(a => !followers.find(f => f.accountId === a.accountId));
|
|
83
|
+
if (remaining.length === 0) break;
|
|
84
|
+
|
|
85
|
+
const followerOptions = remaining.map(acc => {
|
|
86
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
87
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
88
|
+
? ` - $${acc.balance.toLocaleString()}`
|
|
89
|
+
: '';
|
|
90
|
+
return {
|
|
91
|
+
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
92
|
+
value: acc
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (followers.length > 0) {
|
|
97
|
+
followerOptions.push({ label: chalk.green('✓ Done selecting followers'), value: 'done' });
|
|
98
|
+
}
|
|
99
|
+
followerOptions.push({ label: '< Back', value: 'back' });
|
|
100
|
+
|
|
101
|
+
const msg = followers.length === 0 ? 'Select Follower:' : `Add another follower (${followers.length} selected):`;
|
|
102
|
+
const selected = await prompts.selectOption(msg, followerOptions);
|
|
103
|
+
|
|
104
|
+
if (!selected || selected === 'back') {
|
|
105
|
+
if (followers.length === 0) return;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (selected === 'done') break;
|
|
109
|
+
|
|
110
|
+
followers.push(selected);
|
|
111
|
+
console.log(chalk.green(` ✓ Added: ${selected.accountName || selected.accountId}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (followers.length === 0) {
|
|
115
|
+
console.log(chalk.red(' No followers selected'));
|
|
55
116
|
await prompts.waitForEnter();
|
|
56
117
|
return;
|
|
57
118
|
}
|
|
58
|
-
|
|
59
|
-
spinner.succeed(`Found ${allAccounts.length} active accounts`);
|
|
60
|
-
|
|
61
|
-
// Step 1: Select Lead Account
|
|
62
|
-
console.log(chalk.cyan(' Step 1: Select LEAD Account'));
|
|
63
|
-
const leadIdx = await selectAccount('Lead Account:', allAccounts, -1);
|
|
64
|
-
if (leadIdx === null || leadIdx === -1) return;
|
|
65
|
-
const lead = allAccounts[leadIdx];
|
|
66
|
-
|
|
67
|
-
// Step 2: Select Follower Account
|
|
68
|
-
console.log();
|
|
69
|
-
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
|
|
70
|
-
const followerIdx = await selectAccount('Follower Account:', allAccounts, leadIdx);
|
|
71
|
-
if (followerIdx === null || followerIdx === -1) return;
|
|
72
|
-
const follower = allAccounts[followerIdx];
|
|
73
|
-
|
|
119
|
+
|
|
74
120
|
// Step 3: Select Symbol
|
|
75
121
|
console.log();
|
|
76
|
-
console.log(chalk.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
console.log(chalk.magenta.bold(' STEP 3: SELECT SYMBOL'));
|
|
123
|
+
const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
|
|
124
|
+
const contract = await selectSymbol(leadService);
|
|
125
|
+
if (!contract) return;
|
|
126
|
+
|
|
80
127
|
// Step 4: Configure Parameters
|
|
81
128
|
console.log();
|
|
82
|
-
console.log(chalk.cyan('
|
|
83
|
-
|
|
129
|
+
console.log(chalk.cyan.bold(' STEP 4: CONFIGURE PARAMETERS'));
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
84
132
|
const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
|
|
85
133
|
if (leadContracts === null) return;
|
|
86
|
-
|
|
87
|
-
const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
|
|
134
|
+
|
|
135
|
+
const followerContracts = await prompts.numberInput('Follower contracts (each):', leadContracts, 1, 10);
|
|
88
136
|
if (followerContracts === null) return;
|
|
89
|
-
|
|
137
|
+
|
|
90
138
|
const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
|
|
91
139
|
if (dailyTarget === null) return;
|
|
92
|
-
|
|
140
|
+
|
|
93
141
|
const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
|
|
94
142
|
if (maxRisk === null) return;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const showNames = await prompts.selectOption('Account names:', [
|
|
98
|
-
{ label: 'Hide account names', value: false },
|
|
99
|
-
{ label: 'Show account names', value: true },
|
|
100
|
-
]);
|
|
143
|
+
|
|
144
|
+
const showNames = await prompts.confirmPrompt('Show account names?', false);
|
|
101
145
|
if (showNames === null) return;
|
|
102
|
-
|
|
103
|
-
//
|
|
146
|
+
|
|
147
|
+
// Summary
|
|
104
148
|
console.log();
|
|
105
|
-
console.log(chalk.white('
|
|
106
|
-
console.log(chalk.cyan(` Symbol: ${
|
|
107
|
-
console.log(chalk.cyan(` Lead: ${
|
|
108
|
-
console.log(chalk.
|
|
149
|
+
console.log(chalk.white.bold(' SUMMARY:'));
|
|
150
|
+
console.log(chalk.cyan(` Symbol: ${contract.name}`));
|
|
151
|
+
console.log(chalk.cyan(` Lead: ${leadAccount.propfirm} x${leadContracts}`));
|
|
152
|
+
console.log(chalk.yellow(` Followers (${followers.length}):`));
|
|
153
|
+
for (const f of followers) {
|
|
154
|
+
console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
|
|
155
|
+
}
|
|
109
156
|
console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
110
157
|
console.log();
|
|
111
|
-
|
|
158
|
+
|
|
112
159
|
const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
|
|
113
160
|
if (!confirm) return;
|
|
114
|
-
|
|
115
|
-
// Launch
|
|
161
|
+
|
|
116
162
|
await launchCopyTrading({
|
|
117
|
-
lead: {
|
|
118
|
-
|
|
163
|
+
lead: { account: leadAccount, contracts: leadContracts },
|
|
164
|
+
followers: followers.map(f => ({ account: f, contracts: followerContracts })),
|
|
165
|
+
contract,
|
|
119
166
|
dailyTarget,
|
|
120
167
|
maxRisk,
|
|
121
|
-
showNames
|
|
168
|
+
showNames
|
|
122
169
|
});
|
|
123
170
|
};
|
|
124
171
|
|
|
125
172
|
/**
|
|
126
|
-
*
|
|
127
|
-
* @param {Array} allConns - All connections
|
|
128
|
-
* @returns {Promise<Array>}
|
|
129
|
-
*/
|
|
130
|
-
const fetchAllAccounts = async (allConns) => {
|
|
131
|
-
const allAccounts = [];
|
|
132
|
-
|
|
133
|
-
for (const conn of allConns) {
|
|
134
|
-
try {
|
|
135
|
-
const result = await conn.service.getTradingAccounts();
|
|
136
|
-
if (result.success && result.accounts) {
|
|
137
|
-
const active = result.accounts.filter(a => a.status === 0);
|
|
138
|
-
for (const acc of active) {
|
|
139
|
-
allAccounts.push({
|
|
140
|
-
account: acc,
|
|
141
|
-
service: conn.service,
|
|
142
|
-
propfirm: conn.propfirm,
|
|
143
|
-
type: conn.type,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} catch (err) {
|
|
148
|
-
log.warn('Failed to get accounts', { type: conn.type, error: err.message });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return allAccounts;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Select account from list
|
|
157
|
-
* @param {string} message - Prompt message
|
|
158
|
-
* @param {Array} accounts - Available accounts
|
|
159
|
-
* @param {number} excludeIdx - Index to exclude
|
|
160
|
-
* @returns {Promise<number|null>}
|
|
161
|
-
*/
|
|
162
|
-
const selectAccount = async (message, accounts, excludeIdx) => {
|
|
163
|
-
const options = accounts
|
|
164
|
-
.map((a, i) => ({ a, i }))
|
|
165
|
-
.filter(x => x.i !== excludeIdx)
|
|
166
|
-
.map(x => {
|
|
167
|
-
const acc = x.a.account;
|
|
168
|
-
const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
|
|
169
|
-
return {
|
|
170
|
-
label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
|
|
171
|
-
value: x.i,
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
options.push({ label: '< Cancel', value: -1 });
|
|
176
|
-
return prompts.selectOption(message, options);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Select trading symbol
|
|
181
|
-
* @param {Object} service - Service instance
|
|
182
|
-
* @returns {Promise<Object|null>}
|
|
173
|
+
* Symbol selection - sorted with popular indices first
|
|
183
174
|
*/
|
|
184
175
|
const selectSymbol = async (service) => {
|
|
185
176
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Fallback to service
|
|
192
|
-
if (!contracts && typeof service.getContracts === 'function') {
|
|
193
|
-
const result = await service.getContracts();
|
|
194
|
-
if (result.success && result.contracts?.length > 0) {
|
|
195
|
-
contracts = result.contracts;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!contracts || !contracts.length) {
|
|
200
|
-
spinner.fail('No contracts available');
|
|
201
|
-
await prompts.waitForEnter();
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
206
|
-
|
|
207
|
-
// Build options from RAW API data - no static mapping
|
|
208
|
-
const options = [];
|
|
209
|
-
let currentGroup = null;
|
|
210
|
-
|
|
211
|
-
for (const c of contracts) {
|
|
212
|
-
// Use RAW API field: contractGroup
|
|
213
|
-
if (c.contractGroup && c.contractGroup !== currentGroup) {
|
|
214
|
-
currentGroup = c.contractGroup;
|
|
215
|
-
options.push({
|
|
216
|
-
label: chalk.cyan.bold(`── ${currentGroup} ──`),
|
|
217
|
-
value: null,
|
|
218
|
-
disabled: true,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Use RAW API fields: symbol (trading symbol), name (product name), exchange
|
|
223
|
-
const label = ` ${c.symbol} - ${c.name} (${c.exchange})`;
|
|
224
|
-
options.push({ label, value: c });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
options.push({ label: '', value: null, disabled: true });
|
|
228
|
-
options.push({ label: chalk.gray('< Cancel'), value: null });
|
|
229
|
-
|
|
230
|
-
return prompts.selectOption('Trading Symbol:', options);
|
|
231
|
-
} catch (err) {
|
|
232
|
-
spinner.fail(`Error loading contracts: ${err.message}`);
|
|
233
|
-
await prompts.waitForEnter();
|
|
177
|
+
|
|
178
|
+
const contractsResult = await service.getContracts();
|
|
179
|
+
if (!contractsResult.success || !contractsResult.contracts?.length) {
|
|
180
|
+
spinner.fail('Failed to load contracts');
|
|
234
181
|
return null;
|
|
235
182
|
}
|
|
183
|
+
|
|
184
|
+
let contracts = contractsResult.contracts;
|
|
185
|
+
|
|
186
|
+
// Sort: Popular indices first
|
|
187
|
+
const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
188
|
+
|
|
189
|
+
contracts.sort((a, b) => {
|
|
190
|
+
const baseA = a.baseSymbol || a.symbol || '';
|
|
191
|
+
const baseB = b.baseSymbol || b.symbol || '';
|
|
192
|
+
const idxA = popularPrefixes.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
193
|
+
const idxB = popularPrefixes.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
194
|
+
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
195
|
+
if (idxA !== -1) return -1;
|
|
196
|
+
if (idxB !== -1) return 1;
|
|
197
|
+
return baseA.localeCompare(baseB);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
201
|
+
|
|
202
|
+
const options = contracts.map(c => ({
|
|
203
|
+
label: `${c.symbol} - ${c.name} (${c.exchange})`,
|
|
204
|
+
value: c
|
|
205
|
+
}));
|
|
206
|
+
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
207
|
+
|
|
208
|
+
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
209
|
+
return selected === 'back' || selected === null ? null : selected;
|
|
236
210
|
};
|
|
237
211
|
|
|
238
212
|
/**
|
|
239
|
-
*
|
|
240
|
-
* @returns {Promise<Array|null>}
|
|
241
|
-
*/
|
|
242
|
-
const getContractsFromAPI = async () => {
|
|
243
|
-
const allConns = connections.getAll();
|
|
244
|
-
const rithmicConn = allConns.find(c => c.type === 'rithmic');
|
|
245
|
-
|
|
246
|
-
if (rithmicConn && typeof rithmicConn.service.getContracts === 'function') {
|
|
247
|
-
const result = await rithmicConn.service.getContracts();
|
|
248
|
-
if (result.success && result.contracts?.length > 0) {
|
|
249
|
-
// Return RAW API data - no mapping
|
|
250
|
-
return result.contracts;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return null;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Launch Copy Trading session
|
|
259
|
-
* @param {Object} config - Session configuration
|
|
213
|
+
* Launch Copy Trading - HQX Ultra Scalping with trade copying
|
|
260
214
|
*/
|
|
261
215
|
const launchCopyTrading = async (config) => {
|
|
262
|
-
const { lead,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
|
|
216
|
+
const { lead, followers, contract, dailyTarget, maxRisk, showNames } = config;
|
|
217
|
+
|
|
218
|
+
const leadAccount = lead.account;
|
|
219
|
+
const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
|
|
220
|
+
const leadName = showNames
|
|
221
|
+
? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
|
|
222
|
+
: 'HQX Lead *****';
|
|
223
|
+
const symbolName = contract.name;
|
|
224
|
+
const contractId = contract.id;
|
|
225
|
+
const tickSize = contract.tickSize || 0.25;
|
|
226
|
+
|
|
227
|
+
const followerNames = followers.map((f, i) =>
|
|
228
|
+
showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
|
|
229
|
+
);
|
|
230
|
+
|
|
268
231
|
const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
|
|
269
|
-
|
|
232
|
+
|
|
270
233
|
const stats = {
|
|
271
|
-
leadName,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
followerQty: follower.contracts,
|
|
234
|
+
accountName: leadName,
|
|
235
|
+
followerNames,
|
|
236
|
+
symbol: symbolName,
|
|
237
|
+
qty: lead.contracts,
|
|
238
|
+
followerQty: followers[0]?.contracts || lead.contracts,
|
|
277
239
|
target: dailyTarget,
|
|
278
240
|
risk: maxRisk,
|
|
241
|
+
propfirm: leadAccount.propfirm || 'Unknown',
|
|
242
|
+
platform: leadAccount.platform || 'Rithmic',
|
|
279
243
|
pnl: 0,
|
|
244
|
+
followerPnl: 0,
|
|
280
245
|
trades: 0,
|
|
281
246
|
wins: 0,
|
|
282
247
|
losses: 0,
|
|
283
248
|
latency: 0,
|
|
284
249
|
connected: false,
|
|
285
|
-
|
|
250
|
+
startTime: Date.now(),
|
|
251
|
+
followersCount: followers.length
|
|
286
252
|
};
|
|
287
|
-
|
|
253
|
+
|
|
288
254
|
let running = true;
|
|
289
255
|
let stopReason = null;
|
|
256
|
+
let startingPnL = null;
|
|
257
|
+
let currentPosition = 0;
|
|
258
|
+
let pendingOrder = false;
|
|
259
|
+
let tickCount = 0;
|
|
290
260
|
|
|
291
|
-
//
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const start = Date.now();
|
|
295
|
-
await lead.service.getPositions(lead.account.accountId);
|
|
296
|
-
stats.latency = Date.now() - start;
|
|
297
|
-
} catch (e) {
|
|
298
|
-
stats.latency = 0;
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// Local copy trading - no external server needed
|
|
303
|
-
ui.addLog('info', `Starting copy trading on ${stats.platform}...`);
|
|
304
|
-
ui.addLog('info', `Lead: ${stats.leadName} -> Follower: ${stats.followerName}`);
|
|
305
|
-
ui.addLog('info', `Symbol: ${stats.symbol} | Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
306
|
-
stats.connected = true;
|
|
261
|
+
// Initialize Strategy
|
|
262
|
+
const strategy = new M1({ tickSize });
|
|
263
|
+
strategy.initialize(contractId, tickSize);
|
|
307
264
|
|
|
308
|
-
//
|
|
309
|
-
|
|
265
|
+
// Initialize Market Data Feed
|
|
266
|
+
const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
|
|
310
267
|
|
|
311
|
-
|
|
268
|
+
// Log startup
|
|
269
|
+
ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
|
|
270
|
+
ui.addLog('info', `Symbol: ${symbolName} | Lead Qty: ${lead.contracts} | Follower Qty: ${followers[0]?.contracts}`);
|
|
271
|
+
ui.addLog('info', `Target: $${dailyTarget} | Max Risk: $${maxRisk}`);
|
|
272
|
+
ui.addLog('info', 'Connecting to market data...');
|
|
273
|
+
|
|
274
|
+
// Handle strategy signals - place on lead AND all followers
|
|
275
|
+
strategy.on('signal', async (signal) => {
|
|
276
|
+
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
277
|
+
|
|
278
|
+
const { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
279
|
+
|
|
280
|
+
ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
281
|
+
|
|
282
|
+
pendingOrder = true;
|
|
312
283
|
try {
|
|
313
|
-
|
|
314
|
-
const leadResult = await lead.service.getPositions(lead.account.accountId);
|
|
315
|
-
if (!leadResult.success) return;
|
|
284
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
316
285
|
|
|
317
|
-
|
|
286
|
+
// Place order on LEAD
|
|
287
|
+
const leadResult = await leadService.placeOrder({
|
|
288
|
+
accountId: leadAccount.accountId,
|
|
289
|
+
contractId: contractId,
|
|
290
|
+
type: 2,
|
|
291
|
+
side: orderSide,
|
|
292
|
+
size: lead.contracts
|
|
293
|
+
});
|
|
318
294
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
295
|
+
if (leadResult.success) {
|
|
296
|
+
currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
|
|
297
|
+
stats.trades++;
|
|
298
|
+
ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x @ market`);
|
|
299
|
+
|
|
300
|
+
// Place orders on ALL FOLLOWERS
|
|
301
|
+
for (let i = 0; i < followers.length; i++) {
|
|
302
|
+
const f = followers[i];
|
|
303
|
+
const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const fResult = await fService.placeOrder({
|
|
307
|
+
accountId: f.account.accountId,
|
|
308
|
+
contractId: contractId,
|
|
309
|
+
type: 2,
|
|
310
|
+
side: orderSide,
|
|
311
|
+
size: f.contracts
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (fResult.success) {
|
|
315
|
+
ui.addLog('trade', `FOLLOWER ${i + 1}: ${direction.toUpperCase()} ${f.contracts}x @ market`);
|
|
316
|
+
} else {
|
|
317
|
+
ui.addLog('error', `FOLLOWER ${i + 1}: Order failed`);
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
ui.addLog('error', `FOLLOWER ${i + 1}: ${e.message}`);
|
|
321
|
+
}
|
|
326
322
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
323
|
+
|
|
324
|
+
// Place bracket orders on lead (SL/TP)
|
|
325
|
+
if (stopLoss && takeProfit) {
|
|
326
|
+
await leadService.placeOrder({
|
|
327
|
+
accountId: leadAccount.accountId, contractId, type: 4,
|
|
328
|
+
side: direction === 'long' ? 1 : 0, size: lead.contracts, stopPrice: stopLoss
|
|
329
|
+
});
|
|
330
|
+
await leadService.placeOrder({
|
|
331
|
+
accountId: leadAccount.accountId, contractId, type: 1,
|
|
332
|
+
side: direction === 'long' ? 1 : 0, size: lead.contracts, limitPrice: takeProfit
|
|
333
|
+
});
|
|
334
|
+
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
335
335
|
}
|
|
336
|
+
} else {
|
|
337
|
+
ui.addLog('error', `Lead order failed: ${leadResult.error}`);
|
|
336
338
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
339
|
+
} catch (e) {
|
|
340
|
+
ui.addLog('error', `Order error: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
pendingOrder = false;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Handle market data ticks
|
|
346
|
+
marketFeed.on('tick', (tick) => {
|
|
347
|
+
tickCount++;
|
|
348
|
+
const latencyStart = Date.now();
|
|
349
|
+
|
|
350
|
+
strategy.processTick({
|
|
351
|
+
contractId: tick.contractId || contractId,
|
|
352
|
+
price: tick.price,
|
|
353
|
+
bid: tick.bid,
|
|
354
|
+
ask: tick.ask,
|
|
355
|
+
volume: tick.volume || 1,
|
|
356
|
+
side: tick.lastTradeSide || 'unknown',
|
|
357
|
+
timestamp: tick.timestamp || Date.now()
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
stats.latency = Date.now() - latencyStart;
|
|
361
|
+
|
|
362
|
+
if (tickCount % 100 === 0) {
|
|
363
|
+
ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
marketFeed.on('connected', () => {
|
|
368
|
+
stats.connected = true;
|
|
369
|
+
ui.addLog('success', 'Market data connected!');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
373
|
+
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market data disconnected'); });
|
|
374
|
+
|
|
375
|
+
// Connect to market data
|
|
376
|
+
try {
|
|
377
|
+
const token = leadService.token || leadService.getToken?.();
|
|
378
|
+
const propfirmKey = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
379
|
+
await marketFeed.connect(token, propfirmKey, contractId);
|
|
380
|
+
await marketFeed.subscribe(symbolName, contractId);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Poll P&L from lead and followers
|
|
386
|
+
const pollPnL = async () => {
|
|
387
|
+
try {
|
|
388
|
+
// Lead P&L
|
|
389
|
+
const leadResult = await leadService.getTradingAccounts();
|
|
390
|
+
if (leadResult.success && leadResult.accounts) {
|
|
391
|
+
const acc = leadResult.accounts.find(a => a.accountId === leadAccount.accountId);
|
|
392
|
+
if (acc && acc.profitAndLoss !== undefined) {
|
|
393
|
+
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
394
|
+
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
348
395
|
}
|
|
349
|
-
stats.pnl = leadPnL;
|
|
350
396
|
}
|
|
351
397
|
|
|
352
|
-
// Check target/risk
|
|
398
|
+
// Check target/risk
|
|
353
399
|
if (stats.pnl >= dailyTarget) {
|
|
354
400
|
stopReason = 'target';
|
|
355
401
|
running = false;
|
|
@@ -357,80 +403,66 @@ const launchCopyTrading = async (config) => {
|
|
|
357
403
|
} else if (stats.pnl <= -maxRisk) {
|
|
358
404
|
stopReason = 'risk';
|
|
359
405
|
running = false;
|
|
360
|
-
ui.addLog('error', `MAX RISK
|
|
406
|
+
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
361
407
|
}
|
|
362
|
-
} catch (e) {
|
|
363
|
-
// Silent fail - will retry
|
|
364
|
-
}
|
|
408
|
+
} catch (e) { /* silent */ }
|
|
365
409
|
};
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
const refreshInterval = setInterval(() => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
|
|
411
|
+
// Start intervals
|
|
412
|
+
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
413
|
+
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
414
|
+
pollPnL();
|
|
415
|
+
|
|
416
|
+
// Keyboard handler
|
|
417
|
+
const setupKeyHandler = () => {
|
|
418
|
+
if (!process.stdin.isTTY) return;
|
|
419
|
+
readline.emitKeypressEvents(process.stdin);
|
|
420
|
+
process.stdin.setRawMode(true);
|
|
421
|
+
process.stdin.resume();
|
|
422
|
+
|
|
423
|
+
const onKey = (str, key) => {
|
|
424
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
425
|
+
running = false;
|
|
426
|
+
stopReason = 'manual';
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
process.stdin.on('keypress', onKey);
|
|
430
|
+
return () => {
|
|
431
|
+
process.stdin.removeListener('keypress', onKey);
|
|
432
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const cleanupKeys = setupKeyHandler();
|
|
437
|
+
|
|
386
438
|
// Wait for stop
|
|
387
|
-
await new Promise(
|
|
439
|
+
await new Promise(resolve => {
|
|
388
440
|
const check = setInterval(() => {
|
|
389
|
-
if (!running) {
|
|
390
|
-
clearInterval(check);
|
|
391
|
-
resolve();
|
|
392
|
-
}
|
|
441
|
+
if (!running) { clearInterval(check); resolve(); }
|
|
393
442
|
}, 100);
|
|
394
443
|
});
|
|
395
|
-
|
|
444
|
+
|
|
396
445
|
// Cleanup
|
|
397
446
|
clearInterval(refreshInterval);
|
|
398
|
-
clearInterval(
|
|
399
|
-
|
|
447
|
+
clearInterval(pnlInterval);
|
|
448
|
+
await marketFeed.disconnect();
|
|
400
449
|
if (cleanupKeys) cleanupKeys();
|
|
401
450
|
ui.cleanup();
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
renderSessionSummary(stats, stopReason);
|
|
405
|
-
await prompts.waitForEnter();
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Setup keyboard handler
|
|
410
|
-
* @param {Function} onStop - Stop callback
|
|
411
|
-
* @returns {Function|null} Cleanup function
|
|
412
|
-
*/
|
|
413
|
-
const setupKeyboardHandler = (onStop) => {
|
|
414
|
-
if (!process.stdin.isTTY) return null;
|
|
415
|
-
|
|
416
|
-
readline.emitKeypressEvents(process.stdin);
|
|
417
|
-
process.stdin.setRawMode(true);
|
|
451
|
+
|
|
452
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
418
453
|
process.stdin.resume();
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
process.stdin.setRawMode(false);
|
|
432
|
-
}
|
|
433
|
-
};
|
|
454
|
+
|
|
455
|
+
// Duration
|
|
456
|
+
const durationMs = Date.now() - stats.startTime;
|
|
457
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
458
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
459
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
460
|
+
stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
461
|
+
|
|
462
|
+
renderSessionSummary(stats, stopReason);
|
|
463
|
+
|
|
464
|
+
console.log('\n Returning to menu in 3 seconds...');
|
|
465
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
434
466
|
};
|
|
435
467
|
|
|
436
468
|
module.exports = { copyTradingMenu };
|