hedgequantx 1.2.144 → 1.2.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.js +48 -5
- package/src/pages/algo/copy-trading.js +404 -0
- package/src/pages/algo/index.js +51 -0
- package/src/pages/algo/one-account.js +352 -0
- package/src/pages/algo/ui.js +268 -0
- package/src/pages/algo.js +3 -2277
- package/src/services/hqx-server.js +27 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -689,13 +689,56 @@ const dashboardMenu = async (service) => {
|
|
|
689
689
|
console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
|
|
690
690
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
691
691
|
|
|
692
|
-
// Connection info - show all active connections
|
|
692
|
+
// Connection info - show all active connections in boxes (max 3 per row)
|
|
693
693
|
const allConns = connections.getAll();
|
|
694
694
|
if (allConns.length > 0) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
695
|
+
const maxPerRow = 3;
|
|
696
|
+
const boxPadding = 2; // padding inside each mini-box
|
|
697
|
+
const gap = 2; // gap between boxes
|
|
698
|
+
|
|
699
|
+
// Calculate box width based on number of connections (max 3)
|
|
700
|
+
const numBoxes = Math.min(allConns.length, maxPerRow);
|
|
701
|
+
const totalGaps = (numBoxes - 1) * gap;
|
|
702
|
+
const boxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
|
|
703
|
+
|
|
704
|
+
// Process connections in rows of 3
|
|
705
|
+
for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
|
|
706
|
+
const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
|
|
707
|
+
const numInRow = rowConns.length;
|
|
708
|
+
const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
|
|
709
|
+
|
|
710
|
+
// Top border of boxes
|
|
711
|
+
let topLine = ' ';
|
|
712
|
+
for (let i = 0; i < numInRow; i++) {
|
|
713
|
+
topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
|
|
714
|
+
if (i < numInRow - 1) topLine += ' '.repeat(gap);
|
|
715
|
+
}
|
|
716
|
+
const topPad = W - topLine.length;
|
|
717
|
+
console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
|
|
718
|
+
|
|
719
|
+
// Content of boxes
|
|
720
|
+
let contentLine = ' ';
|
|
721
|
+
for (let i = 0; i < numInRow; i++) {
|
|
722
|
+
const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
|
|
723
|
+
const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
|
|
724
|
+
const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
|
|
725
|
+
const textPad = Math.floor((innerWidth - truncated.length) / 2);
|
|
726
|
+
const textPadRight = innerWidth - truncated.length - textPad;
|
|
727
|
+
contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
|
|
728
|
+
if (i < numInRow - 1) contentLine += ' '.repeat(gap);
|
|
729
|
+
}
|
|
730
|
+
const contentPad = W - contentLine.length;
|
|
731
|
+
console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
|
|
732
|
+
|
|
733
|
+
// Bottom border of boxes
|
|
734
|
+
let bottomLine = ' ';
|
|
735
|
+
for (let i = 0; i < numInRow; i++) {
|
|
736
|
+
bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
|
|
737
|
+
if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
|
|
738
|
+
}
|
|
739
|
+
const bottomPad = W - bottomLine.length;
|
|
740
|
+
console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
|
|
741
|
+
}
|
|
699
742
|
}
|
|
700
743
|
|
|
701
744
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy Trading Mode - Mirror trades from Lead to Follower
|
|
3
|
+
* Lightweight - UI + HQX Server handles all execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
const { connections } = require('../../services');
|
|
12
|
+
const { HQXServerService } = require('../../services/hqx-server');
|
|
13
|
+
const { FUTURES_SYMBOLS } = require('../../config');
|
|
14
|
+
const { AlgoUI } = require('./ui');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copy Trading Menu
|
|
18
|
+
*/
|
|
19
|
+
const copyTradingMenu = async () => {
|
|
20
|
+
const allConns = connections.getAll();
|
|
21
|
+
|
|
22
|
+
if (allConns.length < 2) {
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.yellow(' Copy Trading requires 2 connected accounts'));
|
|
25
|
+
console.log(chalk.gray(' Connect to another PropFirm first'));
|
|
26
|
+
console.log();
|
|
27
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(chalk.magenta.bold(' Copy Trading Setup'));
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
// Get all active accounts from all connections
|
|
36
|
+
const allAccounts = [];
|
|
37
|
+
for (const conn of allConns) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await conn.service.getTradingAccounts();
|
|
40
|
+
if (result.success && result.accounts) {
|
|
41
|
+
const active = result.accounts.filter(a => a.status === 0);
|
|
42
|
+
for (const acc of active) {
|
|
43
|
+
allAccounts.push({
|
|
44
|
+
account: acc,
|
|
45
|
+
service: conn.service,
|
|
46
|
+
propfirm: conn.propfirm,
|
|
47
|
+
type: conn.type
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) { /* ignore */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (allAccounts.length < 2) {
|
|
55
|
+
console.log(chalk.yellow(' Need at least 2 active accounts'));
|
|
56
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 1: Select Lead Account
|
|
61
|
+
console.log(chalk.cyan(' Step 1: Select LEAD Account (source of trades)'));
|
|
62
|
+
const leadChoices = allAccounts.map((a, i) => ({
|
|
63
|
+
name: `${a.propfirm} - ${a.account.accountName || a.account.accountId} ($${a.account.balance.toLocaleString()})`,
|
|
64
|
+
value: i
|
|
65
|
+
}));
|
|
66
|
+
leadChoices.push({ name: chalk.yellow('< Cancel'), value: -1 });
|
|
67
|
+
|
|
68
|
+
const { leadIdx } = await inquirer.prompt([{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'leadIdx',
|
|
71
|
+
message: 'Lead Account:',
|
|
72
|
+
choices: leadChoices
|
|
73
|
+
}]);
|
|
74
|
+
|
|
75
|
+
if (leadIdx === -1) return;
|
|
76
|
+
const lead = allAccounts[leadIdx];
|
|
77
|
+
|
|
78
|
+
// Step 2: Select Follower Account
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account (copies trades)'));
|
|
81
|
+
const followerChoices = allAccounts
|
|
82
|
+
.map((a, i) => ({ a, i }))
|
|
83
|
+
.filter(x => x.i !== leadIdx)
|
|
84
|
+
.map(x => ({
|
|
85
|
+
name: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId} ($${x.a.account.balance.toLocaleString()})`,
|
|
86
|
+
value: x.i
|
|
87
|
+
}));
|
|
88
|
+
followerChoices.push({ name: chalk.yellow('< Cancel'), value: -1 });
|
|
89
|
+
|
|
90
|
+
const { followerIdx } = await inquirer.prompt([{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'followerIdx',
|
|
93
|
+
message: 'Follower Account:',
|
|
94
|
+
choices: followerChoices
|
|
95
|
+
}]);
|
|
96
|
+
|
|
97
|
+
if (followerIdx === -1) return;
|
|
98
|
+
const follower = allAccounts[followerIdx];
|
|
99
|
+
|
|
100
|
+
// Step 3: Select Symbol for Lead
|
|
101
|
+
console.log();
|
|
102
|
+
console.log(chalk.cyan(' Step 3: Select Symbol for LEAD'));
|
|
103
|
+
const leadSymbol = await selectSymbol(lead.service, 'Lead');
|
|
104
|
+
if (!leadSymbol) return;
|
|
105
|
+
|
|
106
|
+
// Step 4: Select Symbol for Follower
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(chalk.cyan(' Step 4: Select Symbol for FOLLOWER'));
|
|
109
|
+
const followerSymbol = await selectSymbol(follower.service, 'Follower');
|
|
110
|
+
if (!followerSymbol) return;
|
|
111
|
+
|
|
112
|
+
// Step 5: Configure parameters
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk.cyan(' Step 5: Configure Parameters'));
|
|
115
|
+
|
|
116
|
+
const { leadContracts } = await inquirer.prompt([{
|
|
117
|
+
type: 'number',
|
|
118
|
+
name: 'leadContracts',
|
|
119
|
+
message: 'Lead contracts:',
|
|
120
|
+
default: 1
|
|
121
|
+
}]);
|
|
122
|
+
|
|
123
|
+
const { followerContracts } = await inquirer.prompt([{
|
|
124
|
+
type: 'number',
|
|
125
|
+
name: 'followerContracts',
|
|
126
|
+
message: 'Follower contracts:',
|
|
127
|
+
default: leadContracts
|
|
128
|
+
}]);
|
|
129
|
+
|
|
130
|
+
const { dailyTarget } = await inquirer.prompt([{
|
|
131
|
+
type: 'number',
|
|
132
|
+
name: 'dailyTarget',
|
|
133
|
+
message: 'Daily target ($):',
|
|
134
|
+
default: 400
|
|
135
|
+
}]);
|
|
136
|
+
|
|
137
|
+
const { maxRisk } = await inquirer.prompt([{
|
|
138
|
+
type: 'number',
|
|
139
|
+
name: 'maxRisk',
|
|
140
|
+
message: 'Max risk ($):',
|
|
141
|
+
default: 200
|
|
142
|
+
}]);
|
|
143
|
+
|
|
144
|
+
// Step 6: Privacy
|
|
145
|
+
const { showNames } = await inquirer.prompt([{
|
|
146
|
+
type: 'confirm',
|
|
147
|
+
name: 'showNames',
|
|
148
|
+
message: 'Show account names?',
|
|
149
|
+
default: false
|
|
150
|
+
}]);
|
|
151
|
+
|
|
152
|
+
// Confirm
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(chalk.white(' Summary:'));
|
|
155
|
+
console.log(chalk.gray(` Lead: ${lead.propfirm} -> ${leadSymbol.name} x${leadContracts}`));
|
|
156
|
+
console.log(chalk.gray(` Follower: ${follower.propfirm} -> ${followerSymbol.name} x${followerContracts}`));
|
|
157
|
+
console.log(chalk.gray(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
const { confirm } = await inquirer.prompt([{
|
|
161
|
+
type: 'confirm',
|
|
162
|
+
name: 'confirm',
|
|
163
|
+
message: chalk.yellow('Start Copy Trading?'),
|
|
164
|
+
default: true
|
|
165
|
+
}]);
|
|
166
|
+
|
|
167
|
+
if (!confirm) return;
|
|
168
|
+
|
|
169
|
+
// Launch
|
|
170
|
+
await launchCopyTrading({
|
|
171
|
+
lead: { ...lead, symbol: leadSymbol, contracts: leadContracts },
|
|
172
|
+
follower: { ...follower, symbol: followerSymbol, contracts: followerContracts },
|
|
173
|
+
dailyTarget,
|
|
174
|
+
maxRisk,
|
|
175
|
+
showNames
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Symbol selection helper
|
|
181
|
+
*/
|
|
182
|
+
const selectSymbol = async (service, label) => {
|
|
183
|
+
try {
|
|
184
|
+
const result = await service.getContracts();
|
|
185
|
+
if (!result.success) return null;
|
|
186
|
+
|
|
187
|
+
const choices = [];
|
|
188
|
+
const cats = {};
|
|
189
|
+
|
|
190
|
+
for (const c of result.contracts) {
|
|
191
|
+
const cat = c.group || 'Other';
|
|
192
|
+
if (!cats[cat]) cats[cat] = [];
|
|
193
|
+
cats[cat].push(c);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [cat, list] of Object.entries(cats)) {
|
|
197
|
+
choices.push(new inquirer.Separator(chalk.gray(`--- ${cat} ---`)));
|
|
198
|
+
for (const c of list.slice(0, 8)) {
|
|
199
|
+
choices.push({ name: c.name || c.symbol, value: c });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
choices.push(new inquirer.Separator());
|
|
203
|
+
choices.push({ name: chalk.yellow('< Cancel'), value: null });
|
|
204
|
+
|
|
205
|
+
const { symbol } = await inquirer.prompt([{
|
|
206
|
+
type: 'list',
|
|
207
|
+
name: 'symbol',
|
|
208
|
+
message: `${label} Symbol:`,
|
|
209
|
+
choices,
|
|
210
|
+
pageSize: 15
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
return symbol;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Launch Copy Trading
|
|
221
|
+
*/
|
|
222
|
+
const launchCopyTrading = async (config) => {
|
|
223
|
+
const { lead, follower, dailyTarget, maxRisk, showNames } = config;
|
|
224
|
+
|
|
225
|
+
const leadName = showNames ? (lead.account.accountName || lead.account.accountId) : 'HQX Lead *****';
|
|
226
|
+
const followerName = showNames ? (follower.account.accountName || follower.account.accountId) : 'HQX Follower *****';
|
|
227
|
+
|
|
228
|
+
// UI with copy trading subtitle
|
|
229
|
+
const ui = new AlgoUI({ subtitle: 'HQX Copy Trading' });
|
|
230
|
+
|
|
231
|
+
// Combined stats
|
|
232
|
+
const stats = {
|
|
233
|
+
accountName: `${leadName} -> ${followerName}`,
|
|
234
|
+
symbol: `${lead.symbol.name} / ${follower.symbol.name}`,
|
|
235
|
+
contracts: `${lead.contracts}/${follower.contracts}`,
|
|
236
|
+
target: dailyTarget,
|
|
237
|
+
risk: maxRisk,
|
|
238
|
+
pnl: 0,
|
|
239
|
+
trades: 0,
|
|
240
|
+
wins: 0,
|
|
241
|
+
losses: 0,
|
|
242
|
+
latency: 0,
|
|
243
|
+
connected: false
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let running = true;
|
|
247
|
+
let stopReason = null;
|
|
248
|
+
|
|
249
|
+
// Connect to HQX Server
|
|
250
|
+
const hqx = new HQXServerService();
|
|
251
|
+
|
|
252
|
+
const spinner = ora('Connecting to HQX Server...').start();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const auth = await hqx.authenticate(lead.account.accountId.toString(), lead.propfirm || 'topstep');
|
|
256
|
+
if (!auth.success) throw new Error(auth.error);
|
|
257
|
+
|
|
258
|
+
const conn = await hqx.connect();
|
|
259
|
+
if (!conn.success) throw new Error('WebSocket failed');
|
|
260
|
+
|
|
261
|
+
spinner.succeed('Connected');
|
|
262
|
+
stats.connected = true;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
spinner.warn('HQX Server unavailable');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Event handlers
|
|
268
|
+
hqx.on('latency', (d) => { stats.latency = d.latency || 0; });
|
|
269
|
+
|
|
270
|
+
hqx.on('log', (d) => {
|
|
271
|
+
let msg = d.message;
|
|
272
|
+
if (!showNames) {
|
|
273
|
+
if (lead.account.accountName) msg = msg.replace(new RegExp(lead.account.accountName, 'gi'), 'Lead *****');
|
|
274
|
+
if (follower.account.accountName) msg = msg.replace(new RegExp(follower.account.accountName, 'gi'), 'Follower *****');
|
|
275
|
+
}
|
|
276
|
+
ui.addLog(d.type || 'info', msg);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
hqx.on('trade', (d) => {
|
|
280
|
+
stats.trades++;
|
|
281
|
+
stats.pnl += d.pnl || 0;
|
|
282
|
+
d.pnl >= 0 ? stats.wins++ : stats.losses++;
|
|
283
|
+
ui.addLog(d.pnl >= 0 ? 'trade' : 'loss', `${d.pnl >= 0 ? '+' : ''}$${d.pnl.toFixed(2)}`);
|
|
284
|
+
|
|
285
|
+
if (stats.pnl >= dailyTarget) {
|
|
286
|
+
stopReason = 'target';
|
|
287
|
+
running = false;
|
|
288
|
+
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
289
|
+
hqx.stopAlgo();
|
|
290
|
+
} else if (stats.pnl <= -maxRisk) {
|
|
291
|
+
stopReason = 'risk';
|
|
292
|
+
running = false;
|
|
293
|
+
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
294
|
+
hqx.stopAlgo();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
hqx.on('copy', (d) => {
|
|
299
|
+
ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x to Follower`);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
hqx.on('error', (d) => { ui.addLog('error', d.message); });
|
|
303
|
+
hqx.on('disconnected', () => { stats.connected = false; });
|
|
304
|
+
|
|
305
|
+
// Start copy trading on server
|
|
306
|
+
if (stats.connected) {
|
|
307
|
+
ui.addLog('info', 'Starting Copy Trading...');
|
|
308
|
+
|
|
309
|
+
// Get credentials
|
|
310
|
+
let leadCreds = null, followerCreds = null;
|
|
311
|
+
|
|
312
|
+
if (lead.service.getRithmicCredentials) {
|
|
313
|
+
leadCreds = lead.service.getRithmicCredentials();
|
|
314
|
+
}
|
|
315
|
+
if (follower.service.getRithmicCredentials) {
|
|
316
|
+
followerCreds = follower.service.getRithmicCredentials();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
hqx.startCopyTrading({
|
|
320
|
+
// Lead config
|
|
321
|
+
leadAccountId: lead.account.accountId,
|
|
322
|
+
leadContractId: lead.symbol.id || lead.symbol.contractId,
|
|
323
|
+
leadSymbol: lead.symbol.symbol || lead.symbol.name,
|
|
324
|
+
leadContracts: lead.contracts,
|
|
325
|
+
leadPropfirm: lead.propfirm,
|
|
326
|
+
leadToken: lead.service.getToken ? lead.service.getToken() : null,
|
|
327
|
+
leadRithmicCredentials: leadCreds,
|
|
328
|
+
|
|
329
|
+
// Follower config
|
|
330
|
+
followerAccountId: follower.account.accountId,
|
|
331
|
+
followerContractId: follower.symbol.id || follower.symbol.contractId,
|
|
332
|
+
followerSymbol: follower.symbol.symbol || follower.symbol.name,
|
|
333
|
+
followerContracts: follower.contracts,
|
|
334
|
+
followerPropfirm: follower.propfirm,
|
|
335
|
+
followerToken: follower.service.getToken ? follower.service.getToken() : null,
|
|
336
|
+
followerRithmicCredentials: followerCreds,
|
|
337
|
+
|
|
338
|
+
// Targets
|
|
339
|
+
dailyTarget,
|
|
340
|
+
maxRisk
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// UI refresh
|
|
345
|
+
const refreshInterval = setInterval(() => {
|
|
346
|
+
if (running) ui.render(stats);
|
|
347
|
+
}, 250);
|
|
348
|
+
|
|
349
|
+
// Keyboard
|
|
350
|
+
const setupKeys = () => {
|
|
351
|
+
if (!process.stdin.isTTY) return null;
|
|
352
|
+
readline.emitKeypressEvents(process.stdin);
|
|
353
|
+
process.stdin.setRawMode(true);
|
|
354
|
+
process.stdin.resume();
|
|
355
|
+
|
|
356
|
+
const handler = (str, key) => {
|
|
357
|
+
if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
|
|
358
|
+
running = false;
|
|
359
|
+
stopReason = 'manual';
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
process.stdin.on('keypress', handler);
|
|
363
|
+
|
|
364
|
+
return () => {
|
|
365
|
+
process.stdin.removeListener('keypress', handler);
|
|
366
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const cleanupKeys = setupKeys();
|
|
371
|
+
|
|
372
|
+
// Wait
|
|
373
|
+
await new Promise(resolve => {
|
|
374
|
+
const check = setInterval(() => {
|
|
375
|
+
if (!running) { clearInterval(check); resolve(); }
|
|
376
|
+
}, 100);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Cleanup
|
|
380
|
+
clearInterval(refreshInterval);
|
|
381
|
+
if (cleanupKeys) cleanupKeys();
|
|
382
|
+
|
|
383
|
+
if (stats.connected) {
|
|
384
|
+
hqx.stopAlgo();
|
|
385
|
+
hqx.disconnect();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ui.cleanup();
|
|
389
|
+
|
|
390
|
+
// Summary
|
|
391
|
+
console.clear();
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(chalk.cyan(' === Copy Trading Summary ==='));
|
|
394
|
+
console.log();
|
|
395
|
+
console.log(chalk.white(` Stop: ${stopReason || 'unknown'}`));
|
|
396
|
+
console.log(chalk.white(` Trades: ${stats.trades} (W: ${stats.wins} / L: ${stats.losses})`));
|
|
397
|
+
const c = stats.pnl >= 0 ? chalk.green : chalk.red;
|
|
398
|
+
console.log(c(` P&L: ${stats.pnl >= 0 ? '+' : ''}$${stats.pnl.toFixed(2)}`));
|
|
399
|
+
console.log();
|
|
400
|
+
|
|
401
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter...' }]);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
module.exports = { copyTradingMenu };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algo Trading - Main Menu
|
|
3
|
+
* Lightweight entry point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const { getSeparator } = require('../../ui');
|
|
9
|
+
|
|
10
|
+
const { oneAccountMenu } = require('./one-account');
|
|
11
|
+
const { copyTradingMenu } = require('./copy-trading');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Algo Trading Menu
|
|
15
|
+
*/
|
|
16
|
+
const algoTradingMenu = async (service) => {
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(chalk.gray(getSeparator()));
|
|
19
|
+
console.log(chalk.magenta.bold(' Algo-Trading'));
|
|
20
|
+
console.log(chalk.gray(getSeparator()));
|
|
21
|
+
console.log();
|
|
22
|
+
|
|
23
|
+
const { action } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'action',
|
|
27
|
+
message: chalk.white.bold('Select Mode:'),
|
|
28
|
+
choices: [
|
|
29
|
+
{ name: chalk.cyan('One Account'), value: 'one_account' },
|
|
30
|
+
{ name: chalk.green('Copy Trading'), value: 'copy_trading' },
|
|
31
|
+
new inquirer.Separator(),
|
|
32
|
+
{ name: chalk.yellow('< Back'), value: 'back' }
|
|
33
|
+
],
|
|
34
|
+
pageSize: 10,
|
|
35
|
+
loop: false
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
switch (action) {
|
|
40
|
+
case 'one_account':
|
|
41
|
+
await oneAccountMenu(service);
|
|
42
|
+
break;
|
|
43
|
+
case 'copy_trading':
|
|
44
|
+
await copyTradingMenu();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return action;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { algoTradingMenu };
|