hedgequantx 2.6.160 → 2.6.162
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/menus/ai-agent-connect.js +181 -0
- package/src/menus/ai-agent-models.js +219 -0
- package/src/menus/ai-agent-oauth.js +292 -0
- package/src/menus/ai-agent-ui.js +141 -0
- package/src/menus/ai-agent.js +88 -1489
- package/src/pages/algo/copy-engine.js +449 -0
- package/src/pages/algo/copy-trading.js +11 -543
- package/src/pages/algo/smart-logs-data.js +218 -0
- package/src/pages/algo/smart-logs.js +9 -214
- package/src/pages/algo/ui-constants.js +144 -0
- package/src/pages/algo/ui-summary.js +184 -0
- package/src/pages/algo/ui.js +42 -526
- package/src/pages/stats-calculations.js +191 -0
- package/src/pages/stats-ui.js +381 -0
- package/src/pages/stats.js +14 -507
- package/src/services/ai/client-analysis.js +194 -0
- package/src/services/ai/client-models.js +333 -0
- package/src/services/ai/client.js +6 -489
- package/src/services/ai/index.js +2 -257
- package/src/services/ai/proxy-install.js +249 -0
- package/src/services/ai/proxy-manager.js +29 -411
- package/src/services/ai/proxy-remote.js +161 -0
- package/src/services/ai/strategy-supervisor.js +10 -765
- package/src/services/ai/supervisor-data.js +195 -0
- package/src/services/ai/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -0
- package/src/services/ai/supervisor-utils.js +158 -0
- package/src/services/ai/supervisor.js +50 -515
- package/src/services/ai/validation.js +250 -0
- package/src/services/hqx-server-events.js +110 -0
- package/src/services/hqx-server-handlers.js +217 -0
- package/src/services/hqx-server-latency.js +136 -0
- package/src/services/hqx-server.js +51 -403
- package/src/services/position-constants.js +28 -0
- package/src/services/position-manager.js +105 -554
- package/src/services/position-momentum.js +206 -0
- package/src/services/projectx/accounts.js +142 -0
- package/src/services/projectx/index.js +40 -289
- package/src/services/projectx/trading.js +180 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +32 -542
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Copy Trading Engine
|
|
3
|
+
* @module pages/algo/copy-engine
|
|
4
|
+
*
|
|
5
|
+
* Ultra-low latency copy trading with:
|
|
6
|
+
* - Fast polling (250ms adaptive)
|
|
7
|
+
* - Multi-follower support
|
|
8
|
+
* - Parallel order execution
|
|
9
|
+
* - Position reconciliation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { logger } = require('../../utils');
|
|
13
|
+
const { algoLogger } = require('./logger');
|
|
14
|
+
const StrategySupervisor = require('../../services/ai/strategy-supervisor');
|
|
15
|
+
|
|
16
|
+
const log = logger.scope('CopyEngine');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CopyEngine - Handles all copy trading logic with professional execution
|
|
20
|
+
*/
|
|
21
|
+
class CopyEngine {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.lead = config.lead;
|
|
24
|
+
this.followers = config.followers;
|
|
25
|
+
this.symbol = config.symbol;
|
|
26
|
+
this.dailyTarget = config.dailyTarget;
|
|
27
|
+
this.maxRisk = config.maxRisk;
|
|
28
|
+
this.ui = config.ui;
|
|
29
|
+
this.stats = config.stats;
|
|
30
|
+
|
|
31
|
+
// Engine state
|
|
32
|
+
this.running = false;
|
|
33
|
+
this.stopReason = null;
|
|
34
|
+
|
|
35
|
+
// Position tracking
|
|
36
|
+
this.leadPositions = new Map();
|
|
37
|
+
this.followerPositions = new Map();
|
|
38
|
+
this.pendingOrders = new Map();
|
|
39
|
+
|
|
40
|
+
// Order queue
|
|
41
|
+
this.orderQueues = new Map();
|
|
42
|
+
this.processingQueue = new Map();
|
|
43
|
+
|
|
44
|
+
// Timing
|
|
45
|
+
this.pollInterval = 250;
|
|
46
|
+
this.lastPollTime = 0;
|
|
47
|
+
this.pollCount = 0;
|
|
48
|
+
this.orderCount = 0;
|
|
49
|
+
this.failedOrders = 0;
|
|
50
|
+
this.lastLogTime = 0;
|
|
51
|
+
this.positionEntryTime = null;
|
|
52
|
+
|
|
53
|
+
// Retry configuration
|
|
54
|
+
this.maxRetries = 3;
|
|
55
|
+
this.retryDelayBase = 100;
|
|
56
|
+
this.maxSlippageTicks = 4;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getPositionKey(position) {
|
|
60
|
+
return position.contractId || position.symbol || position.id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
resolveSymbol(position, targetAccount) {
|
|
64
|
+
const targetType = targetAccount.type;
|
|
65
|
+
|
|
66
|
+
if (targetType === 'rithmic') {
|
|
67
|
+
return {
|
|
68
|
+
symbol: position.symbol || this.symbol.name,
|
|
69
|
+
exchange: position.exchange || this.symbol.exchange || 'CME',
|
|
70
|
+
contractId: null
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
return {
|
|
74
|
+
contractId: position.contractId || this.symbol.id || this.symbol.contractId,
|
|
75
|
+
symbol: null,
|
|
76
|
+
exchange: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
buildOrderData(params, platformType) {
|
|
82
|
+
const { accountId, contractId, symbol, exchange, side, size, type, price } = params;
|
|
83
|
+
|
|
84
|
+
if (platformType === 'rithmic') {
|
|
85
|
+
return { accountId, symbol, exchange: exchange || 'CME', size, side, type, price: price || 0 };
|
|
86
|
+
} else {
|
|
87
|
+
return { accountId, contractId, type, side, size };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async executeOrderWithRetry(follower, orderData, retryCount = 0) {
|
|
92
|
+
try {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
const result = await follower.service.placeOrder(orderData);
|
|
95
|
+
const latency = Date.now() - startTime;
|
|
96
|
+
|
|
97
|
+
if (result.success) {
|
|
98
|
+
this.orderCount++;
|
|
99
|
+
this.stats.latency = Math.round((this.stats.latency + latency) / 2);
|
|
100
|
+
return { success: true, latency };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (retryCount < this.maxRetries) {
|
|
104
|
+
const delay = this.retryDelayBase * Math.pow(2, retryCount);
|
|
105
|
+
await this.sleep(delay);
|
|
106
|
+
return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.failedOrders++;
|
|
110
|
+
return { success: false, error: result.error || 'Max retries exceeded' };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (retryCount < this.maxRetries) {
|
|
113
|
+
const delay = this.retryDelayBase * Math.pow(2, retryCount);
|
|
114
|
+
await this.sleep(delay);
|
|
115
|
+
return this.executeOrderWithRetry(follower, orderData, retryCount + 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.failedOrders++;
|
|
119
|
+
return { success: false, error: err.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async queueOrder(followerIdx, orderFn) {
|
|
124
|
+
if (!this.orderQueues.has(followerIdx)) {
|
|
125
|
+
this.orderQueues.set(followerIdx, []);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
this.orderQueues.get(followerIdx).push({ fn: orderFn, resolve });
|
|
130
|
+
this.processQueue(followerIdx);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async processQueue(followerIdx) {
|
|
135
|
+
if (this.processingQueue.get(followerIdx)) return;
|
|
136
|
+
|
|
137
|
+
const queue = this.orderQueues.get(followerIdx);
|
|
138
|
+
if (!queue || queue.length === 0) return;
|
|
139
|
+
|
|
140
|
+
this.processingQueue.set(followerIdx, true);
|
|
141
|
+
|
|
142
|
+
while (queue.length > 0 && this.running) {
|
|
143
|
+
const { fn, resolve } = queue.shift();
|
|
144
|
+
try {
|
|
145
|
+
const result = await fn();
|
|
146
|
+
resolve(result);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
resolve({ success: false, error: err.message });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.processingQueue.set(followerIdx, false);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async copyPositionOpen(position) {
|
|
156
|
+
const side = position.quantity > 0 ? 'LONG' : 'SHORT';
|
|
157
|
+
const orderSide = position.quantity > 0 ? 0 : 1;
|
|
158
|
+
const displaySymbol = position.symbol || this.symbol.name;
|
|
159
|
+
const size = Math.abs(position.quantity);
|
|
160
|
+
const entry = position.averagePrice || 0;
|
|
161
|
+
|
|
162
|
+
this.positionEntryTime = Date.now();
|
|
163
|
+
algoLogger.positionOpened(this.ui, displaySymbol, side, size, entry);
|
|
164
|
+
|
|
165
|
+
if (this.stats.aiSupervision) {
|
|
166
|
+
StrategySupervisor.feedSignal({
|
|
167
|
+
direction: side.toLowerCase(),
|
|
168
|
+
entry,
|
|
169
|
+
stopLoss: null,
|
|
170
|
+
takeProfit: null,
|
|
171
|
+
confidence: 0.5
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const promises = this.followers.map((follower, idx) => {
|
|
176
|
+
return this.queueOrder(idx, async () => {
|
|
177
|
+
const resolved = this.resolveSymbol(position, follower);
|
|
178
|
+
const orderData = this.buildOrderData({
|
|
179
|
+
accountId: follower.account.accountId,
|
|
180
|
+
contractId: resolved.contractId,
|
|
181
|
+
symbol: resolved.symbol,
|
|
182
|
+
exchange: resolved.exchange,
|
|
183
|
+
side: orderSide,
|
|
184
|
+
size: follower.contracts,
|
|
185
|
+
type: 2
|
|
186
|
+
}, follower.type);
|
|
187
|
+
|
|
188
|
+
algoLogger.info(this.ui, 'COPY ORDER', `${side} ${follower.contracts}x -> ${follower.propfirm}`);
|
|
189
|
+
|
|
190
|
+
const result = await this.executeOrderWithRetry(follower, orderData);
|
|
191
|
+
|
|
192
|
+
if (result.success) {
|
|
193
|
+
algoLogger.orderFilled(this.ui, displaySymbol, side, follower.contracts, entry);
|
|
194
|
+
const posKey = this.getPositionKey(position);
|
|
195
|
+
this.followerPositions.set(`${idx}:${posKey}`, {
|
|
196
|
+
...position,
|
|
197
|
+
followerIdx: idx,
|
|
198
|
+
openTime: Date.now()
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
algoLogger.orderRejected(this.ui, displaySymbol, result.error);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const results = await Promise.all(promises);
|
|
209
|
+
const successCount = results.filter(r => r.success).length;
|
|
210
|
+
|
|
211
|
+
if (successCount === this.followers.length) {
|
|
212
|
+
algoLogger.info(this.ui, 'ALL COPIED', `${successCount}/${this.followers.length} followers`);
|
|
213
|
+
} else if (successCount > 0) {
|
|
214
|
+
algoLogger.info(this.ui, 'PARTIAL COPY', `${successCount}/${this.followers.length} followers`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async copyPositionClose(position, exitPrice, pnl) {
|
|
221
|
+
const side = position.quantity > 0 ? 'LONG' : 'SHORT';
|
|
222
|
+
const closeSide = position.quantity > 0 ? 1 : 0;
|
|
223
|
+
const displaySymbol = position.symbol || this.symbol.name;
|
|
224
|
+
const size = Math.abs(position.quantity);
|
|
225
|
+
|
|
226
|
+
this.positionEntryTime = null;
|
|
227
|
+
algoLogger.positionClosed(this.ui, displaySymbol, side, size, exitPrice, pnl);
|
|
228
|
+
|
|
229
|
+
if (this.stats.aiSupervision) {
|
|
230
|
+
StrategySupervisor.feedTradeResult({
|
|
231
|
+
side,
|
|
232
|
+
qty: size,
|
|
233
|
+
price: exitPrice,
|
|
234
|
+
pnl,
|
|
235
|
+
symbol: displaySymbol,
|
|
236
|
+
direction: side
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const aiStatus = StrategySupervisor.getStatus();
|
|
240
|
+
if (aiStatus.patternsLearned.winning + aiStatus.patternsLearned.losing > 0) {
|
|
241
|
+
algoLogger.info(this.ui, 'AI LEARNING',
|
|
242
|
+
`${aiStatus.patternsLearned.winning}W/${aiStatus.patternsLearned.losing}L patterns`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const posKey = this.getPositionKey(position);
|
|
247
|
+
|
|
248
|
+
const promises = this.followers.map((follower, idx) => {
|
|
249
|
+
return this.queueOrder(idx, async () => {
|
|
250
|
+
const resolved = this.resolveSymbol(position, follower);
|
|
251
|
+
const posIdentifier = follower.type === 'rithmic'
|
|
252
|
+
? (position.symbol || this.symbol.name)
|
|
253
|
+
: (position.contractId || this.symbol.id);
|
|
254
|
+
|
|
255
|
+
algoLogger.info(this.ui, 'CLOSE ORDER', `${displaySymbol} -> ${follower.propfirm}`);
|
|
256
|
+
|
|
257
|
+
let result = await follower.service.closePosition(follower.account.accountId, posIdentifier);
|
|
258
|
+
|
|
259
|
+
if (!result.success) {
|
|
260
|
+
const orderData = this.buildOrderData({
|
|
261
|
+
accountId: follower.account.accountId,
|
|
262
|
+
contractId: resolved.contractId,
|
|
263
|
+
symbol: resolved.symbol,
|
|
264
|
+
exchange: resolved.exchange,
|
|
265
|
+
side: closeSide,
|
|
266
|
+
size: follower.contracts,
|
|
267
|
+
type: 2
|
|
268
|
+
}, follower.type);
|
|
269
|
+
|
|
270
|
+
result = await this.executeOrderWithRetry(follower, orderData);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (result.success) {
|
|
274
|
+
algoLogger.info(this.ui, 'CLOSED', `${displaySymbol} on ${follower.propfirm}`);
|
|
275
|
+
this.followerPositions.delete(`${idx}:${posKey}`);
|
|
276
|
+
} else {
|
|
277
|
+
algoLogger.error(this.ui, 'CLOSE FAILED', `${follower.propfirm}: ${result.error}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const results = await Promise.all(promises);
|
|
285
|
+
const successCount = results.filter(r => r.success).length;
|
|
286
|
+
|
|
287
|
+
if (successCount === this.followers.length) {
|
|
288
|
+
this.stats.trades++;
|
|
289
|
+
this.stats.sessionPnl += pnl;
|
|
290
|
+
if (pnl >= 0) this.stats.wins++;
|
|
291
|
+
else this.stats.losses++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async pollLeadPositions() {
|
|
298
|
+
if (!this.running) return;
|
|
299
|
+
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = await this.lead.service.getPositions(this.lead.account.accountId);
|
|
304
|
+
if (!result.success) return;
|
|
305
|
+
|
|
306
|
+
const currentPositions = result.positions || [];
|
|
307
|
+
const currentMap = new Map();
|
|
308
|
+
|
|
309
|
+
for (const pos of currentPositions) {
|
|
310
|
+
if (pos.quantity === 0) continue;
|
|
311
|
+
const key = this.getPositionKey(pos);
|
|
312
|
+
currentMap.set(key, pos);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Detect new positions
|
|
316
|
+
for (const [key, pos] of currentMap) {
|
|
317
|
+
if (!this.leadPositions.has(key)) {
|
|
318
|
+
await this.copyPositionOpen(pos);
|
|
319
|
+
this.leadPositions.set(key, pos);
|
|
320
|
+
} else {
|
|
321
|
+
const oldPos = this.leadPositions.get(key);
|
|
322
|
+
if (Math.abs(pos.quantity) !== Math.abs(oldPos.quantity)) {
|
|
323
|
+
this.leadPositions.set(key, pos);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Detect closed positions
|
|
329
|
+
for (const [key, oldPos] of this.leadPositions) {
|
|
330
|
+
if (!currentMap.has(key)) {
|
|
331
|
+
const exitPrice = oldPos.averagePrice || 0;
|
|
332
|
+
const pnl = oldPos.profitAndLoss || 0;
|
|
333
|
+
await this.copyPositionClose(oldPos, exitPrice, pnl);
|
|
334
|
+
this.leadPositions.delete(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Update P&L
|
|
339
|
+
const totalPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
|
|
340
|
+
this.stats.pnl = totalPnL;
|
|
341
|
+
|
|
342
|
+
// Check limits
|
|
343
|
+
if (totalPnL >= this.dailyTarget) {
|
|
344
|
+
this.stop('target');
|
|
345
|
+
algoLogger.info(this.ui, 'TARGET REACHED', `+$${totalPnL.toFixed(2)}`);
|
|
346
|
+
} else if (totalPnL <= -this.maxRisk) {
|
|
347
|
+
this.stop('risk');
|
|
348
|
+
algoLogger.error(this.ui, 'MAX RISK HIT', `-$${Math.abs(totalPnL).toFixed(2)}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Adaptive polling
|
|
352
|
+
const pollTime = Date.now() - startTime;
|
|
353
|
+
this.stats.latency = pollTime;
|
|
354
|
+
|
|
355
|
+
if (this.leadPositions.size > 0) {
|
|
356
|
+
this.pollInterval = Math.max(100, Math.min(250, pollTime * 2));
|
|
357
|
+
} else {
|
|
358
|
+
this.pollInterval = Math.max(250, Math.min(500, pollTime * 3));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.pollCount++;
|
|
362
|
+
|
|
363
|
+
// Smart logs
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
if (now - this.lastLogTime > 1000) {
|
|
366
|
+
const smartLogs = require('./smart-logs');
|
|
367
|
+
|
|
368
|
+
if (this.leadPositions.size === 0) {
|
|
369
|
+
const scanLog = smartLogs.getScanningLog(true);
|
|
370
|
+
this.ui.addLog('info', `${scanLog.message} poll #${this.pollCount} | ${pollTime}ms`);
|
|
371
|
+
}
|
|
372
|
+
this.lastLogTime = now;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
} catch (err) {
|
|
376
|
+
log.warn('Poll error', { error: err.message });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async reconcilePositions() {
|
|
381
|
+
for (let idx = 0; idx < this.followers.length; idx++) {
|
|
382
|
+
const follower = this.followers[idx];
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const result = await follower.service.getPositions(follower.account.accountId);
|
|
386
|
+
if (!result.success) continue;
|
|
387
|
+
|
|
388
|
+
const followerPositions = result.positions || [];
|
|
389
|
+
|
|
390
|
+
// Check missing positions
|
|
391
|
+
for (const [key, leadPos] of this.leadPositions) {
|
|
392
|
+
const hasFollowerPos = followerPositions.some(fp => {
|
|
393
|
+
const fpKey = this.getPositionKey(fp);
|
|
394
|
+
return fpKey === key && fp.quantity !== 0;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!hasFollowerPos) {
|
|
398
|
+
algoLogger.info(this.ui, 'RECONCILE', `Missing ${key} on ${follower.propfirm}`);
|
|
399
|
+
await this.copyPositionOpen(leadPos);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check orphaned positions
|
|
404
|
+
for (const fp of followerPositions) {
|
|
405
|
+
if (fp.quantity === 0) continue;
|
|
406
|
+
const fpKey = this.getPositionKey(fp);
|
|
407
|
+
|
|
408
|
+
if (!this.leadPositions.has(fpKey)) {
|
|
409
|
+
algoLogger.info(this.ui, 'RECONCILE', `Orphaned ${fpKey} on ${follower.propfirm}`);
|
|
410
|
+
const posIdentifier = follower.type === 'rithmic' ? fp.symbol : fp.contractId;
|
|
411
|
+
await follower.service.closePosition(follower.account.accountId, posIdentifier);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
} catch (err) {
|
|
416
|
+
log.warn('Reconcile error', { follower: follower.propfirm, error: err.message });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async start() {
|
|
422
|
+
this.running = true;
|
|
423
|
+
this.stats.connected = true;
|
|
424
|
+
|
|
425
|
+
algoLogger.info(this.ui, 'ENGINE STARTED', `Polling every ${this.pollInterval}ms`);
|
|
426
|
+
algoLogger.info(this.ui, 'FOLLOWERS', `${this.followers.length} account(s)`);
|
|
427
|
+
|
|
428
|
+
await this.reconcilePositions();
|
|
429
|
+
|
|
430
|
+
while (this.running) {
|
|
431
|
+
await this.pollLeadPositions();
|
|
432
|
+
await this.sleep(this.pollInterval);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return this.stopReason;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
stop(reason = 'manual') {
|
|
439
|
+
this.running = false;
|
|
440
|
+
this.stopReason = reason;
|
|
441
|
+
this.stats.connected = false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
sleep(ms) {
|
|
445
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = { CopyEngine };
|