hedgequantx 1.7.6 → 1.7.8
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 -2
- package/src/app.js +54 -159
- package/src/menus/connect.js +54 -166
- package/src/menus/dashboard.js +49 -120
- package/src/pages/accounts.js +11 -22
- package/src/pages/algo/copy-trading.js +63 -210
- package/src/pages/algo/index.js +10 -20
- package/src/pages/algo/one-account.js +66 -172
- package/src/pages/orders.js +11 -32
- package/src/pages/positions.js +11 -32
- package/src/pages/stats.js +5 -14
- package/src/pages/user.js +8 -22
- package/src/utils/index.js +2 -1
- package/src/utils/prompts.js +195 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copy Trading Mode
|
|
3
|
-
* Lightweight - UI + HQX Server handles all execution
|
|
2
|
+
* Copy Trading Mode
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const chalk = require('chalk');
|
|
7
6
|
const ora = require('ora');
|
|
8
|
-
const inquirer = require('inquirer');
|
|
9
7
|
const readline = require('readline');
|
|
10
8
|
|
|
11
9
|
const { connections } = require('../../services');
|
|
12
10
|
const { HQXServerService } = require('../../services/hqx-server');
|
|
13
|
-
const { FUTURES_SYMBOLS } = require('../../config');
|
|
14
11
|
const { AlgoUI } = require('./ui');
|
|
15
|
-
const { logger } = require('../../utils');
|
|
12
|
+
const { logger, prompts } = require('../../utils');
|
|
16
13
|
|
|
17
14
|
const log = logger.scope('CopyTrading');
|
|
18
15
|
|
|
@@ -22,14 +19,13 @@ const log = logger.scope('CopyTrading');
|
|
|
22
19
|
const copyTradingMenu = async () => {
|
|
23
20
|
log.info('Copy Trading menu opened');
|
|
24
21
|
const allConns = connections.getAll();
|
|
25
|
-
log.debug('Connections found', { count: allConns.length });
|
|
26
22
|
|
|
27
23
|
if (allConns.length < 2) {
|
|
28
24
|
console.log();
|
|
29
25
|
console.log(chalk.yellow(' Copy Trading requires 2 connected accounts'));
|
|
30
26
|
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
31
27
|
console.log();
|
|
32
|
-
await
|
|
28
|
+
await prompts.waitForEnter();
|
|
33
29
|
return;
|
|
34
30
|
}
|
|
35
31
|
|
|
@@ -37,7 +33,6 @@ const copyTradingMenu = async () => {
|
|
|
37
33
|
console.log(chalk.magenta.bold(' Copy Trading Setup'));
|
|
38
34
|
console.log();
|
|
39
35
|
|
|
40
|
-
// Get all active accounts from all connections
|
|
41
36
|
const spinner = ora({ text: 'Fetching accounts...', color: 'yellow' }).start();
|
|
42
37
|
|
|
43
38
|
const allAccounts = [];
|
|
@@ -47,86 +42,54 @@ const copyTradingMenu = async () => {
|
|
|
47
42
|
if (result.success && result.accounts) {
|
|
48
43
|
const active = result.accounts.filter(a => a.status === 0);
|
|
49
44
|
for (const acc of active) {
|
|
50
|
-
allAccounts.push({
|
|
51
|
-
account: acc,
|
|
52
|
-
service: conn.service,
|
|
53
|
-
propfirm: conn.propfirm,
|
|
54
|
-
type: conn.type
|
|
55
|
-
});
|
|
45
|
+
allAccounts.push({ account: acc, service: conn.service, propfirm: conn.propfirm, type: conn.type });
|
|
56
46
|
}
|
|
57
47
|
}
|
|
58
|
-
} catch (e) {
|
|
48
|
+
} catch (e) {}
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
if (allAccounts.length < 2) {
|
|
62
52
|
spinner.fail('Need at least 2 active accounts');
|
|
63
|
-
await
|
|
53
|
+
await prompts.waitForEnter();
|
|
64
54
|
return;
|
|
65
55
|
}
|
|
66
56
|
|
|
67
57
|
spinner.succeed(`Found ${allAccounts.length} active accounts`);
|
|
68
|
-
log.debug('Active accounts loaded', { count: allAccounts.length, accounts: allAccounts.map(a => ({ propfirm: a.propfirm, name: a.account.accountName })) });
|
|
69
58
|
|
|
70
59
|
// Step 1: Select Lead Account
|
|
71
|
-
console.log(chalk.cyan(' Step 1: Select LEAD Account
|
|
72
|
-
const
|
|
73
|
-
|
|
60
|
+
console.log(chalk.cyan(' Step 1: Select LEAD Account'));
|
|
61
|
+
const leadOptions = allAccounts.map((a, i) => ({
|
|
62
|
+
label: `${a.propfirm} - ${a.account.accountName || a.account.accountId} ($${a.account.balance.toLocaleString()})`,
|
|
74
63
|
value: i
|
|
75
64
|
}));
|
|
76
|
-
|
|
65
|
+
leadOptions.push({ label: '< Cancel', value: -1 });
|
|
77
66
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
name: 'leadIdx',
|
|
81
|
-
message: 'Lead Account:',
|
|
82
|
-
choices: leadChoices
|
|
83
|
-
}]);
|
|
84
|
-
|
|
85
|
-
if (leadIdx === -1) {
|
|
86
|
-
log.debug('User cancelled at lead selection');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
67
|
+
const leadIdx = await prompts.selectOption('Lead Account:', leadOptions);
|
|
68
|
+
if (leadIdx === null || leadIdx === -1) return;
|
|
89
69
|
const lead = allAccounts[leadIdx];
|
|
90
|
-
log.debug('Lead account selected', { propfirm: lead.propfirm, account: lead.account.accountName });
|
|
91
70
|
|
|
92
71
|
// Step 2: Select Follower Account
|
|
93
72
|
console.log();
|
|
94
|
-
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account
|
|
95
|
-
const
|
|
73
|
+
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
|
|
74
|
+
const followerOptions = allAccounts
|
|
96
75
|
.map((a, i) => ({ a, i }))
|
|
97
76
|
.filter(x => x.i !== leadIdx)
|
|
98
77
|
.map(x => ({
|
|
99
|
-
|
|
78
|
+
label: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId} ($${x.a.account.balance.toLocaleString()})`,
|
|
100
79
|
value: x.i
|
|
101
80
|
}));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const { followerIdx } = await inquirer.prompt([{
|
|
105
|
-
type: 'list',
|
|
106
|
-
name: 'followerIdx',
|
|
107
|
-
message: 'Follower Account:',
|
|
108
|
-
choices: followerChoices
|
|
109
|
-
}]);
|
|
81
|
+
followerOptions.push({ label: '< Cancel', value: -1 });
|
|
110
82
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
83
|
+
const followerIdx = await prompts.selectOption('Follower Account:', followerOptions);
|
|
84
|
+
if (followerIdx === null || followerIdx === -1) return;
|
|
115
85
|
const follower = allAccounts[followerIdx];
|
|
116
|
-
log.debug('Follower account selected', { propfirm: follower.propfirm, account: follower.account.accountName });
|
|
117
86
|
|
|
118
|
-
// Step 3: Select
|
|
87
|
+
// Step 3-4: Select Symbols
|
|
119
88
|
console.log();
|
|
120
89
|
console.log(chalk.cyan(' Step 3: Select Symbol for LEAD'));
|
|
121
|
-
log.debug('Selecting symbol for lead', { serviceType: lead.type });
|
|
122
90
|
const leadSymbol = await selectSymbol(lead.service, 'Lead');
|
|
123
|
-
if (!leadSymbol)
|
|
124
|
-
log.debug('Lead symbol selection failed or cancelled');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
log.debug('Lead symbol selected', { symbol: leadSymbol.name || leadSymbol.symbol });
|
|
91
|
+
if (!leadSymbol) return;
|
|
128
92
|
|
|
129
|
-
// Step 4: Select Symbol for Follower
|
|
130
93
|
console.log();
|
|
131
94
|
console.log(chalk.cyan(' Step 4: Select Symbol for FOLLOWER'));
|
|
132
95
|
const followerSymbol = await selectSymbol(follower.service, 'Follower');
|
|
@@ -136,53 +99,24 @@ const copyTradingMenu = async () => {
|
|
|
136
99
|
console.log();
|
|
137
100
|
console.log(chalk.cyan(' Step 5: Configure Parameters'));
|
|
138
101
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
name: 'followerContractsInput',
|
|
151
|
-
message: 'Follower contracts:',
|
|
152
|
-
default: String(leadContracts),
|
|
153
|
-
validate: v => !isNaN(parseInt(v)) && parseInt(v) > 0 ? true : 'Enter a positive number'
|
|
154
|
-
}]);
|
|
155
|
-
const followerContracts = parseInt(followerContractsInput) || leadContracts;
|
|
156
|
-
|
|
157
|
-
const { dailyTargetInput } = await inquirer.prompt([{
|
|
158
|
-
type: 'input',
|
|
159
|
-
name: 'dailyTargetInput',
|
|
160
|
-
message: 'Daily target ($):',
|
|
161
|
-
default: '400',
|
|
162
|
-
validate: v => !isNaN(parseInt(v)) && parseInt(v) > 0 ? true : 'Enter a positive number'
|
|
163
|
-
}]);
|
|
164
|
-
const dailyTarget = parseInt(dailyTargetInput) || 400;
|
|
165
|
-
|
|
166
|
-
const { maxRiskInput } = await inquirer.prompt([{
|
|
167
|
-
type: 'input',
|
|
168
|
-
name: 'maxRiskInput',
|
|
169
|
-
message: 'Max risk ($):',
|
|
170
|
-
default: '200',
|
|
171
|
-
validate: v => !isNaN(parseInt(v)) && parseInt(v) > 0 ? true : 'Enter a positive number'
|
|
172
|
-
}]);
|
|
173
|
-
const maxRisk = parseInt(maxRiskInput) || 200;
|
|
102
|
+
const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
|
|
103
|
+
if (leadContracts === null) return;
|
|
104
|
+
|
|
105
|
+
const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
|
|
106
|
+
if (followerContracts === null) return;
|
|
107
|
+
|
|
108
|
+
const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
|
|
109
|
+
if (dailyTarget === null) return;
|
|
110
|
+
|
|
111
|
+
const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
|
|
112
|
+
if (maxRisk === null) return;
|
|
174
113
|
|
|
175
114
|
// Step 6: Privacy
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{ name: 'Hide account names', value: false },
|
|
182
|
-
{ name: 'Show account names', value: true }
|
|
183
|
-
]
|
|
184
|
-
}]);
|
|
185
|
-
const showNames = privacyChoice;
|
|
115
|
+
const showNames = await prompts.selectOption('Account names:', [
|
|
116
|
+
{ label: 'Hide account names', value: false },
|
|
117
|
+
{ label: 'Show account names', value: true }
|
|
118
|
+
]);
|
|
119
|
+
if (showNames === null) return;
|
|
186
120
|
|
|
187
121
|
// Confirm
|
|
188
122
|
console.log();
|
|
@@ -192,22 +126,14 @@ const copyTradingMenu = async () => {
|
|
|
192
126
|
console.log(chalk.gray(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
193
127
|
console.log();
|
|
194
128
|
|
|
195
|
-
const
|
|
196
|
-
type: 'confirm',
|
|
197
|
-
name: 'confirm',
|
|
198
|
-
message: chalk.yellow('Start Copy Trading?'),
|
|
199
|
-
default: true
|
|
200
|
-
}]);
|
|
201
|
-
|
|
129
|
+
const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
|
|
202
130
|
if (!confirm) return;
|
|
203
131
|
|
|
204
132
|
// Launch
|
|
205
133
|
await launchCopyTrading({
|
|
206
134
|
lead: { ...lead, symbol: leadSymbol, contracts: leadContracts },
|
|
207
135
|
follower: { ...follower, symbol: followerSymbol, contracts: followerContracts },
|
|
208
|
-
dailyTarget,
|
|
209
|
-
maxRisk,
|
|
210
|
-
showNames
|
|
136
|
+
dailyTarget, maxRisk, showNames
|
|
211
137
|
});
|
|
212
138
|
};
|
|
213
139
|
|
|
@@ -215,59 +141,31 @@ const copyTradingMenu = async () => {
|
|
|
215
141
|
* Symbol selection helper
|
|
216
142
|
*/
|
|
217
143
|
const selectSymbol = async (service, label) => {
|
|
218
|
-
log.debug('selectSymbol called', { label, hasGetContracts: typeof service.getContracts === 'function' });
|
|
219
144
|
try {
|
|
220
145
|
let contracts = [];
|
|
221
146
|
|
|
222
|
-
// Try getContracts first
|
|
223
147
|
if (typeof service.getContracts === 'function') {
|
|
224
148
|
const result = await service.getContracts();
|
|
225
|
-
log.debug('getContracts result', { success: result?.success, count: result?.contracts?.length });
|
|
226
149
|
if (result.success && result.contracts?.length > 0) {
|
|
227
150
|
contracts = result.contracts;
|
|
228
151
|
}
|
|
229
152
|
}
|
|
230
153
|
|
|
231
|
-
// Fallback to searchContracts if no contracts yet
|
|
232
154
|
if (contracts.length === 0 && typeof service.searchContracts === 'function') {
|
|
233
|
-
log.debug('Trying searchContracts fallback');
|
|
234
|
-
// For Rithmic, searchContracts returns array directly
|
|
235
155
|
const searchResult = await service.searchContracts('ES');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (Array.isArray(searchResult)) {
|
|
239
|
-
contracts = searchResult;
|
|
240
|
-
} else if (searchResult?.contracts) {
|
|
241
|
-
contracts = searchResult.contracts;
|
|
242
|
-
}
|
|
156
|
+
if (Array.isArray(searchResult)) contracts = searchResult;
|
|
157
|
+
else if (searchResult?.contracts) contracts = searchResult.contracts;
|
|
243
158
|
}
|
|
244
159
|
|
|
245
|
-
// If still no contracts, show error
|
|
246
160
|
if (!contracts || contracts.length === 0) {
|
|
247
|
-
log.
|
|
248
|
-
console.log(chalk.red(' No contracts available for this service'));
|
|
161
|
+
console.log(chalk.red(' No contracts available'));
|
|
249
162
|
return null;
|
|
250
163
|
}
|
|
251
164
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Build choices - simple list without categories
|
|
255
|
-
const choices = contracts.map(c => ({
|
|
256
|
-
name: c.name || c.symbol,
|
|
257
|
-
value: c
|
|
258
|
-
}));
|
|
259
|
-
choices.push(new inquirer.Separator());
|
|
260
|
-
choices.push({ name: chalk.yellow('< Cancel'), value: null });
|
|
261
|
-
|
|
262
|
-
const { symbol } = await inquirer.prompt([{
|
|
263
|
-
type: 'list',
|
|
264
|
-
name: 'symbol',
|
|
265
|
-
message: `${label} Symbol:`,
|
|
266
|
-
choices,
|
|
267
|
-
pageSize: 15
|
|
268
|
-
}]);
|
|
165
|
+
const options = contracts.map(c => ({ label: c.name || c.symbol, value: c }));
|
|
166
|
+
options.push({ label: '< Cancel', value: null });
|
|
269
167
|
|
|
270
|
-
return
|
|
168
|
+
return await prompts.selectOption(`${label} Symbol:`, options);
|
|
271
169
|
} catch (e) {
|
|
272
170
|
return null;
|
|
273
171
|
}
|
|
@@ -282,33 +180,23 @@ const launchCopyTrading = async (config) => {
|
|
|
282
180
|
const leadName = showNames ? (lead.account.accountName || lead.account.accountId) : 'HQX Lead *****';
|
|
283
181
|
const followerName = showNames ? (follower.account.accountName || follower.account.accountId) : 'HQX Follower *****';
|
|
284
182
|
|
|
285
|
-
// UI with copy trading subtitle
|
|
286
183
|
const ui = new AlgoUI({ subtitle: 'HQX Copy Trading' });
|
|
287
184
|
|
|
288
|
-
// Combined stats
|
|
289
185
|
const stats = {
|
|
290
|
-
leadName,
|
|
291
|
-
followerName,
|
|
186
|
+
leadName, followerName,
|
|
292
187
|
leadSymbol: lead.symbol.name,
|
|
293
188
|
followerSymbol: follower.symbol.name,
|
|
294
189
|
leadQty: lead.contracts,
|
|
295
190
|
followerQty: follower.contracts,
|
|
296
|
-
target: dailyTarget,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
trades: 0,
|
|
300
|
-
wins: 0,
|
|
301
|
-
losses: 0,
|
|
302
|
-
latency: 0,
|
|
303
|
-
connected: false
|
|
191
|
+
target: dailyTarget, risk: maxRisk,
|
|
192
|
+
pnl: 0, trades: 0, wins: 0, losses: 0,
|
|
193
|
+
latency: 0, connected: false
|
|
304
194
|
};
|
|
305
195
|
|
|
306
196
|
let running = true;
|
|
307
197
|
let stopReason = null;
|
|
308
198
|
|
|
309
|
-
// Connect to HQX Server
|
|
310
199
|
const hqx = new HQXServerService();
|
|
311
|
-
|
|
312
200
|
const spinner = ora({ text: 'Connecting to HQX Server...', color: 'yellow' }).start();
|
|
313
201
|
|
|
314
202
|
try {
|
|
@@ -326,7 +214,6 @@ const launchCopyTrading = async (config) => {
|
|
|
326
214
|
|
|
327
215
|
// Event handlers
|
|
328
216
|
hqx.on('latency', (d) => { stats.latency = d.latency || 0; });
|
|
329
|
-
|
|
330
217
|
hqx.on('log', (d) => {
|
|
331
218
|
let msg = d.message;
|
|
332
219
|
if (!showNames) {
|
|
@@ -335,7 +222,6 @@ const launchCopyTrading = async (config) => {
|
|
|
335
222
|
}
|
|
336
223
|
ui.addLog(d.type || 'info', msg);
|
|
337
224
|
});
|
|
338
|
-
|
|
339
225
|
hqx.on('trade', (d) => {
|
|
340
226
|
stats.trades++;
|
|
341
227
|
stats.pnl += d.pnl || 0;
|
|
@@ -343,68 +229,47 @@ const launchCopyTrading = async (config) => {
|
|
|
343
229
|
ui.addLog(d.pnl >= 0 ? 'trade' : 'loss', `${d.pnl >= 0 ? '+' : ''}$${d.pnl.toFixed(2)}`);
|
|
344
230
|
|
|
345
231
|
if (stats.pnl >= dailyTarget) {
|
|
346
|
-
stopReason = 'target';
|
|
347
|
-
running = false;
|
|
232
|
+
stopReason = 'target'; running = false;
|
|
348
233
|
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
349
234
|
hqx.stopAlgo();
|
|
350
235
|
} else if (stats.pnl <= -maxRisk) {
|
|
351
|
-
stopReason = 'risk';
|
|
352
|
-
running = false;
|
|
236
|
+
stopReason = 'risk'; running = false;
|
|
353
237
|
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
354
238
|
hqx.stopAlgo();
|
|
355
239
|
}
|
|
356
240
|
});
|
|
357
|
-
|
|
358
|
-
hqx.on('copy', (d) => {
|
|
359
|
-
ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x to Follower`);
|
|
360
|
-
});
|
|
361
|
-
|
|
241
|
+
hqx.on('copy', (d) => { ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x`); });
|
|
362
242
|
hqx.on('error', (d) => { ui.addLog('error', d.message); });
|
|
363
243
|
hqx.on('disconnected', () => { stats.connected = false; });
|
|
364
244
|
|
|
365
|
-
// Start
|
|
245
|
+
// Start on server
|
|
366
246
|
if (stats.connected) {
|
|
367
247
|
ui.addLog('info', 'Starting Copy Trading...');
|
|
368
248
|
|
|
369
|
-
// Get credentials
|
|
370
249
|
let leadCreds = null, followerCreds = null;
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
leadCreds = lead.service.getRithmicCredentials();
|
|
374
|
-
}
|
|
375
|
-
if (follower.service.getRithmicCredentials) {
|
|
376
|
-
followerCreds = follower.service.getRithmicCredentials();
|
|
377
|
-
}
|
|
250
|
+
if (lead.service.getRithmicCredentials) leadCreds = lead.service.getRithmicCredentials();
|
|
251
|
+
if (follower.service.getRithmicCredentials) followerCreds = follower.service.getRithmicCredentials();
|
|
378
252
|
|
|
379
253
|
hqx.startCopyTrading({
|
|
380
|
-
// Lead config
|
|
381
254
|
leadAccountId: lead.account.accountId,
|
|
382
255
|
leadContractId: lead.symbol.id || lead.symbol.contractId,
|
|
383
256
|
leadSymbol: lead.symbol.symbol || lead.symbol.name,
|
|
384
257
|
leadContracts: lead.contracts,
|
|
385
258
|
leadPropfirm: lead.propfirm,
|
|
386
|
-
leadToken: lead.service.getToken
|
|
259
|
+
leadToken: lead.service.getToken?.() || null,
|
|
387
260
|
leadRithmicCredentials: leadCreds,
|
|
388
|
-
|
|
389
|
-
// Follower config
|
|
390
261
|
followerAccountId: follower.account.accountId,
|
|
391
262
|
followerContractId: follower.symbol.id || follower.symbol.contractId,
|
|
392
263
|
followerSymbol: follower.symbol.symbol || follower.symbol.name,
|
|
393
264
|
followerContracts: follower.contracts,
|
|
394
265
|
followerPropfirm: follower.propfirm,
|
|
395
|
-
followerToken: follower.service.getToken
|
|
266
|
+
followerToken: follower.service.getToken?.() || null,
|
|
396
267
|
followerRithmicCredentials: followerCreds,
|
|
397
|
-
|
|
398
|
-
// Targets
|
|
399
|
-
dailyTarget,
|
|
400
|
-
maxRisk
|
|
268
|
+
dailyTarget, maxRisk
|
|
401
269
|
});
|
|
402
270
|
}
|
|
403
271
|
|
|
404
|
-
|
|
405
|
-
const refreshInterval = setInterval(() => {
|
|
406
|
-
if (running) ui.render(stats);
|
|
407
|
-
}, 250);
|
|
272
|
+
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
408
273
|
|
|
409
274
|
// Keyboard
|
|
410
275
|
const setupKeys = () => {
|
|
@@ -415,12 +280,10 @@ const launchCopyTrading = async (config) => {
|
|
|
415
280
|
|
|
416
281
|
const handler = (str, key) => {
|
|
417
282
|
if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
|
|
418
|
-
running = false;
|
|
419
|
-
stopReason = 'manual';
|
|
283
|
+
running = false; stopReason = 'manual';
|
|
420
284
|
}
|
|
421
285
|
};
|
|
422
286
|
process.stdin.on('keypress', handler);
|
|
423
|
-
|
|
424
287
|
return () => {
|
|
425
288
|
process.stdin.removeListener('keypress', handler);
|
|
426
289
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
@@ -429,22 +292,13 @@ const launchCopyTrading = async (config) => {
|
|
|
429
292
|
|
|
430
293
|
const cleanupKeys = setupKeys();
|
|
431
294
|
|
|
432
|
-
// Wait
|
|
433
295
|
await new Promise(resolve => {
|
|
434
|
-
const check = setInterval(() => {
|
|
435
|
-
if (!running) { clearInterval(check); resolve(); }
|
|
436
|
-
}, 100);
|
|
296
|
+
const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
|
|
437
297
|
});
|
|
438
298
|
|
|
439
|
-
// Cleanup
|
|
440
299
|
clearInterval(refreshInterval);
|
|
441
300
|
if (cleanupKeys) cleanupKeys();
|
|
442
|
-
|
|
443
|
-
if (stats.connected) {
|
|
444
|
-
hqx.stopAlgo();
|
|
445
|
-
hqx.disconnect();
|
|
446
|
-
}
|
|
447
|
-
|
|
301
|
+
if (stats.connected) { hqx.stopAlgo(); hqx.disconnect(); }
|
|
448
302
|
ui.cleanup();
|
|
449
303
|
|
|
450
304
|
// Summary
|
|
@@ -454,11 +308,10 @@ const launchCopyTrading = async (config) => {
|
|
|
454
308
|
console.log();
|
|
455
309
|
console.log(chalk.white(` Stop: ${stopReason || 'unknown'}`));
|
|
456
310
|
console.log(chalk.white(` Trades: ${stats.trades} (W: ${stats.wins} / L: ${stats.losses})`));
|
|
457
|
-
|
|
458
|
-
console.log(c(` P&L: ${stats.pnl >= 0 ? '+' : ''}$${stats.pnl.toFixed(2)}`));
|
|
311
|
+
console.log((stats.pnl >= 0 ? chalk.green : chalk.red)(` P&L: ${stats.pnl >= 0 ? '+' : ''}$${stats.pnl.toFixed(2)}`));
|
|
459
312
|
console.log();
|
|
460
313
|
|
|
461
|
-
await
|
|
314
|
+
await prompts.waitForEnter();
|
|
462
315
|
};
|
|
463
316
|
|
|
464
317
|
module.exports = { copyTradingMenu };
|
package/src/pages/algo/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Algo Trading - Main Menu
|
|
3
|
-
* Lightweight entry point
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const chalk = require('chalk');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
6
|
const { getSeparator } = require('../../ui');
|
|
9
|
-
const { logger } = require('../../utils');
|
|
7
|
+
const { logger, prompts } = require('../../utils');
|
|
10
8
|
|
|
11
9
|
const log = logger.scope('AlgoMenu');
|
|
12
10
|
|
|
@@ -25,22 +23,17 @@ const algoTradingMenu = async (service) => {
|
|
|
25
23
|
console.log(chalk.gray(getSeparator()));
|
|
26
24
|
console.log();
|
|
27
25
|
|
|
28
|
-
const
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
message: chalk.white.bold('Select Mode:'),
|
|
33
|
-
choices: [
|
|
34
|
-
{ name: chalk.cyan('One Account'), value: 'one_account' },
|
|
35
|
-
{ name: chalk.green('Copy Trading'), value: 'copy_trading' },
|
|
36
|
-
new inquirer.Separator(),
|
|
37
|
-
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
38
|
-
],
|
|
39
|
-
pageSize: 10,
|
|
40
|
-
loop: false
|
|
41
|
-
}
|
|
26
|
+
const action = await prompts.selectOption('Select Mode:', [
|
|
27
|
+
{ value: 'one_account', label: 'One Account' },
|
|
28
|
+
{ value: 'copy_trading', label: 'Copy Trading' },
|
|
29
|
+
{ value: 'back', label: '< Back' }
|
|
42
30
|
]);
|
|
43
31
|
|
|
32
|
+
if (!action || action === 'back') {
|
|
33
|
+
log.debug('User went back');
|
|
34
|
+
return 'back';
|
|
35
|
+
}
|
|
36
|
+
|
|
44
37
|
log.debug('Algo mode selected', { action });
|
|
45
38
|
|
|
46
39
|
switch (action) {
|
|
@@ -52,9 +45,6 @@ const algoTradingMenu = async (service) => {
|
|
|
52
45
|
log.info('Starting Copy Trading mode');
|
|
53
46
|
await copyTradingMenu();
|
|
54
47
|
break;
|
|
55
|
-
case 'back':
|
|
56
|
-
log.debug('User went back');
|
|
57
|
-
break;
|
|
58
48
|
}
|
|
59
49
|
|
|
60
50
|
return action;
|