hedgequantx 2.7.86 → 2.7.88
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/src/pages/algo/custom-strategy.js +394 -0
- package/src/pages/algo/index.js +1 -23
- package/src/ui/index.js +1 -1
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 };
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Strategy - AI-powered modular strategy builder
|
|
3
|
+
* Each strategy is a folder with modular components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const ora = require('ora');
|
|
11
|
+
|
|
12
|
+
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
13
|
+
const { prompts } = require('../../utils');
|
|
14
|
+
const { getActiveProvider } = require('../ai-agents');
|
|
15
|
+
const cliproxy = require('../../services/cliproxy');
|
|
16
|
+
|
|
17
|
+
// Base strategies directory
|
|
18
|
+
const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
|
|
19
|
+
|
|
20
|
+
/** Ensure strategies directory exists */
|
|
21
|
+
const ensureStrategiesDir = () => {
|
|
22
|
+
if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Load all saved strategies (folders) */
|
|
26
|
+
const loadStrategies = () => {
|
|
27
|
+
ensureStrategiesDir();
|
|
28
|
+
try {
|
|
29
|
+
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
30
|
+
return items.filter(i => i.isDirectory()).map(dir => {
|
|
31
|
+
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
34
|
+
return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), ...config };
|
|
35
|
+
}
|
|
36
|
+
return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), name: dir.name };
|
|
37
|
+
});
|
|
38
|
+
} catch (e) { return []; }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Create strategy folder structure */
|
|
42
|
+
const createStrategyFolder = (name) => {
|
|
43
|
+
ensureStrategiesDir();
|
|
44
|
+
const folderName = name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
|
|
45
|
+
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(strategyPath)) {
|
|
48
|
+
return { success: false, error: 'Strategy folder already exists', path: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.mkdirSync(strategyPath, { recursive: true });
|
|
52
|
+
return { success: true, path: strategyPath, folder: folderName };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Save strategy module */
|
|
56
|
+
const saveModule = (strategyPath, moduleName, content) => {
|
|
57
|
+
const filePath = path.join(strategyPath, moduleName);
|
|
58
|
+
fs.writeFileSync(filePath, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
59
|
+
return filePath;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Delete strategy folder */
|
|
63
|
+
const deleteStrategy = (strategyPath) => {
|
|
64
|
+
if (fs.existsSync(strategyPath)) {
|
|
65
|
+
fs.rmSync(strategyPath, { recursive: true, force: true });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Custom Strategy Menu */
|
|
72
|
+
const customStrategyMenu = async (service) => {
|
|
73
|
+
while (true) {
|
|
74
|
+
console.clear();
|
|
75
|
+
displayBanner();
|
|
76
|
+
|
|
77
|
+
const boxWidth = getLogoWidth();
|
|
78
|
+
const W = boxWidth - 2;
|
|
79
|
+
const aiProvider = getActiveProvider();
|
|
80
|
+
const strategies = loadStrategies();
|
|
81
|
+
|
|
82
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
83
|
+
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY BUILDER', W)) + chalk.cyan('║'));
|
|
84
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
85
|
+
|
|
86
|
+
// AI Status
|
|
87
|
+
if (aiProvider) {
|
|
88
|
+
const status = `AI: ${aiProvider.name} (${aiProvider.modelName || 'default'})`;
|
|
89
|
+
console.log(chalk.cyan('║') + chalk.green(centerText('● ' + status, W)) + chalk.cyan('║'));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('○ NO AI AGENT CONNECTED', W)) + chalk.cyan('║'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
95
|
+
|
|
96
|
+
// Options
|
|
97
|
+
const col1 = '[1] CREATE NEW';
|
|
98
|
+
const col2 = `[2] MY STRATEGIES (${strategies.length})`;
|
|
99
|
+
const colWidth = Math.floor(W / 2);
|
|
100
|
+
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
101
|
+
const pad2 = Math.floor((W - colWidth - col2.length) / 2);
|
|
102
|
+
console.log(chalk.cyan('║') +
|
|
103
|
+
' '.repeat(pad1) + chalk.yellow(col1) + ' '.repeat(colWidth - col1.length - pad1) +
|
|
104
|
+
' '.repeat(pad2) + chalk.cyan(col2) + ' '.repeat(W - colWidth - col2.length - pad2) +
|
|
105
|
+
chalk.cyan('║'));
|
|
106
|
+
|
|
107
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
108
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
109
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
110
|
+
|
|
111
|
+
const input = await prompts.textInput(chalk.cyan('SELECT (1/2/B): '));
|
|
112
|
+
const choice = (input || '').toLowerCase().trim();
|
|
113
|
+
|
|
114
|
+
if (choice === 'b' || choice === '') return;
|
|
115
|
+
|
|
116
|
+
if (choice === '1') {
|
|
117
|
+
if (!aiProvider) {
|
|
118
|
+
console.log(chalk.red('\n Connect an AI Agent first (AI Agents menu)'));
|
|
119
|
+
await prompts.waitForEnter();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
await createStrategyWizard(aiProvider);
|
|
123
|
+
} else if (choice === '2') {
|
|
124
|
+
await myStrategiesMenu(strategies, service);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** AI Wizard to create modular strategy */
|
|
130
|
+
const createStrategyWizard = async (aiProvider) => {
|
|
131
|
+
console.clear();
|
|
132
|
+
displayBanner();
|
|
133
|
+
|
|
134
|
+
const boxWidth = getLogoWidth();
|
|
135
|
+
const W = boxWidth - 2;
|
|
136
|
+
|
|
137
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
138
|
+
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CREATE STRATEGY WITH AI', W)) + chalk.cyan('║'));
|
|
139
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
140
|
+
console.log();
|
|
141
|
+
|
|
142
|
+
// Step 1: Strategy name
|
|
143
|
+
console.log(chalk.yellow(' STEP 1: Name your strategy'));
|
|
144
|
+
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
145
|
+
if (!name || !name.trim()) {
|
|
146
|
+
console.log(chalk.red(' Cancelled'));
|
|
147
|
+
await prompts.waitForEnter();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create folder
|
|
152
|
+
const folder = createStrategyFolder(name.trim());
|
|
153
|
+
if (!folder.success) {
|
|
154
|
+
console.log(chalk.red(` Error: ${folder.error}`));
|
|
155
|
+
await prompts.waitForEnter();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.green(` ✓ Created: ${folder.path}`));
|
|
160
|
+
console.log();
|
|
161
|
+
|
|
162
|
+
// Step 2: Chat with AI to build strategy
|
|
163
|
+
console.log(chalk.yellow(' STEP 2: Describe your strategy to the AI'));
|
|
164
|
+
console.log(chalk.gray(' Type your strategy idea in plain English.'));
|
|
165
|
+
console.log(chalk.gray(' The AI will help you build each module.'));
|
|
166
|
+
console.log(chalk.gray(' Type "done" when finished, "cancel" to abort.'));
|
|
167
|
+
console.log();
|
|
168
|
+
|
|
169
|
+
const systemPrompt = `You are an expert algo trading strategy builder for futures (ES, NQ, MES, MNQ, etc).
|
|
170
|
+
Help the user create a modular trading strategy. Build these components:
|
|
171
|
+
|
|
172
|
+
1. ENTRY CONDITIONS - When to open a position (long/short signals)
|
|
173
|
+
2. EXIT CONDITIONS - Take profit, stop loss, trailing stops
|
|
174
|
+
3. RISK MANAGEMENT - Position sizing, max loss, max positions
|
|
175
|
+
4. FILTERS - Market conditions when NOT to trade
|
|
176
|
+
|
|
177
|
+
Ask clarifying questions. Be concise. When ready, output each module.
|
|
178
|
+
|
|
179
|
+
For each module, output JavaScript code in this format:
|
|
180
|
+
\`\`\`javascript:entry.js
|
|
181
|
+
module.exports = {
|
|
182
|
+
checkLongEntry: (data) => { /* return true/false */ },
|
|
183
|
+
checkShortEntry: (data) => { /* return true/false */ }
|
|
184
|
+
};
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, macd, vwap, high, low, open, close }`;
|
|
188
|
+
|
|
189
|
+
const messages = [{ role: 'system', content: systemPrompt }];
|
|
190
|
+
const modules = {};
|
|
191
|
+
|
|
192
|
+
console.log(chalk.green(' AI: ') + 'What kind of trading strategy do you want to create?');
|
|
193
|
+
console.log(chalk.gray(' Example: "A mean reversion strategy that buys when RSI < 30"'));
|
|
194
|
+
console.log();
|
|
195
|
+
|
|
196
|
+
while (true) {
|
|
197
|
+
const userInput = await prompts.textInput(chalk.yellow(' You: '));
|
|
198
|
+
|
|
199
|
+
if (!userInput) continue;
|
|
200
|
+
|
|
201
|
+
if (userInput.toLowerCase() === 'cancel') {
|
|
202
|
+
deleteStrategy(folder.path);
|
|
203
|
+
console.log(chalk.gray('\n Strategy cancelled and folder deleted.'));
|
|
204
|
+
await prompts.waitForEnter();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (userInput.toLowerCase() === 'done') {
|
|
209
|
+
// Save config
|
|
210
|
+
saveModule(folder.path, 'config.json', {
|
|
211
|
+
name: name.trim(),
|
|
212
|
+
description: modules.description || '',
|
|
213
|
+
createdAt: new Date().toISOString(),
|
|
214
|
+
modules: Object.keys(modules).filter(k => k !== 'description')
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log(chalk.green('\n ✓ Strategy saved!'));
|
|
218
|
+
console.log(chalk.cyan(` Location: ${folder.path}`));
|
|
219
|
+
console.log(chalk.gray(' Modules created:'));
|
|
220
|
+
for (const m of Object.keys(modules)) {
|
|
221
|
+
if (m !== 'description') console.log(chalk.gray(` - ${m}`));
|
|
222
|
+
}
|
|
223
|
+
await prompts.waitForEnter();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
messages.push({ role: 'user', content: userInput });
|
|
228
|
+
|
|
229
|
+
const spinner = ora({ text: 'AI thinking...', color: 'yellow' }).start();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
233
|
+
const result = await cliproxy.chatCompletion(modelId, messages);
|
|
234
|
+
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
spinner.fail(`AI Error: ${result.error}`);
|
|
237
|
+
messages.pop();
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const response = result.response?.choices?.[0]?.message?.content || '';
|
|
242
|
+
messages.push({ role: 'assistant', content: response });
|
|
243
|
+
|
|
244
|
+
spinner.stop();
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
// Extract and save code modules
|
|
248
|
+
const codeBlocks = response.matchAll(/```javascript:(\w+\.js)\n([\s\S]*?)```/g);
|
|
249
|
+
for (const match of codeBlocks) {
|
|
250
|
+
const [, filename, code] = match;
|
|
251
|
+
saveModule(folder.path, filename, code.trim());
|
|
252
|
+
modules[filename] = true;
|
|
253
|
+
console.log(chalk.green(` ✓ Saved module: ${filename}`));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Extract description if present
|
|
257
|
+
const descMatch = response.match(/description[:\s]*["']?([^"'\n]+)/i);
|
|
258
|
+
if (descMatch) modules.description = descMatch[1];
|
|
259
|
+
|
|
260
|
+
// Print AI response (without code blocks for cleaner output)
|
|
261
|
+
const cleanResponse = response.replace(/```[\s\S]*?```/g, '[code saved]');
|
|
262
|
+
console.log(chalk.green(' AI: ') + formatResponse(cleanResponse));
|
|
263
|
+
console.log();
|
|
264
|
+
|
|
265
|
+
} catch (e) {
|
|
266
|
+
spinner.fail(`Error: ${e.message}`);
|
|
267
|
+
messages.pop();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** Get default model */
|
|
273
|
+
const getDefaultModel = (providerId) => {
|
|
274
|
+
const defaults = {
|
|
275
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
276
|
+
google: 'gemini-2.5-pro',
|
|
277
|
+
openai: 'gpt-4o'
|
|
278
|
+
};
|
|
279
|
+
return defaults[providerId] || 'claude-sonnet-4-20250514';
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/** Format response for terminal */
|
|
283
|
+
const formatResponse = (text) => {
|
|
284
|
+
const lines = text.split('\n');
|
|
285
|
+
return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** My Strategies Menu */
|
|
289
|
+
const myStrategiesMenu = async (strategies, service) => {
|
|
290
|
+
while (true) {
|
|
291
|
+
console.clear();
|
|
292
|
+
displayBanner();
|
|
293
|
+
|
|
294
|
+
const boxWidth = getLogoWidth();
|
|
295
|
+
const W = boxWidth - 2;
|
|
296
|
+
|
|
297
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
298
|
+
console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('MY STRATEGIES', W)) + chalk.cyan('║'));
|
|
299
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
300
|
+
|
|
301
|
+
if (strategies.length === 0) {
|
|
302
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('No strategies yet', W)) + chalk.cyan('║'));
|
|
303
|
+
} else {
|
|
304
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
305
|
+
const s = strategies[i];
|
|
306
|
+
const num = `[${i + 1}]`.padEnd(4);
|
|
307
|
+
const sname = (s.name || s.folder).substring(0, 30).padEnd(32);
|
|
308
|
+
const modules = s.modules ? `${s.modules.length} modules` : '';
|
|
309
|
+
const line = `${num} ${sname} ${chalk.gray(modules)}`;
|
|
310
|
+
console.log(chalk.cyan('║') + ' ' + chalk.white(num) + chalk.cyan(sname) + chalk.gray(modules.padEnd(W - 38)) + chalk.cyan('║'));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
315
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
316
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
317
|
+
|
|
318
|
+
if (strategies.length === 0) {
|
|
319
|
+
await prompts.waitForEnter();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const input = await prompts.textInput(chalk.cyan(`SELECT (1-${strategies.length}/B): `));
|
|
324
|
+
const choice = (input || '').toLowerCase().trim();
|
|
325
|
+
|
|
326
|
+
if (choice === 'b' || choice === '') return;
|
|
327
|
+
|
|
328
|
+
const num = parseInt(choice);
|
|
329
|
+
if (!isNaN(num) && num >= 1 && num <= strategies.length) {
|
|
330
|
+
await strategyDetailMenu(strategies[num - 1], service);
|
|
331
|
+
strategies.length = 0;
|
|
332
|
+
strategies.push(...loadStrategies());
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/** Strategy Detail Menu */
|
|
338
|
+
const strategyDetailMenu = async (strategy, service) => {
|
|
339
|
+
console.clear();
|
|
340
|
+
displayBanner();
|
|
341
|
+
|
|
342
|
+
const boxWidth = getLogoWidth();
|
|
343
|
+
const W = boxWidth - 2;
|
|
344
|
+
|
|
345
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
346
|
+
console.log(chalk.cyan('║') + chalk.green.bold(centerText((strategy.name || strategy.folder).toUpperCase(), W)) + chalk.cyan('║'));
|
|
347
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
348
|
+
|
|
349
|
+
// Show modules
|
|
350
|
+
const files = fs.readdirSync(strategy.path);
|
|
351
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText(`Path: ${strategy.path}`, W)) + chalk.cyan('║'));
|
|
352
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
353
|
+
console.log(chalk.cyan('║') + chalk.white(centerText('MODULES:', W)) + chalk.cyan('║'));
|
|
354
|
+
|
|
355
|
+
for (const f of files) {
|
|
356
|
+
const icon = f.endsWith('.js') ? '📄' : f.endsWith('.json') ? '⚙️' : '📁';
|
|
357
|
+
console.log(chalk.cyan('║') + centerText(`${icon} ${f}`, W) + chalk.cyan('║'));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
361
|
+
|
|
362
|
+
// Options: Run, Edit with AI, Delete
|
|
363
|
+
const opts = ['[1] RUN', '[2] EDIT WITH AI', '[3] DELETE'];
|
|
364
|
+
const optLine = opts.join(' ');
|
|
365
|
+
console.log(chalk.cyan('║') + centerText(
|
|
366
|
+
chalk.green(opts[0]) + ' ' + chalk.yellow(opts[1]) + ' ' + chalk.red(opts[2]), W
|
|
367
|
+
) + chalk.cyan('║'));
|
|
368
|
+
|
|
369
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
370
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
371
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
372
|
+
|
|
373
|
+
const input = await prompts.textInput(chalk.cyan('SELECT (1/2/3/B): '));
|
|
374
|
+
const choice = (input || '').toLowerCase().trim();
|
|
375
|
+
|
|
376
|
+
if (choice === '1') {
|
|
377
|
+
console.log(chalk.yellow('\n Running custom strategy...'));
|
|
378
|
+
console.log(chalk.gray(' This will use your connected accounts and market data.'));
|
|
379
|
+
console.log(chalk.gray(' (Full execution coming soon)'));
|
|
380
|
+
await prompts.waitForEnter();
|
|
381
|
+
} else if (choice === '2') {
|
|
382
|
+
console.log(chalk.yellow('\n Edit with AI coming soon...'));
|
|
383
|
+
await prompts.waitForEnter();
|
|
384
|
+
} else if (choice === '3') {
|
|
385
|
+
const confirm = await prompts.confirmPrompt(`Delete "${strategy.name || strategy.folder}"?`, false);
|
|
386
|
+
if (confirm) {
|
|
387
|
+
deleteStrategy(strategy.path);
|
|
388
|
+
console.log(chalk.green('\n ✓ Strategy deleted'));
|
|
389
|
+
await prompts.waitForEnter();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
module.exports = { customStrategyMenu, loadStrategies, createStrategyFolder, saveModule };
|
package/src/pages/algo/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const log = logger.scope('AlgoMenu');
|
|
|
10
10
|
|
|
11
11
|
const { oneAccountMenu } = require('./one-account');
|
|
12
12
|
const { copyTradingMenu } = require('./copy-trading');
|
|
13
|
+
const { customStrategyMenu } = require('./custom-strategy');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Algo Trading Menu
|
|
@@ -87,27 +88,4 @@ const algoTradingMenu = async (service) => {
|
|
|
87
88
|
}
|
|
88
89
|
};
|
|
89
90
|
|
|
90
|
-
/**
|
|
91
|
-
* Custom Strategy Menu - AI-powered strategy creation
|
|
92
|
-
*/
|
|
93
|
-
const customStrategyMenu = async (service) => {
|
|
94
|
-
console.clear();
|
|
95
|
-
displayBanner();
|
|
96
|
-
|
|
97
|
-
const boxWidth = getLogoWidth();
|
|
98
|
-
const W = boxWidth - 2;
|
|
99
|
-
|
|
100
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
101
|
-
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY', W)) + chalk.cyan('║'));
|
|
102
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
103
|
-
console.log(chalk.cyan('║') + centerText('Create your own trading strategy with AI assistance', W) + chalk.cyan('║'));
|
|
104
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
105
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText('Coming soon...', W)) + chalk.cyan('║'));
|
|
106
|
-
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
107
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
108
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
109
|
-
|
|
110
|
-
await prompts.waitForEnter();
|
|
111
|
-
};
|
|
112
|
-
|
|
113
91
|
module.exports = { algoTradingMenu };
|
package/src/ui/index.js
CHANGED
|
@@ -70,7 +70,7 @@ const displayBanner = () => {
|
|
|
70
70
|
|
|
71
71
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
72
72
|
const tagline = isMobile ? `HQX V${version}` : `PROP FUTURES ALGO TRADING V${version}`;
|
|
73
|
-
console.log(chalk.cyan('║') + chalk.
|
|
73
|
+
console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
|
|
74
74
|
|
|
75
75
|
// ALWAYS close the banner
|
|
76
76
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|