hedgequantx 2.5.43 → 2.6.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/bin/cli.js +11 -0
- package/package.json +1 -1
- package/src/app.js +11 -0
- package/src/config/settings.js +29 -0
- package/src/pages/algo/copy-trading.js +692 -253
- package/src/pages/algo/one-account.js +241 -47
- package/src/services/position-manager.js +927 -0
- package/src/services/rithmic/connection.js +201 -1
- package/src/services/rithmic/handlers.js +341 -5
- package/src/services/rithmic/index.js +75 -3
- package/src/services/rithmic/orders.js +193 -1
- package/src/services/rithmic/protobuf.js +171 -2
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Copy Trading
|
|
2
|
+
* @fileoverview Professional Copy Trading System
|
|
3
3
|
* @module pages/algo/copy-trading
|
|
4
|
+
*
|
|
5
|
+
* Ultra-low latency copy trading with:
|
|
6
|
+
* - Fast polling (250ms adaptive)
|
|
7
|
+
* - Multi-follower support
|
|
8
|
+
* - Parallel order execution
|
|
9
|
+
* - Automatic retry with exponential backoff
|
|
10
|
+
* - Position reconciliation
|
|
11
|
+
* - Slippage protection
|
|
12
|
+
* - Cross-platform support (ProjectX <-> Rithmic)
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
15
|
const chalk = require('chalk');
|
|
@@ -13,8 +22,512 @@ const { logger, prompts } = require('../../utils');
|
|
|
13
22
|
const { checkMarketHours } = require('../../services/projectx/market');
|
|
14
23
|
const { algoLogger } = require('./logger');
|
|
15
24
|
|
|
25
|
+
// AI Strategy Supervisor
|
|
26
|
+
const aiService = require('../../services/ai');
|
|
27
|
+
const StrategySupervisor = require('../../services/ai/strategy-supervisor');
|
|
28
|
+
|
|
16
29
|
const log = logger.scope('CopyTrading');
|
|
17
30
|
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// COPY ENGINE - Professional Order Execution
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CopyEngine - Handles all copy trading logic with professional execution
|
|
37
|
+
*/
|
|
38
|
+
class CopyEngine {
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.lead = config.lead;
|
|
41
|
+
this.followers = config.followers; // Array of followers
|
|
42
|
+
this.symbol = config.symbol;
|
|
43
|
+
this.dailyTarget = config.dailyTarget;
|
|
44
|
+
this.maxRisk = config.maxRisk;
|
|
45
|
+
this.ui = config.ui;
|
|
46
|
+
this.stats = config.stats;
|
|
47
|
+
|
|
48
|
+
// Engine state
|
|
49
|
+
this.running = false;
|
|
50
|
+
this.stopReason = null;
|
|
51
|
+
|
|
52
|
+
// Position tracking
|
|
53
|
+
this.leadPositions = new Map(); // key: positionKey, value: position
|
|
54
|
+
this.followerPositions = new Map(); // key: `${followerIdx}:${posKey}`, value: position
|
|
55
|
+
this.pendingOrders = new Map(); // key: orderId, value: orderInfo
|
|
56
|
+
|
|
57
|
+
// Order queue for sequential execution per follower
|
|
58
|
+
this.orderQueues = new Map(); // key: followerIdx, value: queue[]
|
|
59
|
+
this.processingQueue = new Map(); // key: followerIdx, value: boolean
|
|
60
|
+
|
|
61
|
+
// Timing
|
|
62
|
+
this.pollInterval = 250; // Start at 250ms, adaptive
|
|
63
|
+
this.lastPollTime = 0;
|
|
64
|
+
this.pollCount = 0;
|
|
65
|
+
this.orderCount = 0;
|
|
66
|
+
this.failedOrders = 0;
|
|
67
|
+
|
|
68
|
+
// Retry configuration
|
|
69
|
+
this.maxRetries = 3;
|
|
70
|
+
this.retryDelayBase = 100; // ms
|
|
71
|
+
|
|
72
|
+
// Slippage protection (ticks)
|
|
73
|
+
this.maxSlippageTicks = 4;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get unique position key (cross-platform compatible)
|
|
78
|
+
*/
|
|
79
|
+
getPositionKey(position) {
|
|
80
|
+
return position.contractId || position.symbol || position.id;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve symbol for target platform
|
|
85
|
+
*/
|
|
86
|
+
resolveSymbol(position, targetAccount) {
|
|
87
|
+
const targetType = targetAccount.type;
|
|
88
|
+
|
|
89
|
+
if (targetType === 'rithmic') {
|
|
90
|
+
return {
|
|
91
|
+
symbol: position.symbol || this.symbol.name,
|
|
92
|
+
exchange: position.exchange || this.symbol.exchange || 'CME',
|
|
93
|
+
contractId: null
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
return {
|
|
97
|
+
contractId: position.contractId || this.symbol.id || this.symbol.contractId,
|
|
98
|
+
symbol: null,
|
|
99
|
+
exchange: null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build order data for specific platform
|
|
106
|
+
*/
|
|
107
|
+
buildOrderData(params, platformType) {
|
|
108
|
+
const { accountId, contractId, symbol, exchange, side, size, type, price } = params;
|
|
109
|
+
|
|
110
|
+
if (platformType === 'rithmic') {
|
|
111
|
+
return {
|
|
112
|
+
accountId,
|
|
113
|
+
symbol,
|
|
114
|
+
exchange: exchange || 'CME',
|
|
115
|
+
size,
|
|
116
|
+
side,
|
|
117
|
+
type,
|
|
118
|
+
price: price || 0
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
return {
|
|
122
|
+
accountId,
|
|
123
|
+
contractId,
|
|
124
|
+
type,
|
|
125
|
+
side,
|
|
126
|
+
size
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute order with retry logic
|
|
133
|
+
*/
|
|
134
|
+
async executeOrderWithRetry(follower, orderData, retryCount = 0) {
|
|
135
|
+
try {
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
const result = await follower.service.placeOrder(orderData);
|
|
138
|
+
const latency = Date.now() - startTime;
|
|
139
|
+
|
|
140
|
+
if (result.success) {
|
|
141
|
+
this.orderCount++;
|
|
142
|
+
this.stats.latency = Math.round((this.stats.latency + latency) / 2);
|
|
143
|
+
return { success: true, latency };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Retry on failure
|
|
147
|
+
if (retryCount < this.maxRetries) {
|
|
148
|
+
const delay = this.retryDelayBase * Math.pow(2, retryCount);
|
|
149
|
+
await this.sleep(delay);
|
|
150
|
+
return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.failedOrders++;
|
|
154
|
+
return { success: false, error: result.error || 'Max retries exceeded' };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (retryCount < this.maxRetries) {
|
|
157
|
+
const delay = this.retryDelayBase * Math.pow(2, retryCount);
|
|
158
|
+
await this.sleep(delay);
|
|
159
|
+
return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.failedOrders++;
|
|
163
|
+
return { success: false, error: err.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Queue order for a follower (ensures sequential execution per follower)
|
|
169
|
+
*/
|
|
170
|
+
async queueOrder(followerIdx, orderFn) {
|
|
171
|
+
if (!this.orderQueues.has(followerIdx)) {
|
|
172
|
+
this.orderQueues.set(followerIdx, []);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
this.orderQueues.get(followerIdx).push({ fn: orderFn, resolve });
|
|
177
|
+
this.processQueue(followerIdx);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Process order queue for a follower
|
|
183
|
+
*/
|
|
184
|
+
async processQueue(followerIdx) {
|
|
185
|
+
if (this.processingQueue.get(followerIdx)) return;
|
|
186
|
+
|
|
187
|
+
const queue = this.orderQueues.get(followerIdx);
|
|
188
|
+
if (!queue || queue.length === 0) return;
|
|
189
|
+
|
|
190
|
+
this.processingQueue.set(followerIdx, true);
|
|
191
|
+
|
|
192
|
+
while (queue.length > 0 && this.running) {
|
|
193
|
+
const { fn, resolve } = queue.shift();
|
|
194
|
+
try {
|
|
195
|
+
const result = await fn();
|
|
196
|
+
resolve(result);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
resolve({ success: false, error: err.message });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.processingQueue.set(followerIdx, false);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Copy position open to all followers (parallel execution)
|
|
207
|
+
*/
|
|
208
|
+
async copyPositionOpen(position) {
|
|
209
|
+
const side = position.quantity > 0 ? 'LONG' : 'SHORT';
|
|
210
|
+
const orderSide = position.quantity > 0 ? 0 : 1;
|
|
211
|
+
const displaySymbol = position.symbol || this.symbol.name;
|
|
212
|
+
const size = Math.abs(position.quantity);
|
|
213
|
+
const entry = position.averagePrice || 0;
|
|
214
|
+
|
|
215
|
+
algoLogger.positionOpened(this.ui, displaySymbol, side, size, entry);
|
|
216
|
+
|
|
217
|
+
// Feed to AI supervisor
|
|
218
|
+
if (this.stats.aiSupervision) {
|
|
219
|
+
StrategySupervisor.feedSignal({
|
|
220
|
+
direction: side.toLowerCase(),
|
|
221
|
+
entry,
|
|
222
|
+
stopLoss: null,
|
|
223
|
+
takeProfit: null,
|
|
224
|
+
confidence: 0.5
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Execute on all followers in parallel
|
|
229
|
+
const promises = this.followers.map((follower, idx) => {
|
|
230
|
+
return this.queueOrder(idx, async () => {
|
|
231
|
+
const resolved = this.resolveSymbol(position, follower);
|
|
232
|
+
const orderData = this.buildOrderData({
|
|
233
|
+
accountId: follower.account.accountId,
|
|
234
|
+
contractId: resolved.contractId,
|
|
235
|
+
symbol: resolved.symbol,
|
|
236
|
+
exchange: resolved.exchange,
|
|
237
|
+
side: orderSide,
|
|
238
|
+
size: follower.contracts,
|
|
239
|
+
type: 2 // Market
|
|
240
|
+
}, follower.type);
|
|
241
|
+
|
|
242
|
+
algoLogger.info(this.ui, 'COPY ORDER', `${side} ${follower.contracts}x -> ${follower.propfirm}`);
|
|
243
|
+
|
|
244
|
+
const result = await this.executeOrderWithRetry(follower, orderData);
|
|
245
|
+
|
|
246
|
+
if (result.success) {
|
|
247
|
+
algoLogger.orderFilled(this.ui, displaySymbol, side, follower.contracts, entry);
|
|
248
|
+
|
|
249
|
+
// Track follower position
|
|
250
|
+
const posKey = this.getPositionKey(position);
|
|
251
|
+
this.followerPositions.set(`${idx}:${posKey}`, {
|
|
252
|
+
...position,
|
|
253
|
+
followerIdx: idx,
|
|
254
|
+
openTime: Date.now()
|
|
255
|
+
});
|
|
256
|
+
} else {
|
|
257
|
+
algoLogger.orderRejected(this.ui, displaySymbol, result.error);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const results = await Promise.all(promises);
|
|
265
|
+
const successCount = results.filter(r => r.success).length;
|
|
266
|
+
|
|
267
|
+
if (successCount === this.followers.length) {
|
|
268
|
+
algoLogger.info(this.ui, 'ALL COPIED', `${successCount}/${this.followers.length} followers`);
|
|
269
|
+
} else if (successCount > 0) {
|
|
270
|
+
algoLogger.info(this.ui, 'PARTIAL COPY', `${successCount}/${this.followers.length} followers`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return results;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Copy position close to all followers (parallel execution)
|
|
278
|
+
*/
|
|
279
|
+
async copyPositionClose(position, exitPrice, pnl) {
|
|
280
|
+
const side = position.quantity > 0 ? 'LONG' : 'SHORT';
|
|
281
|
+
const closeSide = position.quantity > 0 ? 1 : 0;
|
|
282
|
+
const displaySymbol = position.symbol || this.symbol.name;
|
|
283
|
+
const size = Math.abs(position.quantity);
|
|
284
|
+
|
|
285
|
+
algoLogger.positionClosed(this.ui, displaySymbol, side, size, exitPrice, pnl);
|
|
286
|
+
|
|
287
|
+
// Feed to AI supervisor
|
|
288
|
+
if (this.stats.aiSupervision) {
|
|
289
|
+
StrategySupervisor.feedTradeResult({
|
|
290
|
+
side,
|
|
291
|
+
qty: size,
|
|
292
|
+
price: exitPrice,
|
|
293
|
+
pnl,
|
|
294
|
+
symbol: displaySymbol,
|
|
295
|
+
direction: side
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const aiStatus = StrategySupervisor.getStatus();
|
|
299
|
+
if (aiStatus.patternsLearned.winning + aiStatus.patternsLearned.losing > 0) {
|
|
300
|
+
algoLogger.info(this.ui, 'AI LEARNING',
|
|
301
|
+
`${aiStatus.patternsLearned.winning}W/${aiStatus.patternsLearned.losing}L patterns`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Close on all followers in parallel
|
|
306
|
+
const posKey = this.getPositionKey(position);
|
|
307
|
+
|
|
308
|
+
const promises = this.followers.map((follower, idx) => {
|
|
309
|
+
return this.queueOrder(idx, async () => {
|
|
310
|
+
const resolved = this.resolveSymbol(position, follower);
|
|
311
|
+
const posIdentifier = follower.type === 'rithmic'
|
|
312
|
+
? (position.symbol || this.symbol.name)
|
|
313
|
+
: (position.contractId || this.symbol.id);
|
|
314
|
+
|
|
315
|
+
algoLogger.info(this.ui, 'CLOSE ORDER', `${displaySymbol} -> ${follower.propfirm}`);
|
|
316
|
+
|
|
317
|
+
// Try closePosition first
|
|
318
|
+
let result = await follower.service.closePosition(
|
|
319
|
+
follower.account.accountId,
|
|
320
|
+
posIdentifier
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
// Fallback: market order
|
|
325
|
+
const orderData = this.buildOrderData({
|
|
326
|
+
accountId: follower.account.accountId,
|
|
327
|
+
contractId: resolved.contractId,
|
|
328
|
+
symbol: resolved.symbol,
|
|
329
|
+
exchange: resolved.exchange,
|
|
330
|
+
side: closeSide,
|
|
331
|
+
size: follower.contracts,
|
|
332
|
+
type: 2
|
|
333
|
+
}, follower.type);
|
|
334
|
+
|
|
335
|
+
result = await this.executeOrderWithRetry(follower, orderData);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (result.success) {
|
|
339
|
+
algoLogger.info(this.ui, 'CLOSED', `${displaySymbol} on ${follower.propfirm}`);
|
|
340
|
+
this.followerPositions.delete(`${idx}:${posKey}`);
|
|
341
|
+
} else {
|
|
342
|
+
algoLogger.error(this.ui, 'CLOSE FAILED', `${follower.propfirm}: ${result.error}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return result;
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const results = await Promise.all(promises);
|
|
350
|
+
const successCount = results.filter(r => r.success).length;
|
|
351
|
+
|
|
352
|
+
if (successCount === this.followers.length) {
|
|
353
|
+
this.stats.trades++;
|
|
354
|
+
if (pnl >= 0) this.stats.wins++;
|
|
355
|
+
else this.stats.losses++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return results;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Poll lead positions and detect changes
|
|
363
|
+
*/
|
|
364
|
+
async pollLeadPositions() {
|
|
365
|
+
if (!this.running) return;
|
|
366
|
+
|
|
367
|
+
const startTime = Date.now();
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const result = await this.lead.service.getPositions(this.lead.account.accountId);
|
|
371
|
+
if (!result.success) return;
|
|
372
|
+
|
|
373
|
+
const currentPositions = result.positions || [];
|
|
374
|
+
const currentMap = new Map();
|
|
375
|
+
|
|
376
|
+
// Build current positions map
|
|
377
|
+
for (const pos of currentPositions) {
|
|
378
|
+
if (pos.quantity === 0) continue;
|
|
379
|
+
const key = this.getPositionKey(pos);
|
|
380
|
+
currentMap.set(key, pos);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Detect new positions (opened)
|
|
384
|
+
for (const [key, pos] of currentMap) {
|
|
385
|
+
if (!this.leadPositions.has(key)) {
|
|
386
|
+
// New position - copy to followers
|
|
387
|
+
await this.copyPositionOpen(pos);
|
|
388
|
+
this.leadPositions.set(key, pos);
|
|
389
|
+
} else {
|
|
390
|
+
// Position exists - check for size change (scaling)
|
|
391
|
+
const oldPos = this.leadPositions.get(key);
|
|
392
|
+
if (Math.abs(pos.quantity) !== Math.abs(oldPos.quantity)) {
|
|
393
|
+
// Size changed - update tracked position (scaling in/out)
|
|
394
|
+
this.leadPositions.set(key, pos);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Detect closed positions
|
|
400
|
+
for (const [key, oldPos] of this.leadPositions) {
|
|
401
|
+
if (!currentMap.has(key)) {
|
|
402
|
+
// Position closed - close on followers
|
|
403
|
+
const exitPrice = oldPos.averagePrice || 0;
|
|
404
|
+
const pnl = oldPos.profitAndLoss || 0;
|
|
405
|
+
await this.copyPositionClose(oldPos, exitPrice, pnl);
|
|
406
|
+
this.leadPositions.delete(key);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Update P&L from current positions
|
|
411
|
+
const totalPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
|
|
412
|
+
this.stats.pnl = totalPnL;
|
|
413
|
+
|
|
414
|
+
// Check limits
|
|
415
|
+
if (totalPnL >= this.dailyTarget) {
|
|
416
|
+
this.stop('target');
|
|
417
|
+
algoLogger.info(this.ui, 'TARGET REACHED', `+$${totalPnL.toFixed(2)}`);
|
|
418
|
+
} else if (totalPnL <= -this.maxRisk) {
|
|
419
|
+
this.stop('risk');
|
|
420
|
+
algoLogger.error(this.ui, 'MAX RISK HIT', `-$${Math.abs(totalPnL).toFixed(2)}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Adaptive polling - faster when positions are open
|
|
424
|
+
const pollTime = Date.now() - startTime;
|
|
425
|
+
this.stats.latency = pollTime;
|
|
426
|
+
|
|
427
|
+
if (this.leadPositions.size > 0) {
|
|
428
|
+
this.pollInterval = Math.max(100, Math.min(250, pollTime * 2));
|
|
429
|
+
} else {
|
|
430
|
+
this.pollInterval = Math.max(250, Math.min(500, pollTime * 3));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.pollCount++;
|
|
434
|
+
|
|
435
|
+
} catch (err) {
|
|
436
|
+
log.warn('Poll error', { error: err.message });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Reconcile follower positions with lead
|
|
442
|
+
*/
|
|
443
|
+
async reconcilePositions() {
|
|
444
|
+
// Get all follower positions and compare with lead
|
|
445
|
+
for (let idx = 0; idx < this.followers.length; idx++) {
|
|
446
|
+
const follower = this.followers[idx];
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const result = await follower.service.getPositions(follower.account.accountId);
|
|
450
|
+
if (!result.success) continue;
|
|
451
|
+
|
|
452
|
+
const followerPositions = result.positions || [];
|
|
453
|
+
|
|
454
|
+
// Check each lead position has corresponding follower position
|
|
455
|
+
for (const [key, leadPos] of this.leadPositions) {
|
|
456
|
+
const hasFollowerPos = followerPositions.some(fp => {
|
|
457
|
+
const fpKey = this.getPositionKey(fp);
|
|
458
|
+
return fpKey === key && fp.quantity !== 0;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (!hasFollowerPos) {
|
|
462
|
+
// Missing position on follower - need to open
|
|
463
|
+
algoLogger.info(this.ui, 'RECONCILE', `Missing ${key} on ${follower.propfirm}`);
|
|
464
|
+
await this.copyPositionOpen(leadPos);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check for orphaned follower positions (position on follower but not on lead)
|
|
469
|
+
for (const fp of followerPositions) {
|
|
470
|
+
if (fp.quantity === 0) continue;
|
|
471
|
+
const fpKey = this.getPositionKey(fp);
|
|
472
|
+
|
|
473
|
+
if (!this.leadPositions.has(fpKey)) {
|
|
474
|
+
// Orphaned position - close it
|
|
475
|
+
algoLogger.info(this.ui, 'RECONCILE', `Orphaned ${fpKey} on ${follower.propfirm}`);
|
|
476
|
+
|
|
477
|
+
const posIdentifier = follower.type === 'rithmic' ? fp.symbol : fp.contractId;
|
|
478
|
+
await follower.service.closePosition(follower.account.accountId, posIdentifier);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
} catch (err) {
|
|
483
|
+
log.warn('Reconcile error', { follower: follower.propfirm, error: err.message });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Start the copy engine
|
|
490
|
+
*/
|
|
491
|
+
async start() {
|
|
492
|
+
this.running = true;
|
|
493
|
+
this.stats.connected = true;
|
|
494
|
+
|
|
495
|
+
algoLogger.info(this.ui, 'ENGINE STARTED', `Polling every ${this.pollInterval}ms`);
|
|
496
|
+
algoLogger.info(this.ui, 'FOLLOWERS', `${this.followers.length} account(s)`);
|
|
497
|
+
|
|
498
|
+
// Initial reconciliation
|
|
499
|
+
await this.reconcilePositions();
|
|
500
|
+
|
|
501
|
+
// Main polling loop
|
|
502
|
+
while (this.running) {
|
|
503
|
+
await this.pollLeadPositions();
|
|
504
|
+
await this.sleep(this.pollInterval);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return this.stopReason;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Stop the copy engine
|
|
512
|
+
*/
|
|
513
|
+
stop(reason = 'manual') {
|
|
514
|
+
this.running = false;
|
|
515
|
+
this.stopReason = reason;
|
|
516
|
+
this.stats.connected = false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Sleep utility
|
|
521
|
+
*/
|
|
522
|
+
sleep(ms) {
|
|
523
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// COPY TRADING MENU
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
18
531
|
/**
|
|
19
532
|
* Copy Trading Menu
|
|
20
533
|
*/
|
|
@@ -34,17 +547,17 @@ const copyTradingMenu = async () => {
|
|
|
34
547
|
|
|
35
548
|
const allConns = connections.getAll();
|
|
36
549
|
|
|
37
|
-
if (allConns.length
|
|
550
|
+
if (allConns.length === 0) {
|
|
38
551
|
console.log();
|
|
39
|
-
console.log(chalk.yellow(
|
|
40
|
-
console.log(chalk.gray(' Connect to
|
|
552
|
+
console.log(chalk.yellow(' No connections found'));
|
|
553
|
+
console.log(chalk.gray(' Connect to a PropFirm first'));
|
|
41
554
|
console.log();
|
|
42
555
|
await prompts.waitForEnter();
|
|
43
556
|
return;
|
|
44
557
|
}
|
|
45
558
|
|
|
46
559
|
console.log();
|
|
47
|
-
console.log(chalk.yellow.bold('
|
|
560
|
+
console.log(chalk.yellow.bold(' COPY TRADING - Professional Mode'));
|
|
48
561
|
console.log();
|
|
49
562
|
|
|
50
563
|
// Fetch all accounts
|
|
@@ -53,61 +566,105 @@ const copyTradingMenu = async () => {
|
|
|
53
566
|
|
|
54
567
|
if (allAccounts.length < 2) {
|
|
55
568
|
spinner.fail('NEED AT LEAST 2 ACTIVE ACCOUNTS');
|
|
569
|
+
console.log(chalk.gray(' Copy Trading requires a lead + at least one follower'));
|
|
56
570
|
await prompts.waitForEnter();
|
|
57
571
|
return;
|
|
58
572
|
}
|
|
59
573
|
|
|
60
|
-
spinner.succeed(`Found ${allAccounts.length}
|
|
574
|
+
spinner.succeed(`Found ${allAccounts.length} accounts across ${allConns.length} connection(s)`);
|
|
61
575
|
|
|
62
576
|
// Step 1: Select Lead Account
|
|
63
|
-
console.log(
|
|
64
|
-
|
|
577
|
+
console.log();
|
|
578
|
+
console.log(chalk.cyan.bold(' Step 1: Select LEAD Account (source)'));
|
|
579
|
+
const leadIdx = await selectAccount('LEAD ACCOUNT:', allAccounts, []);
|
|
65
580
|
if (leadIdx === null || leadIdx === -1) return;
|
|
66
581
|
const lead = allAccounts[leadIdx];
|
|
67
582
|
|
|
68
|
-
// Step 2: Select Follower
|
|
583
|
+
// Step 2: Select Follower Accounts (multiple)
|
|
69
584
|
console.log();
|
|
70
|
-
console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
585
|
+
console.log(chalk.cyan.bold(' Step 2: Select FOLLOWER Account(s)'));
|
|
586
|
+
console.log(chalk.gray(' Select accounts to copy trades to'));
|
|
587
|
+
|
|
588
|
+
const followers = [];
|
|
589
|
+
let selectingFollowers = true;
|
|
590
|
+
const excludeIndices = [leadIdx];
|
|
591
|
+
|
|
592
|
+
while (selectingFollowers && excludeIndices.length < allAccounts.length) {
|
|
593
|
+
const followerIdx = await selectAccount(
|
|
594
|
+
followers.length === 0 ? 'FOLLOWER ACCOUNT:' : 'ADD ANOTHER FOLLOWER:',
|
|
595
|
+
allAccounts,
|
|
596
|
+
excludeIndices,
|
|
597
|
+
followers.length > 0 // Allow skip if at least one follower
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
if (followerIdx === null || followerIdx === -1) {
|
|
601
|
+
if (followers.length === 0) return; // Cancel
|
|
602
|
+
selectingFollowers = false;
|
|
603
|
+
} else if (followerIdx === -2) {
|
|
604
|
+
selectingFollowers = false; // Done adding
|
|
605
|
+
} else {
|
|
606
|
+
followers.push(allAccounts[followerIdx]);
|
|
607
|
+
excludeIndices.push(followerIdx);
|
|
608
|
+
console.log(chalk.green(` Added: ${allAccounts[followerIdx].propfirm}`));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (followers.length === 0) {
|
|
613
|
+
console.log(chalk.red(' No followers selected'));
|
|
614
|
+
await prompts.waitForEnter();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
74
617
|
|
|
75
618
|
// Step 3: Select Symbol
|
|
76
619
|
console.log();
|
|
77
|
-
console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
|
|
620
|
+
console.log(chalk.cyan.bold(' Step 3: Select Trading Symbol'));
|
|
78
621
|
const symbol = await selectSymbol(lead.service);
|
|
79
622
|
if (!symbol) return;
|
|
80
623
|
|
|
81
|
-
// Step 4: Configure
|
|
624
|
+
// Step 4: Configure contracts for each account
|
|
82
625
|
console.log();
|
|
83
|
-
console.log(chalk.cyan(' Step 4: Configure
|
|
84
|
-
|
|
85
|
-
const leadContracts = await prompts.numberInput(
|
|
626
|
+
console.log(chalk.cyan.bold(' Step 4: Configure Contract Sizes'));
|
|
627
|
+
|
|
628
|
+
const leadContracts = await prompts.numberInput(`${lead.propfirm} (LEAD) contracts:`, 1, 1, 10);
|
|
86
629
|
if (leadContracts === null) return;
|
|
630
|
+
lead.contracts = leadContracts;
|
|
631
|
+
|
|
632
|
+
for (const follower of followers) {
|
|
633
|
+
const contracts = await prompts.numberInput(`${follower.propfirm} contracts:`, leadContracts, 1, 10);
|
|
634
|
+
if (contracts === null) return;
|
|
635
|
+
follower.contracts = contracts;
|
|
636
|
+
}
|
|
87
637
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
638
|
+
// Step 5: Risk parameters
|
|
639
|
+
console.log();
|
|
640
|
+
console.log(chalk.cyan.bold(' Step 5: Risk Parameters'));
|
|
641
|
+
|
|
642
|
+
const dailyTarget = await prompts.numberInput('Daily target ($):', 500, 1, 10000);
|
|
92
643
|
if (dailyTarget === null) return;
|
|
93
644
|
|
|
94
|
-
const maxRisk = await prompts.numberInput('Max risk ($):',
|
|
645
|
+
const maxRisk = await prompts.numberInput('Max risk ($):', 250, 1, 5000);
|
|
95
646
|
if (maxRisk === null) return;
|
|
96
647
|
|
|
97
|
-
// Step
|
|
648
|
+
// Step 6: Privacy
|
|
98
649
|
const showNames = await prompts.selectOption('ACCOUNT NAMES:', [
|
|
99
650
|
{ label: 'HIDE ACCOUNT NAMES', value: false },
|
|
100
651
|
{ label: 'SHOW ACCOUNT NAMES', value: true },
|
|
101
652
|
]);
|
|
102
653
|
if (showNames === null) return;
|
|
103
654
|
|
|
104
|
-
//
|
|
655
|
+
// Summary
|
|
105
656
|
console.log();
|
|
106
|
-
console.log(chalk.white('
|
|
657
|
+
console.log(chalk.white.bold(' ═══════════════════════════════════════'));
|
|
658
|
+
console.log(chalk.white.bold(' COPY TRADING CONFIGURATION'));
|
|
659
|
+
console.log(chalk.white.bold(' ═══════════════════════════════════════'));
|
|
107
660
|
console.log(chalk.cyan(` Symbol: ${symbol.name}`));
|
|
108
|
-
console.log(chalk.cyan(` Lead: ${lead.propfirm}
|
|
109
|
-
console.log(chalk.cyan(`
|
|
661
|
+
console.log(chalk.cyan(` Lead: ${lead.propfirm} (${leadContracts} contracts)`));
|
|
662
|
+
console.log(chalk.cyan(` Followers: ${followers.length}`));
|
|
663
|
+
followers.forEach(f => {
|
|
664
|
+
console.log(chalk.gray(` → ${f.propfirm} (${f.contracts} contracts)`));
|
|
665
|
+
});
|
|
110
666
|
console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
|
|
667
|
+
console.log(chalk.white.bold(' ═══════════════════════════════════════'));
|
|
111
668
|
console.log();
|
|
112
669
|
|
|
113
670
|
const confirm = await prompts.confirmPrompt('START COPY TRADING?', true);
|
|
@@ -116,7 +673,7 @@ const copyTradingMenu = async () => {
|
|
|
116
673
|
// Launch
|
|
117
674
|
await launchCopyTrading({
|
|
118
675
|
lead: { ...lead, symbol, contracts: leadContracts },
|
|
119
|
-
|
|
676
|
+
followers: followers.map(f => ({ ...f, symbol })),
|
|
120
677
|
dailyTarget,
|
|
121
678
|
maxRisk,
|
|
122
679
|
showNames,
|
|
@@ -125,71 +682,72 @@ const copyTradingMenu = async () => {
|
|
|
125
682
|
|
|
126
683
|
/**
|
|
127
684
|
* Fetch all active accounts from connections
|
|
128
|
-
* @param {Array} allConns - All connections
|
|
129
|
-
* @returns {Promise<Array>}
|
|
130
685
|
*/
|
|
131
686
|
const fetchAllAccounts = async (allConns) => {
|
|
132
687
|
const allAccounts = [];
|
|
133
688
|
|
|
134
|
-
|
|
689
|
+
// Fetch in parallel
|
|
690
|
+
const promises = allConns.map(async (conn) => {
|
|
135
691
|
try {
|
|
136
692
|
const result = await conn.service.getTradingAccounts();
|
|
137
693
|
if (result.success && result.accounts) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
694
|
+
return result.accounts
|
|
695
|
+
.filter(a => a.status === 0)
|
|
696
|
+
.map(acc => ({
|
|
141
697
|
account: acc,
|
|
142
698
|
service: conn.service,
|
|
143
699
|
propfirm: conn.propfirm,
|
|
144
700
|
type: conn.type,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
701
|
+
}));
|
|
147
702
|
}
|
|
148
703
|
} catch (err) {
|
|
149
704
|
log.warn('Failed to get accounts', { type: conn.type, error: err.message });
|
|
150
705
|
}
|
|
151
|
-
|
|
706
|
+
return [];
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const results = await Promise.all(promises);
|
|
710
|
+
results.forEach(accounts => allAccounts.push(...accounts));
|
|
152
711
|
|
|
153
712
|
return allAccounts;
|
|
154
713
|
};
|
|
155
714
|
|
|
156
715
|
/**
|
|
157
716
|
* Select account from list
|
|
158
|
-
* @param {string} message - Prompt message
|
|
159
|
-
* @param {Array} accounts - Available accounts
|
|
160
|
-
* @param {number} excludeIdx - Index to exclude
|
|
161
|
-
* @returns {Promise<number|null>}
|
|
162
717
|
*/
|
|
163
|
-
const selectAccount = async (message, accounts,
|
|
718
|
+
const selectAccount = async (message, accounts, excludeIndices = [], allowDone = false) => {
|
|
164
719
|
const options = accounts
|
|
165
720
|
.map((a, i) => ({ a, i }))
|
|
166
|
-
.filter(x => x.i
|
|
721
|
+
.filter(x => !excludeIndices.includes(x.i))
|
|
167
722
|
.map(x => {
|
|
168
723
|
const acc = x.a.account;
|
|
169
|
-
const balance = acc.balance !== null
|
|
724
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
725
|
+
? ` ($${acc.balance.toLocaleString()})`
|
|
726
|
+
: '';
|
|
727
|
+
const platform = x.a.type === 'rithmic' ? ' [Rithmic]' : '';
|
|
170
728
|
return {
|
|
171
|
-
label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.
|
|
729
|
+
label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.accountId}${balance}${platform}`,
|
|
172
730
|
value: x.i,
|
|
173
731
|
};
|
|
174
732
|
});
|
|
175
733
|
|
|
176
|
-
|
|
734
|
+
if (allowDone) {
|
|
735
|
+
options.push({ label: chalk.green('✓ DONE ADDING FOLLOWERS'), value: -2 });
|
|
736
|
+
}
|
|
737
|
+
options.push({ label: chalk.gray('< CANCEL'), value: -1 });
|
|
738
|
+
|
|
177
739
|
return prompts.selectOption(message, options);
|
|
178
740
|
};
|
|
179
741
|
|
|
180
742
|
/**
|
|
181
743
|
* Select trading symbol
|
|
182
|
-
* @param {Object} service - Service instance
|
|
183
|
-
* @returns {Promise<Object|null>}
|
|
184
744
|
*/
|
|
185
745
|
const selectSymbol = async (service) => {
|
|
186
746
|
const spinner = ora({ text: 'LOADING SYMBOLS...', color: 'yellow' }).start();
|
|
187
747
|
|
|
188
748
|
try {
|
|
189
|
-
// Try ProjectX API first for consistency
|
|
190
749
|
let contracts = await getContractsFromAPI();
|
|
191
750
|
|
|
192
|
-
// Fallback to service
|
|
193
751
|
if (!contracts && typeof service.getContracts === 'function') {
|
|
194
752
|
const result = await service.getContracts();
|
|
195
753
|
if (result.success && result.contracts?.length > 0) {
|
|
@@ -205,27 +763,22 @@ const selectSymbol = async (service) => {
|
|
|
205
763
|
|
|
206
764
|
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
207
765
|
|
|
208
|
-
//
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
value: null,
|
|
219
|
-
disabled: true,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
766
|
+
// Sort by popular symbols first
|
|
767
|
+
const popular = ['ES', 'NQ', 'MES', 'MNQ', 'RTY', 'YM', 'CL', 'GC'];
|
|
768
|
+
contracts.sort((a, b) => {
|
|
769
|
+
const aIdx = popular.findIndex(p => (a.name || '').startsWith(p));
|
|
770
|
+
const bIdx = popular.findIndex(p => (b.name || '').startsWith(p));
|
|
771
|
+
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
|
|
772
|
+
if (aIdx !== -1) return -1;
|
|
773
|
+
if (bIdx !== -1) return 1;
|
|
774
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
775
|
+
});
|
|
222
776
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
777
|
+
const options = contracts.slice(0, 30).map(c => ({
|
|
778
|
+
label: `${c.name} - ${c.description || ''} (${c.exchange || 'CME'})`,
|
|
779
|
+
value: c
|
|
780
|
+
}));
|
|
227
781
|
|
|
228
|
-
options.push({ label: '', value: null, disabled: true });
|
|
229
782
|
options.push({ label: chalk.gray('< CANCEL'), value: null });
|
|
230
783
|
|
|
231
784
|
return prompts.selectOption('TRADING SYMBOL:', options);
|
|
@@ -237,8 +790,7 @@ const selectSymbol = async (service) => {
|
|
|
237
790
|
};
|
|
238
791
|
|
|
239
792
|
/**
|
|
240
|
-
* Get contracts from ProjectX API
|
|
241
|
-
* @returns {Promise<Array|null>}
|
|
793
|
+
* Get contracts from ProjectX API
|
|
242
794
|
*/
|
|
243
795
|
const getContractsFromAPI = async () => {
|
|
244
796
|
const allConns = connections.getAll();
|
|
@@ -247,7 +799,6 @@ const getContractsFromAPI = async () => {
|
|
|
247
799
|
if (projectxConn && typeof projectxConn.service.getContracts === 'function') {
|
|
248
800
|
const result = await projectxConn.service.getContracts();
|
|
249
801
|
if (result.success && result.contracts?.length > 0) {
|
|
250
|
-
// Return RAW API data - no mapping
|
|
251
802
|
return result.contracts;
|
|
252
803
|
}
|
|
253
804
|
}
|
|
@@ -257,24 +808,21 @@ const getContractsFromAPI = async () => {
|
|
|
257
808
|
|
|
258
809
|
/**
|
|
259
810
|
* Launch Copy Trading session
|
|
260
|
-
* @param {Object} config - Session configuration
|
|
261
811
|
*/
|
|
262
812
|
const launchCopyTrading = async (config) => {
|
|
263
|
-
const { lead,
|
|
264
|
-
|
|
265
|
-
// Account names (masked for privacy)
|
|
266
|
-
const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
|
|
267
|
-
const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
|
|
813
|
+
const { lead, followers, dailyTarget, maxRisk, showNames } = config;
|
|
268
814
|
|
|
269
|
-
const
|
|
815
|
+
const leadName = showNames
|
|
816
|
+
? (lead.account.accountName || lead.account.accountId)
|
|
817
|
+
: 'Lead *****';
|
|
818
|
+
|
|
819
|
+
const ui = new AlgoUI({ subtitle: 'COPY TRADING PRO', mode: 'copy-trading' });
|
|
270
820
|
|
|
271
821
|
const stats = {
|
|
272
822
|
leadName,
|
|
273
|
-
|
|
823
|
+
followerCount: followers.length,
|
|
274
824
|
leadSymbol: lead.symbol.name,
|
|
275
|
-
followerSymbol: follower.symbol.name,
|
|
276
825
|
leadQty: lead.contracts,
|
|
277
|
-
followerQty: follower.contracts,
|
|
278
826
|
target: dailyTarget,
|
|
279
827
|
risk: maxRisk,
|
|
280
828
|
pnl: 0,
|
|
@@ -283,205 +831,96 @@ const launchCopyTrading = async (config) => {
|
|
|
283
831
|
losses: 0,
|
|
284
832
|
latency: 0,
|
|
285
833
|
connected: false,
|
|
286
|
-
platform: lead.
|
|
834
|
+
platform: lead.type === 'rithmic' ? 'Rithmic' : 'ProjectX',
|
|
835
|
+
startTime: Date.now(),
|
|
836
|
+
aiSupervision: false,
|
|
837
|
+
aiMode: null
|
|
287
838
|
};
|
|
288
839
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
await lead.service.getPositions(lead.account.accountId);
|
|
297
|
-
stats.latency = Date.now() - start;
|
|
298
|
-
} catch (e) {
|
|
299
|
-
stats.latency = 0;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
840
|
+
// Initialize AI Supervisor
|
|
841
|
+
const aiAgents = aiService.getAgents();
|
|
842
|
+
if (aiAgents.length > 0) {
|
|
843
|
+
const supervisorResult = StrategySupervisor.initialize(null, aiAgents, lead.service, lead.account.accountId);
|
|
844
|
+
stats.aiSupervision = supervisorResult.success;
|
|
845
|
+
stats.aiMode = supervisorResult.mode;
|
|
846
|
+
}
|
|
302
847
|
|
|
303
|
-
//
|
|
848
|
+
// Startup logs
|
|
304
849
|
const market = checkMarketHours();
|
|
305
850
|
const sessionName = market.session || 'AMERICAN';
|
|
306
|
-
const etTime = new Date().toLocaleTimeString('en-US', {
|
|
307
|
-
|
|
851
|
+
const etTime = new Date().toLocaleTimeString('en-US', {
|
|
852
|
+
hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York'
|
|
853
|
+
});
|
|
854
|
+
|
|
308
855
|
algoLogger.connectingToEngine(ui, lead.account.accountId);
|
|
309
856
|
algoLogger.engineStarting(ui, stats.platform, dailyTarget, maxRisk);
|
|
310
857
|
algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
|
|
311
|
-
algoLogger.info(ui, 'COPY MODE', `Lead: ${lead.propfirm} ->
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// Detect new positions on lead
|
|
328
|
-
for (const pos of currentPositions) {
|
|
329
|
-
const existing = lastLeadPositions.find(p => p.contractId === pos.contractId);
|
|
330
|
-
if (!existing && pos.quantity !== 0) {
|
|
331
|
-
// New position opened - copy to follower
|
|
332
|
-
const side = pos.quantity > 0 ? 'LONG' : 'SHORT';
|
|
333
|
-
const orderSide = pos.quantity > 0 ? 0 : 1; // 0=Buy, 1=Sell
|
|
334
|
-
const symbol = pos.symbol || pos.contractId;
|
|
335
|
-
const size = Math.abs(pos.quantity);
|
|
336
|
-
const entry = pos.averagePrice || 0;
|
|
337
|
-
|
|
338
|
-
algoLogger.positionOpened(ui, symbol, side, size, entry);
|
|
339
|
-
algoLogger.info(ui, 'COPYING TO FOLLOWER', `${side} ${size}x ${symbol}`);
|
|
340
|
-
|
|
341
|
-
// Place order on follower account
|
|
342
|
-
try {
|
|
343
|
-
const orderResult = await follower.service.placeOrder({
|
|
344
|
-
accountId: follower.account.accountId,
|
|
345
|
-
contractId: pos.contractId,
|
|
346
|
-
type: 2, // Market order
|
|
347
|
-
side: orderSide,
|
|
348
|
-
size: follower.contracts
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
if (orderResult.success) {
|
|
352
|
-
algoLogger.orderFilled(ui, symbol, side, follower.contracts, entry);
|
|
353
|
-
algoLogger.info(ui, 'FOLLOWER ORDER', `${side} ${follower.contracts}x filled`);
|
|
354
|
-
} else {
|
|
355
|
-
algoLogger.orderRejected(ui, symbol, orderResult.error || 'Order failed');
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
algoLogger.error(ui, 'FOLLOWER ORDER FAILED', err.message);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Detect closed positions
|
|
364
|
-
for (const oldPos of lastLeadPositions) {
|
|
365
|
-
const stillOpen = currentPositions.find(p => p.contractId === oldPos.contractId);
|
|
366
|
-
if (!stillOpen || stillOpen.quantity === 0) {
|
|
367
|
-
const side = oldPos.quantity > 0 ? 'LONG' : 'SHORT';
|
|
368
|
-
const closeSide = oldPos.quantity > 0 ? 1 : 0; // Opposite side to close
|
|
369
|
-
const symbol = oldPos.symbol || oldPos.contractId;
|
|
370
|
-
const size = Math.abs(oldPos.quantity);
|
|
371
|
-
const exit = stillOpen?.averagePrice || oldPos.averagePrice || 0;
|
|
372
|
-
const pnl = oldPos.profitAndLoss || 0;
|
|
373
|
-
|
|
374
|
-
algoLogger.positionClosed(ui, symbol, side, size, exit, pnl);
|
|
375
|
-
algoLogger.info(ui, 'CLOSING ON FOLLOWER', symbol);
|
|
376
|
-
|
|
377
|
-
// Close position on follower account
|
|
378
|
-
try {
|
|
379
|
-
// First try closePosition API
|
|
380
|
-
const closeResult = await follower.service.closePosition(
|
|
381
|
-
follower.account.accountId,
|
|
382
|
-
oldPos.contractId
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
if (closeResult.success) {
|
|
386
|
-
algoLogger.info(ui, 'FOLLOWER CLOSED', `${symbol} position closed`);
|
|
387
|
-
} else {
|
|
388
|
-
// Fallback: place market order to close
|
|
389
|
-
const orderResult = await follower.service.placeOrder({
|
|
390
|
-
accountId: follower.account.accountId,
|
|
391
|
-
contractId: oldPos.contractId,
|
|
392
|
-
type: 2, // Market order
|
|
393
|
-
side: closeSide,
|
|
394
|
-
size: follower.contracts
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
if (orderResult.success) {
|
|
398
|
-
algoLogger.info(ui, 'FOLLOWER CLOSED', `${symbol} via market order`);
|
|
399
|
-
} else {
|
|
400
|
-
algoLogger.error(ui, 'FOLLOWER CLOSE FAILED', orderResult.error || 'Close failed');
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch (err) {
|
|
404
|
-
algoLogger.error(ui, 'FOLLOWER CLOSE ERROR', err.message);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
lastLeadPositions = currentPositions;
|
|
410
|
-
|
|
411
|
-
// Update P&L from lead
|
|
412
|
-
const leadPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
|
|
413
|
-
if (leadPnL !== stats.pnl) {
|
|
414
|
-
const diff = leadPnL - stats.pnl;
|
|
415
|
-
if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
|
|
416
|
-
stats.trades++;
|
|
417
|
-
if (diff >= 0) stats.wins++;
|
|
418
|
-
else stats.losses++;
|
|
419
|
-
}
|
|
420
|
-
stats.pnl = leadPnL;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Check target/risk limits
|
|
424
|
-
if (stats.pnl >= dailyTarget) {
|
|
425
|
-
stopReason = 'target';
|
|
426
|
-
running = false;
|
|
427
|
-
algoLogger.targetHit(ui, lead.symbol.name, 0, stats.pnl);
|
|
428
|
-
algoLogger.info(ui, 'DAILY TARGET REACHED', `+$${stats.pnl.toFixed(2)} - Stopping algo`);
|
|
429
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
430
|
-
stopReason = 'risk';
|
|
431
|
-
running = false;
|
|
432
|
-
algoLogger.dailyLimitWarning(ui, stats.pnl, -maxRisk);
|
|
433
|
-
algoLogger.error(ui, 'MAX RISK HIT', `-$${Math.abs(stats.pnl).toFixed(2)} - Stopping algo`);
|
|
434
|
-
}
|
|
435
|
-
} catch (e) {
|
|
436
|
-
// Silent fail - will retry
|
|
437
|
-
}
|
|
438
|
-
};
|
|
858
|
+
algoLogger.info(ui, 'COPY MODE', `Lead: ${lead.propfirm} -> ${followers.length} follower(s)`);
|
|
859
|
+
|
|
860
|
+
if (stats.aiSupervision) {
|
|
861
|
+
algoLogger.info(ui, 'AI SUPERVISION', `${aiAgents.length} agent(s) - LEARNING ACTIVE`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Create copy engine
|
|
865
|
+
const engine = new CopyEngine({
|
|
866
|
+
lead,
|
|
867
|
+
followers,
|
|
868
|
+
symbol: lead.symbol,
|
|
869
|
+
dailyTarget,
|
|
870
|
+
maxRisk,
|
|
871
|
+
ui,
|
|
872
|
+
stats
|
|
873
|
+
});
|
|
439
874
|
|
|
440
875
|
// UI refresh loop
|
|
441
876
|
const refreshInterval = setInterval(() => {
|
|
442
|
-
if (running) ui.render(stats);
|
|
877
|
+
if (engine.running) ui.render(stats);
|
|
443
878
|
}, 250);
|
|
444
|
-
|
|
445
|
-
// Measure API latency every 5 seconds
|
|
446
|
-
measureLatency(); // Initial measurement
|
|
447
|
-
const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
|
|
448
|
-
|
|
449
|
-
// Poll and copy every 2 seconds
|
|
450
|
-
pollAndCopy(); // Initial poll
|
|
451
|
-
const copyInterval = setInterval(() => { if (running) pollAndCopy(); }, 2000);
|
|
452
879
|
|
|
453
880
|
// Keyboard handling
|
|
454
881
|
const cleanupKeys = setupKeyboardHandler(() => {
|
|
455
|
-
|
|
456
|
-
stopReason = 'manual';
|
|
882
|
+
engine.stop('manual');
|
|
457
883
|
});
|
|
458
884
|
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
resolve();
|
|
465
|
-
}
|
|
466
|
-
}, 100);
|
|
467
|
-
});
|
|
885
|
+
// Start engine
|
|
886
|
+
algoLogger.dataConnected(ui, 'API');
|
|
887
|
+
algoLogger.algoOperational(ui, stats.platform);
|
|
888
|
+
|
|
889
|
+
const stopReason = await engine.start();
|
|
468
890
|
|
|
469
891
|
// Cleanup
|
|
470
892
|
clearInterval(refreshInterval);
|
|
471
|
-
clearInterval(latencyInterval);
|
|
472
|
-
clearInterval(copyInterval);
|
|
473
893
|
if (cleanupKeys) cleanupKeys();
|
|
894
|
+
|
|
895
|
+
// Stop AI Supervisor
|
|
896
|
+
if (stats.aiSupervision) {
|
|
897
|
+
const aiSummary = StrategySupervisor.stop();
|
|
898
|
+
stats.aiLearning = {
|
|
899
|
+
optimizations: aiSummary.optimizationsApplied || 0,
|
|
900
|
+
patternsLearned: (aiSummary.winningPatterns || 0) + (aiSummary.losingPatterns || 0)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
474
904
|
ui.cleanup();
|
|
475
905
|
|
|
476
|
-
//
|
|
906
|
+
// Duration
|
|
907
|
+
const durationMs = Date.now() - stats.startTime;
|
|
908
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
909
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
910
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
911
|
+
stats.duration = hours > 0
|
|
912
|
+
? `${hours}h ${minutes}m ${seconds}s`
|
|
913
|
+
: minutes > 0
|
|
914
|
+
? `${minutes}m ${seconds}s`
|
|
915
|
+
: `${seconds}s`;
|
|
916
|
+
|
|
917
|
+
// Summary
|
|
477
918
|
renderSessionSummary(stats, stopReason);
|
|
478
919
|
await prompts.waitForEnter();
|
|
479
920
|
};
|
|
480
921
|
|
|
481
922
|
/**
|
|
482
923
|
* Setup keyboard handler
|
|
483
|
-
* @param {Function} onStop - Stop callback
|
|
484
|
-
* @returns {Function|null} Cleanup function
|
|
485
924
|
*/
|
|
486
925
|
const setupKeyboardHandler = (onStop) => {
|
|
487
926
|
if (!process.stdin.isTTY) return null;
|