hedgequantx 2.5.42 → 2.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/pages/algo/copy-trading.js +692 -253
- package/src/services/ai/index.js +24 -2
- package/src/services/ai/strategy-supervisor.js +129 -0
package/package.json
CHANGED
|
@@ -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;
|
package/src/services/ai/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { getProviders, getProvider } = require('./providers');
|
|
7
7
|
const { storage } = require('../session');
|
|
8
8
|
const AISupervisor = require('./supervisor');
|
|
9
|
+
const StrategySupervisor = require('./strategy-supervisor');
|
|
9
10
|
|
|
10
11
|
// In-memory cache of connections
|
|
11
12
|
let connectionsCache = null;
|
|
@@ -228,10 +229,17 @@ const addAgent = async (providerId, optionId, credentials, model = null, customN
|
|
|
228
229
|
|
|
229
230
|
saveAISettings(aiSettings);
|
|
230
231
|
|
|
231
|
-
//
|
|
232
|
-
// Supervision requires a real service connection and account
|
|
232
|
+
// Get the full agent object
|
|
233
233
|
const agent = getAgent(agentId);
|
|
234
234
|
|
|
235
|
+
// Notify StrategySupervisor if algo is running
|
|
236
|
+
// This ensures new agents are immediately connected to live trading
|
|
237
|
+
try {
|
|
238
|
+
StrategySupervisor.addAgent(agent);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// Supervisor might not be active - that's OK
|
|
241
|
+
}
|
|
242
|
+
|
|
235
243
|
return agent;
|
|
236
244
|
};
|
|
237
245
|
|
|
@@ -253,6 +261,13 @@ const removeAgent = (agentId) => {
|
|
|
253
261
|
// Stop AI supervision for this agent
|
|
254
262
|
AISupervisor.stop(agentId);
|
|
255
263
|
|
|
264
|
+
// Notify StrategySupervisor to remove agent from live trading
|
|
265
|
+
try {
|
|
266
|
+
StrategySupervisor.removeAgent(agentId);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
// Supervisor might not be active - that's OK
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
// If removed agent was active, set new active
|
|
257
272
|
if (aiSettings.activeAgentId === agentId) {
|
|
258
273
|
aiSettings.activeAgentId = agents.length > 0 ? agents[0].id : null;
|
|
@@ -289,6 +304,13 @@ const disconnectAll = () => {
|
|
|
289
304
|
// Stop all AI supervision sessions
|
|
290
305
|
AISupervisor.stopAll();
|
|
291
306
|
|
|
307
|
+
// Refresh StrategySupervisor to clear agents
|
|
308
|
+
try {
|
|
309
|
+
StrategySupervisor.refreshAgents();
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// Supervisor might not be active
|
|
312
|
+
}
|
|
313
|
+
|
|
292
314
|
saveAISettings({ agents: [] });
|
|
293
315
|
};
|
|
294
316
|
|
|
@@ -609,6 +609,11 @@ const initialize = (strategy, agents, service, accountId) => {
|
|
|
609
609
|
}
|
|
610
610
|
}, 10000);
|
|
611
611
|
|
|
612
|
+
// Start agent sync interval - ensures new agents are picked up dynamically
|
|
613
|
+
if (!supervisorState.agentSyncInterval) {
|
|
614
|
+
supervisorState.agentSyncInterval = setInterval(syncAgents, 5000);
|
|
615
|
+
}
|
|
616
|
+
|
|
612
617
|
return {
|
|
613
618
|
success: true,
|
|
614
619
|
agents: agents.length,
|
|
@@ -616,6 +621,108 @@ const initialize = (strategy, agents, service, accountId) => {
|
|
|
616
621
|
};
|
|
617
622
|
};
|
|
618
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Sync agents with AI service
|
|
626
|
+
* Called periodically to pick up newly added/removed agents
|
|
627
|
+
* Ensures all agents are always connected to the supervisor
|
|
628
|
+
*/
|
|
629
|
+
const syncAgents = () => {
|
|
630
|
+
if (!supervisorState.active) return;
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
// Dynamic import to avoid circular dependency
|
|
634
|
+
const aiService = require('./index');
|
|
635
|
+
const currentAgents = aiService.getAgents();
|
|
636
|
+
|
|
637
|
+
if (!currentAgents) return;
|
|
638
|
+
|
|
639
|
+
const currentIds = new Set(currentAgents.map(a => a.id));
|
|
640
|
+
const supervisorIds = new Set(supervisorState.agents.map(a => a.id));
|
|
641
|
+
|
|
642
|
+
// Check for new agents
|
|
643
|
+
const newAgents = currentAgents.filter(a => !supervisorIds.has(a.id));
|
|
644
|
+
|
|
645
|
+
// Check for removed agents
|
|
646
|
+
const removedIds = [...supervisorIds].filter(id => !currentIds.has(id));
|
|
647
|
+
|
|
648
|
+
// Add new agents
|
|
649
|
+
if (newAgents.length > 0) {
|
|
650
|
+
supervisorState.agents = [...supervisorState.agents, ...newAgents];
|
|
651
|
+
// Log would go here if we had UI access
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Remove agents that were disconnected
|
|
655
|
+
if (removedIds.length > 0) {
|
|
656
|
+
supervisorState.agents = supervisorState.agents.filter(a => !removedIds.includes(a.id));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Update mode based on current agent count
|
|
660
|
+
supervisorState.mode = supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL';
|
|
661
|
+
|
|
662
|
+
} catch (e) {
|
|
663
|
+
// Silent fail - aiService might not be ready
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Force refresh agents from AI service
|
|
669
|
+
* Call this when you know agents have changed
|
|
670
|
+
*/
|
|
671
|
+
const refreshAgents = () => {
|
|
672
|
+
syncAgents();
|
|
673
|
+
return {
|
|
674
|
+
agents: supervisorState.agents.length,
|
|
675
|
+
mode: supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL'
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Add a single agent to the supervisor (called when agent is added)
|
|
681
|
+
*/
|
|
682
|
+
const addAgent = (agent) => {
|
|
683
|
+
if (!supervisorState.active) return false;
|
|
684
|
+
|
|
685
|
+
// Check if already exists
|
|
686
|
+
if (supervisorState.agents.some(a => a.id === agent.id)) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
supervisorState.agents.push(agent);
|
|
691
|
+
supervisorState.mode = supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL';
|
|
692
|
+
|
|
693
|
+
return true;
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Remove an agent from the supervisor (called when agent is removed)
|
|
698
|
+
*/
|
|
699
|
+
const removeAgent = (agentId) => {
|
|
700
|
+
if (!supervisorState.active) return false;
|
|
701
|
+
|
|
702
|
+
const index = supervisorState.agents.findIndex(a => a.id === agentId);
|
|
703
|
+
if (index === -1) return false;
|
|
704
|
+
|
|
705
|
+
supervisorState.agents.splice(index, 1);
|
|
706
|
+
supervisorState.mode = supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL';
|
|
707
|
+
|
|
708
|
+
return true;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get current agent count and mode
|
|
713
|
+
*/
|
|
714
|
+
const getAgentInfo = () => {
|
|
715
|
+
return {
|
|
716
|
+
count: supervisorState.agents.length,
|
|
717
|
+
mode: supervisorState.agents.length >= 2 ? 'CONSENSUS' : 'INDIVIDUAL',
|
|
718
|
+
agents: supervisorState.agents.map(a => ({
|
|
719
|
+
id: a.id,
|
|
720
|
+
name: a.name,
|
|
721
|
+
provider: a.providerId
|
|
722
|
+
}))
|
|
723
|
+
};
|
|
724
|
+
};
|
|
725
|
+
|
|
619
726
|
/**
|
|
620
727
|
* Stop supervisor and save learned data
|
|
621
728
|
*/
|
|
@@ -625,6 +732,12 @@ const stop = () => {
|
|
|
625
732
|
analysisInterval = null;
|
|
626
733
|
}
|
|
627
734
|
|
|
735
|
+
// Stop agent sync
|
|
736
|
+
if (supervisorState.agentSyncInterval) {
|
|
737
|
+
clearInterval(supervisorState.agentSyncInterval);
|
|
738
|
+
supervisorState.agentSyncInterval = null;
|
|
739
|
+
}
|
|
740
|
+
|
|
628
741
|
// Save all learned data before stopping
|
|
629
742
|
const saved = saveLearningData();
|
|
630
743
|
|
|
@@ -1921,18 +2034,34 @@ const clearLearningData = () => {
|
|
|
1921
2034
|
};
|
|
1922
2035
|
|
|
1923
2036
|
module.exports = {
|
|
2037
|
+
// Core lifecycle
|
|
1924
2038
|
initialize,
|
|
1925
2039
|
stop,
|
|
2040
|
+
|
|
2041
|
+
// Data feeds (from algo)
|
|
1926
2042
|
feedTick,
|
|
1927
2043
|
feedSignal,
|
|
1928
2044
|
feedTradeResult,
|
|
2045
|
+
|
|
2046
|
+
// Trading decisions
|
|
1929
2047
|
getCurrentAdvice,
|
|
1930
2048
|
shouldTrade,
|
|
2049
|
+
|
|
2050
|
+
// Agent management (dynamic sync)
|
|
2051
|
+
syncAgents,
|
|
2052
|
+
refreshAgents,
|
|
2053
|
+
addAgent,
|
|
2054
|
+
removeAgent,
|
|
2055
|
+
getAgentInfo,
|
|
2056
|
+
|
|
2057
|
+
// Status & stats
|
|
1931
2058
|
getStatus,
|
|
1932
2059
|
analyzeAndOptimize,
|
|
1933
2060
|
getBehaviorHistory,
|
|
1934
2061
|
getLearningStats,
|
|
1935
2062
|
getLifetimeStats,
|
|
2063
|
+
|
|
2064
|
+
// Data management
|
|
1936
2065
|
clearLearningData,
|
|
1937
2066
|
loadLearningData
|
|
1938
2067
|
};
|