hedgequantx 2.7.98 → 2.8.0
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 +3 -4
- package/src/pages/ai-agents-ui.js +46 -1
- package/src/pages/ai-agents.js +84 -54
- package/src/pages/algo/algo-executor.js +61 -19
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +44 -263
- package/src/pages/algo/custom-strategy.js +27 -2
- package/src/pages/algo/one-account.js +48 -1
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +288 -0
- package/src/services/ai-supervision/index.js +309 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +32 -1
- package/src/services/index.js +5 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy Trading Executor - Execution engine for copy trading
|
|
3
|
+
* Handles signal processing, order placement, and AI supervision
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { connections } = require('../../services');
|
|
8
|
+
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
9
|
+
const { SupervisionEngine } = require('../../services/ai-supervision');
|
|
10
|
+
const { M1 } = require('../../lib/m/s1');
|
|
11
|
+
const { MarketDataFeed } = require('../../lib/data');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Launch Copy Trading execution
|
|
15
|
+
*/
|
|
16
|
+
const launchCopyTrading = async (config) => {
|
|
17
|
+
const { lead, followers, contract, dailyTarget, maxRisk, showNames, supervisionConfig } = config;
|
|
18
|
+
|
|
19
|
+
// Initialize AI Supervision if configured
|
|
20
|
+
const supervisionEnabled = supervisionConfig?.supervisionEnabled && supervisionConfig?.agents?.length > 0;
|
|
21
|
+
const supervisionEngine = supervisionEnabled ? new SupervisionEngine(supervisionConfig) : null;
|
|
22
|
+
const aiContext = { recentTicks: [], recentSignals: [], maxTicks: 100 };
|
|
23
|
+
|
|
24
|
+
const leadAccount = lead.account;
|
|
25
|
+
const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
|
|
26
|
+
const leadName = showNames
|
|
27
|
+
? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
|
|
28
|
+
: 'HQX Lead *****';
|
|
29
|
+
const symbolName = contract.name;
|
|
30
|
+
const contractId = contract.id;
|
|
31
|
+
const tickSize = contract.tickSize || 0.25;
|
|
32
|
+
|
|
33
|
+
const followerNames = followers.map((f, i) =>
|
|
34
|
+
showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const ui = new AlgoUI({
|
|
38
|
+
subtitle: supervisionEnabled ? 'HQX Copy + AI' : 'HQX Copy Trading',
|
|
39
|
+
mode: 'copy-trading'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const stats = {
|
|
43
|
+
accountName: leadName,
|
|
44
|
+
followerNames,
|
|
45
|
+
symbol: symbolName,
|
|
46
|
+
qty: lead.contracts,
|
|
47
|
+
followerQty: followers[0]?.contracts || lead.contracts,
|
|
48
|
+
target: dailyTarget,
|
|
49
|
+
risk: maxRisk,
|
|
50
|
+
propfirm: leadAccount.propfirm || 'Unknown',
|
|
51
|
+
platform: leadAccount.platform || 'Rithmic',
|
|
52
|
+
pnl: 0,
|
|
53
|
+
followerPnl: 0,
|
|
54
|
+
trades: 0,
|
|
55
|
+
wins: 0,
|
|
56
|
+
losses: 0,
|
|
57
|
+
latency: 0,
|
|
58
|
+
connected: false,
|
|
59
|
+
startTime: Date.now(),
|
|
60
|
+
followersCount: followers.length
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let running = true;
|
|
64
|
+
let stopReason = null;
|
|
65
|
+
let startingPnL = null;
|
|
66
|
+
let currentPosition = 0;
|
|
67
|
+
let pendingOrder = false;
|
|
68
|
+
let tickCount = 0;
|
|
69
|
+
|
|
70
|
+
// Initialize Strategy
|
|
71
|
+
const strategy = new M1({ tickSize });
|
|
72
|
+
strategy.initialize(contractId, tickSize);
|
|
73
|
+
|
|
74
|
+
// Initialize Market Data Feed
|
|
75
|
+
const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
|
|
76
|
+
|
|
77
|
+
// Log startup
|
|
78
|
+
ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
|
|
79
|
+
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${lead.contracts}/${followers[0]?.contracts}`);
|
|
80
|
+
ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
81
|
+
if (supervisionEnabled) ui.addLog('info', `AI: ${supervisionEngine.getActiveCount()} agents`);
|
|
82
|
+
ui.addLog('info', 'Connecting...');
|
|
83
|
+
|
|
84
|
+
// Handle strategy signals
|
|
85
|
+
strategy.on('signal', async (signal) => {
|
|
86
|
+
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
87
|
+
|
|
88
|
+
let { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
89
|
+
|
|
90
|
+
aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
|
|
91
|
+
if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
|
|
92
|
+
|
|
93
|
+
ui.addLog('signal', `${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
94
|
+
|
|
95
|
+
// AI Supervision
|
|
96
|
+
if (supervisionEnabled && supervisionEngine) {
|
|
97
|
+
const result = await supervisionEngine.supervise({
|
|
98
|
+
symbolId: symbolName,
|
|
99
|
+
signal: { direction, entry, stopLoss, takeProfit, confidence },
|
|
100
|
+
recentTicks: aiContext.recentTicks,
|
|
101
|
+
recentSignals: aiContext.recentSignals,
|
|
102
|
+
stats,
|
|
103
|
+
config: { dailyTarget, maxRisk }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (result.decision === 'reject') {
|
|
107
|
+
ui.addLog('info', `AI rejected: ${result.reason}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Apply optimizations
|
|
112
|
+
if (result.optimizedSignal?.aiOptimized) {
|
|
113
|
+
const opt = result.optimizedSignal;
|
|
114
|
+
if (opt.entry) entry = opt.entry;
|
|
115
|
+
if (opt.stopLoss) stopLoss = opt.stopLoss;
|
|
116
|
+
if (opt.takeProfit) takeProfit = opt.takeProfit;
|
|
117
|
+
}
|
|
118
|
+
ui.addLog('info', `AI ${result.decision} (${result.confidence}%)`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Execute orders
|
|
122
|
+
pendingOrder = true;
|
|
123
|
+
try {
|
|
124
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
125
|
+
|
|
126
|
+
// Place order on LEAD
|
|
127
|
+
const leadResult = await leadService.placeOrder({
|
|
128
|
+
accountId: leadAccount.accountId,
|
|
129
|
+
contractId,
|
|
130
|
+
type: 2,
|
|
131
|
+
side: orderSide,
|
|
132
|
+
size: lead.contracts
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (leadResult.success) {
|
|
136
|
+
currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
|
|
137
|
+
stats.trades++;
|
|
138
|
+
ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x`);
|
|
139
|
+
|
|
140
|
+
// Place orders on FOLLOWERS
|
|
141
|
+
await placeFollowerOrders(followers, contractId, orderSide, direction, ui);
|
|
142
|
+
|
|
143
|
+
// Bracket orders on lead
|
|
144
|
+
if (stopLoss && takeProfit) {
|
|
145
|
+
await placeBracketOrders(leadService, leadAccount, contractId, direction, lead.contracts, stopLoss, takeProfit);
|
|
146
|
+
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
ui.addLog('error', `Lead failed: ${leadResult.error}`);
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
ui.addLog('error', `Order error: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
pendingOrder = false;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Handle market data ticks
|
|
158
|
+
marketFeed.on('tick', (tick) => {
|
|
159
|
+
tickCount++;
|
|
160
|
+
const latencyStart = Date.now();
|
|
161
|
+
|
|
162
|
+
aiContext.recentTicks.push(tick);
|
|
163
|
+
if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
|
|
164
|
+
|
|
165
|
+
strategy.processTick({
|
|
166
|
+
contractId: tick.contractId || contractId,
|
|
167
|
+
price: tick.price,
|
|
168
|
+
bid: tick.bid,
|
|
169
|
+
ask: tick.ask,
|
|
170
|
+
volume: tick.volume || 1,
|
|
171
|
+
side: tick.lastTradeSide || 'unknown',
|
|
172
|
+
timestamp: tick.timestamp || Date.now()
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
stats.latency = Date.now() - latencyStart;
|
|
176
|
+
if (tickCount % 100 === 0) ui.addLog('info', `#${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
marketFeed.on('connected', () => { stats.connected = true; ui.addLog('success', 'Connected!'); });
|
|
180
|
+
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
181
|
+
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Disconnected'); });
|
|
182
|
+
|
|
183
|
+
// Connect to market data
|
|
184
|
+
try {
|
|
185
|
+
const token = leadService.token || leadService.getToken?.();
|
|
186
|
+
const pk = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
187
|
+
await marketFeed.connect(token, pk, contractId);
|
|
188
|
+
await marketFeed.subscribe(symbolName, contractId);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
ui.addLog('error', `Connect failed: ${e.message}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Poll P&L
|
|
194
|
+
const pollPnL = async () => {
|
|
195
|
+
try {
|
|
196
|
+
const res = await leadService.getTradingAccounts();
|
|
197
|
+
if (res.success && res.accounts) {
|
|
198
|
+
const acc = res.accounts.find(a => a.accountId === leadAccount.accountId);
|
|
199
|
+
if (acc?.profitAndLoss !== undefined) {
|
|
200
|
+
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
201
|
+
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (stats.pnl >= dailyTarget) {
|
|
205
|
+
stopReason = 'target';
|
|
206
|
+
running = false;
|
|
207
|
+
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
208
|
+
} else if (stats.pnl <= -maxRisk) {
|
|
209
|
+
stopReason = 'risk';
|
|
210
|
+
running = false;
|
|
211
|
+
ui.addLog('error', `RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
212
|
+
}
|
|
213
|
+
} catch (e) { /* silent */ }
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Start intervals
|
|
217
|
+
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
218
|
+
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
219
|
+
pollPnL();
|
|
220
|
+
|
|
221
|
+
// Keyboard handler
|
|
222
|
+
const cleanupKeys = setupKeyHandler(() => { running = false; stopReason = 'manual'; });
|
|
223
|
+
|
|
224
|
+
// Wait for stop
|
|
225
|
+
await new Promise(resolve => {
|
|
226
|
+
const check = setInterval(() => {
|
|
227
|
+
if (!running) { clearInterval(check); resolve(); }
|
|
228
|
+
}, 100);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Cleanup
|
|
232
|
+
clearInterval(refreshInterval);
|
|
233
|
+
clearInterval(pnlInterval);
|
|
234
|
+
await marketFeed.disconnect();
|
|
235
|
+
if (cleanupKeys) cleanupKeys();
|
|
236
|
+
ui.cleanup();
|
|
237
|
+
|
|
238
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
239
|
+
process.stdin.resume();
|
|
240
|
+
|
|
241
|
+
// Duration
|
|
242
|
+
const durationMs = Date.now() - stats.startTime;
|
|
243
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
244
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
245
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
246
|
+
stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
247
|
+
|
|
248
|
+
renderSessionSummary(stats, stopReason);
|
|
249
|
+
|
|
250
|
+
console.log('\n Returning to menu in 3 seconds...');
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Place orders on all follower accounts
|
|
256
|
+
*/
|
|
257
|
+
const placeFollowerOrders = async (followers, contractId, orderSide, direction, ui) => {
|
|
258
|
+
for (let i = 0; i < followers.length; i++) {
|
|
259
|
+
const f = followers[i];
|
|
260
|
+
const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const fResult = await fService.placeOrder({
|
|
264
|
+
accountId: f.account.accountId,
|
|
265
|
+
contractId,
|
|
266
|
+
type: 2,
|
|
267
|
+
side: orderSide,
|
|
268
|
+
size: f.contracts
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (fResult.success) {
|
|
272
|
+
ui.addLog('trade', `F${i + 1}: ${direction.toUpperCase()} ${f.contracts}x`);
|
|
273
|
+
} else {
|
|
274
|
+
ui.addLog('error', `F${i + 1}: Failed`);
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
ui.addLog('error', `F${i + 1}: ${e.message}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Place bracket orders (stop loss and take profit)
|
|
284
|
+
*/
|
|
285
|
+
const placeBracketOrders = async (service, account, contractId, direction, size, stopLoss, takeProfit) => {
|
|
286
|
+
const exitSide = direction === 'long' ? 1 : 0;
|
|
287
|
+
|
|
288
|
+
await service.placeOrder({
|
|
289
|
+
accountId: account.accountId,
|
|
290
|
+
contractId,
|
|
291
|
+
type: 4,
|
|
292
|
+
side: exitSide,
|
|
293
|
+
size,
|
|
294
|
+
stopPrice: stopLoss
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await service.placeOrder({
|
|
298
|
+
accountId: account.accountId,
|
|
299
|
+
contractId,
|
|
300
|
+
type: 1,
|
|
301
|
+
side: exitSide,
|
|
302
|
+
size,
|
|
303
|
+
limitPrice: takeProfit
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Setup keyboard handler for stopping
|
|
309
|
+
*/
|
|
310
|
+
const setupKeyHandler = (onStop) => {
|
|
311
|
+
if (!process.stdin.isTTY) return null;
|
|
312
|
+
|
|
313
|
+
readline.emitKeypressEvents(process.stdin);
|
|
314
|
+
process.stdin.setRawMode(true);
|
|
315
|
+
process.stdin.resume();
|
|
316
|
+
|
|
317
|
+
const onKey = (str, key) => {
|
|
318
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
319
|
+
onStop();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
process.stdin.on('keypress', onKey);
|
|
324
|
+
|
|
325
|
+
return () => {
|
|
326
|
+
process.stdin.removeListener('keypress', onKey);
|
|
327
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
module.exports = { launchCopyTrading };
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copy Trading Mode - HQX Ultra Scalping
|
|
3
3
|
* Same as One Account but copies trades to multiple followers
|
|
4
|
+
* Supports multi-agent AI supervision
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const chalk = require('chalk');
|
|
7
8
|
const ora = require('ora');
|
|
8
|
-
const readline = require('readline');
|
|
9
9
|
|
|
10
10
|
const { connections } = require('../../services');
|
|
11
|
-
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
12
11
|
const { prompts } = require('../../utils');
|
|
13
12
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const {
|
|
17
|
-
const { MarketDataFeed } = require('../../lib/data');
|
|
13
|
+
const { getActiveAgentCount, getSupervisionConfig, getActiveAgents } = require('../ai-agents');
|
|
14
|
+
const { launchCopyTrading } = require('./copy-executor');
|
|
15
|
+
const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('../../services/ai-supervision');
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
18
|
* Copy Trading Menu
|
|
@@ -144,6 +142,43 @@ const copyTradingMenu = async () => {
|
|
|
144
142
|
const showNames = await prompts.confirmPrompt('Show account names?', false);
|
|
145
143
|
if (showNames === null) return;
|
|
146
144
|
|
|
145
|
+
// Check for AI Supervision
|
|
146
|
+
const agentCount = getActiveAgentCount();
|
|
147
|
+
let supervisionConfig = null;
|
|
148
|
+
|
|
149
|
+
if (agentCount > 0) {
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.cyan(` ${agentCount} AI Agent(s) available for supervision`));
|
|
152
|
+
const enableAI = await prompts.confirmPrompt('Enable AI Supervision?', true);
|
|
153
|
+
|
|
154
|
+
if (enableAI) {
|
|
155
|
+
// Run pre-flight check - ALL agents must pass
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(chalk.yellow(' Running AI pre-flight check...'));
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
const agents = getActiveAgents();
|
|
161
|
+
const preflightResults = await runPreflightCheck(agents);
|
|
162
|
+
|
|
163
|
+
// Display results
|
|
164
|
+
const lines = formatPreflightResults(preflightResults, 60);
|
|
165
|
+
for (const line of lines) console.log(line);
|
|
166
|
+
|
|
167
|
+
const summary = getPreflightSummary(preflightResults);
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(` ${summary.text}`);
|
|
170
|
+
console.log();
|
|
171
|
+
|
|
172
|
+
if (!preflightResults.success) {
|
|
173
|
+
console.log(chalk.red(' Cannot start algo - fix agent connections first.'));
|
|
174
|
+
await prompts.waitForEnter();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
supervisionConfig = getSupervisionConfig();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
// Summary
|
|
148
183
|
console.log();
|
|
149
184
|
console.log(chalk.white.bold(' SUMMARY:'));
|
|
@@ -154,6 +189,7 @@ const copyTradingMenu = async () => {
|
|
|
154
189
|
console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
|
|
155
190
|
}
|
|
156
191
|
console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
192
|
+
if (supervisionConfig) console.log(chalk.green(` AI Supervision: ${agentCount} agent(s)`));
|
|
157
193
|
console.log();
|
|
158
194
|
|
|
159
195
|
const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
|
|
@@ -165,7 +201,8 @@ const copyTradingMenu = async () => {
|
|
|
165
201
|
contract,
|
|
166
202
|
dailyTarget,
|
|
167
203
|
maxRisk,
|
|
168
|
-
showNames
|
|
204
|
+
showNames,
|
|
205
|
+
supervisionConfig
|
|
169
206
|
});
|
|
170
207
|
};
|
|
171
208
|
|
|
@@ -209,260 +246,4 @@ const selectSymbol = async (service) => {
|
|
|
209
246
|
return selected === 'back' || selected === null ? null : selected;
|
|
210
247
|
};
|
|
211
248
|
|
|
212
|
-
/**
|
|
213
|
-
* Launch Copy Trading - HQX Ultra Scalping with trade copying
|
|
214
|
-
*/
|
|
215
|
-
const launchCopyTrading = async (config) => {
|
|
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
|
-
|
|
231
|
-
const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
|
|
232
|
-
|
|
233
|
-
const stats = {
|
|
234
|
-
accountName: leadName,
|
|
235
|
-
followerNames,
|
|
236
|
-
symbol: symbolName,
|
|
237
|
-
qty: lead.contracts,
|
|
238
|
-
followerQty: followers[0]?.contracts || lead.contracts,
|
|
239
|
-
target: dailyTarget,
|
|
240
|
-
risk: maxRisk,
|
|
241
|
-
propfirm: leadAccount.propfirm || 'Unknown',
|
|
242
|
-
platform: leadAccount.platform || 'Rithmic',
|
|
243
|
-
pnl: 0,
|
|
244
|
-
followerPnl: 0,
|
|
245
|
-
trades: 0,
|
|
246
|
-
wins: 0,
|
|
247
|
-
losses: 0,
|
|
248
|
-
latency: 0,
|
|
249
|
-
connected: false,
|
|
250
|
-
startTime: Date.now(),
|
|
251
|
-
followersCount: followers.length
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
let running = true;
|
|
255
|
-
let stopReason = null;
|
|
256
|
-
let startingPnL = null;
|
|
257
|
-
let currentPosition = 0;
|
|
258
|
-
let pendingOrder = false;
|
|
259
|
-
let tickCount = 0;
|
|
260
|
-
|
|
261
|
-
// Initialize Strategy
|
|
262
|
-
const strategy = new M1({ tickSize });
|
|
263
|
-
strategy.initialize(contractId, tickSize);
|
|
264
|
-
|
|
265
|
-
// Initialize Market Data Feed
|
|
266
|
-
const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
|
|
267
|
-
|
|
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;
|
|
283
|
-
try {
|
|
284
|
-
const orderSide = direction === 'long' ? 0 : 1;
|
|
285
|
-
|
|
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
|
-
});
|
|
294
|
-
|
|
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
|
-
}
|
|
322
|
-
}
|
|
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
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
ui.addLog('error', `Lead order failed: ${leadResult.error}`);
|
|
338
|
-
}
|
|
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;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Check target/risk
|
|
399
|
-
if (stats.pnl >= dailyTarget) {
|
|
400
|
-
stopReason = 'target';
|
|
401
|
-
running = false;
|
|
402
|
-
ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
403
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
404
|
-
stopReason = 'risk';
|
|
405
|
-
running = false;
|
|
406
|
-
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
407
|
-
}
|
|
408
|
-
} catch (e) { /* silent */ }
|
|
409
|
-
};
|
|
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
|
-
|
|
438
|
-
// Wait for stop
|
|
439
|
-
await new Promise(resolve => {
|
|
440
|
-
const check = setInterval(() => {
|
|
441
|
-
if (!running) { clearInterval(check); resolve(); }
|
|
442
|
-
}, 100);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
// Cleanup
|
|
446
|
-
clearInterval(refreshInterval);
|
|
447
|
-
clearInterval(pnlInterval);
|
|
448
|
-
await marketFeed.disconnect();
|
|
449
|
-
if (cleanupKeys) cleanupKeys();
|
|
450
|
-
ui.cleanup();
|
|
451
|
-
|
|
452
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
453
|
-
process.stdin.resume();
|
|
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));
|
|
466
|
-
};
|
|
467
|
-
|
|
468
249
|
module.exports = { copyTradingMenu };
|