backtest-kit 1.1.9 → 1.2.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/README.md +393 -32
- package/build/index.cjs +1759 -440
- package/build/index.mjs +1758 -441
- package/package.json +1 -1
- package/types.d.ts +485 -23
package/build/index.mjs
CHANGED
|
@@ -6,6 +6,37 @@ import path, { join } from 'path';
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
|
|
9
|
+
const GLOBAL_CONFIG = {
|
|
10
|
+
/**
|
|
11
|
+
* Time to wait for scheduled signal to activate (in minutes)
|
|
12
|
+
* If signal does not activate within this time, it will be cancelled.
|
|
13
|
+
*/
|
|
14
|
+
CC_SCHEDULE_AWAIT_MINUTES: 120,
|
|
15
|
+
/**
|
|
16
|
+
* Number of candles to use for average price calculation (VWAP)
|
|
17
|
+
* Default: 5 candles (last 5 minutes when using 1m interval)
|
|
18
|
+
*/
|
|
19
|
+
CC_AVG_PRICE_CANDLES_COUNT: 5,
|
|
20
|
+
/**
|
|
21
|
+
* Minimum TakeProfit distance from priceOpen (percentage)
|
|
22
|
+
* Must be greater than trading fees to ensure profitable trades
|
|
23
|
+
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
24
|
+
*/
|
|
25
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.1,
|
|
26
|
+
/**
|
|
27
|
+
* Maximum StopLoss distance from priceOpen (percentage)
|
|
28
|
+
* Prevents catastrophic losses from extreme StopLoss values
|
|
29
|
+
* Default: 20% (one signal cannot lose more than 20% of position)
|
|
30
|
+
*/
|
|
31
|
+
CC_MAX_STOPLOSS_DISTANCE_PERCENT: 20,
|
|
32
|
+
/**
|
|
33
|
+
* Maximum signal lifetime in minutes
|
|
34
|
+
* Prevents eternal signals that block risk limits for weeks/months
|
|
35
|
+
* Default: 1440 minutes (1 day)
|
|
36
|
+
*/
|
|
37
|
+
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
38
|
+
};
|
|
39
|
+
|
|
9
40
|
const { init, inject, provide } = createActivator("backtest");
|
|
10
41
|
|
|
11
42
|
/**
|
|
@@ -79,6 +110,7 @@ const logicPublicServices$1 = {
|
|
|
79
110
|
const markdownServices$1 = {
|
|
80
111
|
backtestMarkdownService: Symbol('backtestMarkdownService'),
|
|
81
112
|
liveMarkdownService: Symbol('liveMarkdownService'),
|
|
113
|
+
scheduleMarkdownService: Symbol('scheduleMarkdownService'),
|
|
82
114
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
83
115
|
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
84
116
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
@@ -338,7 +370,8 @@ class ClientExchange {
|
|
|
338
370
|
return filteredData;
|
|
339
371
|
}
|
|
340
372
|
/**
|
|
341
|
-
* Calculates VWAP (Volume Weighted Average Price) from last
|
|
373
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
374
|
+
* The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
|
|
342
375
|
*
|
|
343
376
|
* Formula:
|
|
344
377
|
* - Typical Price = (high + low + close) / 3
|
|
@@ -354,7 +387,7 @@ class ClientExchange {
|
|
|
354
387
|
this.params.logger.debug(`ClientExchange getAveragePrice`, {
|
|
355
388
|
symbol,
|
|
356
389
|
});
|
|
357
|
-
const candles = await this.getCandles(symbol, "1m",
|
|
390
|
+
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
358
391
|
if (candles.length === 0) {
|
|
359
392
|
throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
|
|
360
393
|
}
|
|
@@ -1300,14 +1333,24 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1300
1333
|
};
|
|
1301
1334
|
const VALIDATE_SIGNAL_FN = (signal) => {
|
|
1302
1335
|
const errors = [];
|
|
1303
|
-
//
|
|
1304
|
-
if (signal.priceOpen
|
|
1336
|
+
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
1337
|
+
if (!isFinite(signal.priceOpen)) {
|
|
1338
|
+
errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
1339
|
+
}
|
|
1340
|
+
if (!isFinite(signal.priceTakeProfit)) {
|
|
1341
|
+
errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
1342
|
+
}
|
|
1343
|
+
if (!isFinite(signal.priceStopLoss)) {
|
|
1344
|
+
errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
1345
|
+
}
|
|
1346
|
+
// Валидация цен (только если они конечные)
|
|
1347
|
+
if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
|
|
1305
1348
|
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
1306
1349
|
}
|
|
1307
|
-
if (signal.priceTakeProfit <= 0) {
|
|
1350
|
+
if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
|
|
1308
1351
|
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
1309
1352
|
}
|
|
1310
|
-
if (signal.priceStopLoss <= 0) {
|
|
1353
|
+
if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
|
|
1311
1354
|
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
1312
1355
|
}
|
|
1313
1356
|
// Валидация для long позиции
|
|
@@ -1318,6 +1361,24 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1318
1361
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1319
1362
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1320
1363
|
}
|
|
1364
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1365
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1366
|
+
const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1367
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1368
|
+
errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1369
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1370
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1374
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1375
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1376
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1377
|
+
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1378
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1379
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1321
1382
|
}
|
|
1322
1383
|
// Валидация для short позиции
|
|
1323
1384
|
if (signal.position === "short") {
|
|
@@ -1327,13 +1388,44 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1327
1388
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1328
1389
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1329
1390
|
}
|
|
1391
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1392
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1393
|
+
const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
|
|
1394
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1395
|
+
errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1396
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1397
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1401
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1402
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1403
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1404
|
+
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1405
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1406
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1330
1409
|
}
|
|
1331
1410
|
// Валидация временных параметров
|
|
1332
1411
|
if (signal.minuteEstimatedTime <= 0) {
|
|
1333
1412
|
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
1334
1413
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1414
|
+
// ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
|
|
1415
|
+
if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1416
|
+
if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1417
|
+
const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
|
|
1418
|
+
const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
|
|
1419
|
+
errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
|
|
1420
|
+
`Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
|
|
1421
|
+
`Eternal signals block risk limits and prevent new trades.`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (signal.scheduledAt <= 0) {
|
|
1425
|
+
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
1426
|
+
}
|
|
1427
|
+
if (signal.pendingAt <= 0) {
|
|
1428
|
+
errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
|
|
1337
1429
|
}
|
|
1338
1430
|
// Кидаем ошибку если есть проблемы
|
|
1339
1431
|
if (errors.length > 0) {
|
|
@@ -1369,6 +1461,27 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
1369
1461
|
if (!signal) {
|
|
1370
1462
|
return null;
|
|
1371
1463
|
}
|
|
1464
|
+
// Если priceOpen указан - создаем scheduled signal (risk check при активации)
|
|
1465
|
+
if (signal.priceOpen !== undefined) {
|
|
1466
|
+
const scheduledSignalRow = {
|
|
1467
|
+
id: randomString(),
|
|
1468
|
+
priceOpen: signal.priceOpen,
|
|
1469
|
+
position: signal.position,
|
|
1470
|
+
note: signal.note,
|
|
1471
|
+
priceTakeProfit: signal.priceTakeProfit,
|
|
1472
|
+
priceStopLoss: signal.priceStopLoss,
|
|
1473
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
1474
|
+
symbol: self.params.execution.context.symbol,
|
|
1475
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1476
|
+
strategyName: self.params.method.context.strategyName,
|
|
1477
|
+
scheduledAt: currentTime,
|
|
1478
|
+
pendingAt: currentTime, // Временно, обновится при активации
|
|
1479
|
+
_isScheduled: true,
|
|
1480
|
+
};
|
|
1481
|
+
// Валидируем сигнал перед возвратом
|
|
1482
|
+
VALIDATE_SIGNAL_FN(scheduledSignalRow);
|
|
1483
|
+
return scheduledSignalRow;
|
|
1484
|
+
}
|
|
1372
1485
|
const signalRow = {
|
|
1373
1486
|
id: randomString(),
|
|
1374
1487
|
priceOpen: currentPrice,
|
|
@@ -1376,7 +1489,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
1376
1489
|
symbol: self.params.execution.context.symbol,
|
|
1377
1490
|
exchangeName: self.params.method.context.exchangeName,
|
|
1378
1491
|
strategyName: self.params.method.context.strategyName,
|
|
1379
|
-
|
|
1492
|
+
scheduledAt: currentTime,
|
|
1493
|
+
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
1494
|
+
_isScheduled: false,
|
|
1380
1495
|
};
|
|
1381
1496
|
// Валидируем сигнал перед возвратом
|
|
1382
1497
|
VALIDATE_SIGNAL_FN(signalRow);
|
|
@@ -1417,6 +1532,555 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1417
1532
|
return;
|
|
1418
1533
|
}
|
|
1419
1534
|
self._pendingSignal = pendingSignal;
|
|
1535
|
+
// Call onActive callback for restored signal
|
|
1536
|
+
if (self.params.callbacks?.onActive) {
|
|
1537
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1538
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
1542
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1543
|
+
const signalTime = scheduled.scheduledAt; // Таймаут для scheduled signal считается от scheduledAt
|
|
1544
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
1545
|
+
const elapsedTime = currentTime - signalTime;
|
|
1546
|
+
if (elapsedTime < maxTimeToWait) {
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled by timeout", {
|
|
1550
|
+
symbol: self.params.execution.context.symbol,
|
|
1551
|
+
signalId: scheduled.id,
|
|
1552
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
1553
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
1554
|
+
});
|
|
1555
|
+
self._scheduledSignal = null;
|
|
1556
|
+
if (self.params.callbacks?.onCancel) {
|
|
1557
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
1558
|
+
}
|
|
1559
|
+
const result = {
|
|
1560
|
+
action: "cancelled",
|
|
1561
|
+
signal: scheduled,
|
|
1562
|
+
currentPrice: currentPrice,
|
|
1563
|
+
closeTimestamp: currentTime,
|
|
1564
|
+
strategyName: self.params.method.context.strategyName,
|
|
1565
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1566
|
+
symbol: self.params.execution.context.symbol,
|
|
1567
|
+
};
|
|
1568
|
+
if (self.params.callbacks?.onTick) {
|
|
1569
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1570
|
+
}
|
|
1571
|
+
return result;
|
|
1572
|
+
};
|
|
1573
|
+
const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
|
|
1574
|
+
let shouldActivate = false;
|
|
1575
|
+
let shouldCancel = false;
|
|
1576
|
+
if (scheduled.position === "long") {
|
|
1577
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1578
|
+
// Отмена если цена упала СЛИШКОМ низко (ниже SL)
|
|
1579
|
+
if (currentPrice <= scheduled.priceStopLoss) {
|
|
1580
|
+
shouldCancel = true;
|
|
1581
|
+
}
|
|
1582
|
+
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1583
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1584
|
+
else if (currentPrice <= scheduled.priceOpen) {
|
|
1585
|
+
shouldActivate = true;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (scheduled.position === "short") {
|
|
1589
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1590
|
+
// Отмена если цена выросла СЛИШКОМ высоко (выше SL)
|
|
1591
|
+
if (currentPrice >= scheduled.priceStopLoss) {
|
|
1592
|
+
shouldCancel = true;
|
|
1593
|
+
}
|
|
1594
|
+
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
1595
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1596
|
+
else if (currentPrice >= scheduled.priceOpen) {
|
|
1597
|
+
shouldActivate = true;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return { shouldActivate, shouldCancel };
|
|
1601
|
+
};
|
|
1602
|
+
const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPrice) => {
|
|
1603
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled", {
|
|
1604
|
+
symbol: self.params.execution.context.symbol,
|
|
1605
|
+
signalId: scheduled.id,
|
|
1606
|
+
position: scheduled.position,
|
|
1607
|
+
averagePrice: currentPrice,
|
|
1608
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1609
|
+
});
|
|
1610
|
+
self._scheduledSignal = null;
|
|
1611
|
+
const result = {
|
|
1612
|
+
action: "idle",
|
|
1613
|
+
signal: null,
|
|
1614
|
+
strategyName: self.params.method.context.strategyName,
|
|
1615
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1616
|
+
symbol: self.params.execution.context.symbol,
|
|
1617
|
+
currentPrice: currentPrice,
|
|
1618
|
+
};
|
|
1619
|
+
if (self.params.callbacks?.onTick) {
|
|
1620
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
};
|
|
1624
|
+
const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
|
|
1625
|
+
// Check if strategy was stopped
|
|
1626
|
+
if (self._isStopped) {
|
|
1627
|
+
self.params.logger.info("ClientStrategy scheduled signal activation cancelled (stopped)", {
|
|
1628
|
+
symbol: self.params.execution.context.symbol,
|
|
1629
|
+
signalId: scheduled.id,
|
|
1630
|
+
});
|
|
1631
|
+
self._scheduledSignal = null;
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
// В LIVE режиме activationTimestamp - это текущее время при tick()
|
|
1635
|
+
// В отличие от BACKTEST (где используется candle.timestamp + 60s),
|
|
1636
|
+
// здесь мы не знаем ТОЧНОЕ время достижения priceOpen,
|
|
1637
|
+
// поэтому используем время обнаружения активации
|
|
1638
|
+
const activationTime = activationTimestamp;
|
|
1639
|
+
self.params.logger.info("ClientStrategy scheduled signal activation begin", {
|
|
1640
|
+
symbol: self.params.execution.context.symbol,
|
|
1641
|
+
signalId: scheduled.id,
|
|
1642
|
+
position: scheduled.position,
|
|
1643
|
+
averagePrice: scheduled.priceOpen,
|
|
1644
|
+
priceOpen: scheduled.priceOpen,
|
|
1645
|
+
scheduledAt: scheduled.scheduledAt,
|
|
1646
|
+
pendingAt: activationTime,
|
|
1647
|
+
});
|
|
1648
|
+
if (await not(self.params.risk.checkSignal({
|
|
1649
|
+
symbol: self.params.execution.context.symbol,
|
|
1650
|
+
strategyName: self.params.method.context.strategyName,
|
|
1651
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1652
|
+
currentPrice: scheduled.priceOpen,
|
|
1653
|
+
timestamp: activationTime,
|
|
1654
|
+
}))) {
|
|
1655
|
+
self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
|
|
1656
|
+
symbol: self.params.execution.context.symbol,
|
|
1657
|
+
signalId: scheduled.id,
|
|
1658
|
+
});
|
|
1659
|
+
self._scheduledSignal = null;
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
self._scheduledSignal = null;
|
|
1663
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
1664
|
+
const activatedSignal = {
|
|
1665
|
+
...scheduled,
|
|
1666
|
+
pendingAt: activationTime,
|
|
1667
|
+
_isScheduled: false,
|
|
1668
|
+
};
|
|
1669
|
+
await self.setPendingSignal(activatedSignal);
|
|
1670
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1671
|
+
strategyName: self.params.method.context.strategyName,
|
|
1672
|
+
riskName: self.params.riskName,
|
|
1673
|
+
});
|
|
1674
|
+
if (self.params.callbacks?.onOpen) {
|
|
1675
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
|
|
1676
|
+
}
|
|
1677
|
+
const result = {
|
|
1678
|
+
action: "opened",
|
|
1679
|
+
signal: self._pendingSignal,
|
|
1680
|
+
strategyName: self.params.method.context.strategyName,
|
|
1681
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1682
|
+
symbol: self.params.execution.context.symbol,
|
|
1683
|
+
currentPrice: self._pendingSignal.priceOpen,
|
|
1684
|
+
};
|
|
1685
|
+
if (self.params.callbacks?.onTick) {
|
|
1686
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1687
|
+
}
|
|
1688
|
+
return result;
|
|
1689
|
+
};
|
|
1690
|
+
const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
|
|
1691
|
+
const result = {
|
|
1692
|
+
action: "active",
|
|
1693
|
+
signal: scheduled,
|
|
1694
|
+
currentPrice: currentPrice,
|
|
1695
|
+
strategyName: self.params.method.context.strategyName,
|
|
1696
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1697
|
+
symbol: self.params.execution.context.symbol,
|
|
1698
|
+
};
|
|
1699
|
+
if (self.params.callbacks?.onTick) {
|
|
1700
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1701
|
+
}
|
|
1702
|
+
return result;
|
|
1703
|
+
};
|
|
1704
|
+
const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
1705
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1706
|
+
self.params.logger.info("ClientStrategy scheduled signal created", {
|
|
1707
|
+
symbol: self.params.execution.context.symbol,
|
|
1708
|
+
signalId: signal.id,
|
|
1709
|
+
position: signal.position,
|
|
1710
|
+
priceOpen: signal.priceOpen,
|
|
1711
|
+
currentPrice: currentPrice,
|
|
1712
|
+
});
|
|
1713
|
+
if (self.params.callbacks?.onSchedule) {
|
|
1714
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1715
|
+
}
|
|
1716
|
+
const result = {
|
|
1717
|
+
action: "scheduled",
|
|
1718
|
+
signal: signal,
|
|
1719
|
+
strategyName: self.params.method.context.strategyName,
|
|
1720
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1721
|
+
symbol: self.params.execution.context.symbol,
|
|
1722
|
+
currentPrice: currentPrice,
|
|
1723
|
+
};
|
|
1724
|
+
if (self.params.callbacks?.onTick) {
|
|
1725
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1726
|
+
}
|
|
1727
|
+
return result;
|
|
1728
|
+
};
|
|
1729
|
+
const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
1730
|
+
if (await not(self.params.risk.checkSignal({
|
|
1731
|
+
symbol: self.params.execution.context.symbol,
|
|
1732
|
+
strategyName: self.params.method.context.strategyName,
|
|
1733
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1734
|
+
currentPrice: signal.priceOpen,
|
|
1735
|
+
timestamp: self.params.execution.context.when.getTime(),
|
|
1736
|
+
}))) {
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1740
|
+
strategyName: self.params.method.context.strategyName,
|
|
1741
|
+
riskName: self.params.riskName,
|
|
1742
|
+
});
|
|
1743
|
+
if (self.params.callbacks?.onOpen) {
|
|
1744
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
|
|
1745
|
+
}
|
|
1746
|
+
const result = {
|
|
1747
|
+
action: "opened",
|
|
1748
|
+
signal: signal,
|
|
1749
|
+
strategyName: self.params.method.context.strategyName,
|
|
1750
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1751
|
+
symbol: self.params.execution.context.symbol,
|
|
1752
|
+
currentPrice: signal.priceOpen,
|
|
1753
|
+
};
|
|
1754
|
+
if (self.params.callbacks?.onTick) {
|
|
1755
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1756
|
+
}
|
|
1757
|
+
return result;
|
|
1758
|
+
};
|
|
1759
|
+
const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
|
|
1760
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1761
|
+
const signalTime = signal.pendingAt; // КРИТИЧНО: используем pendingAt, а не scheduledAt!
|
|
1762
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
1763
|
+
const elapsedTime = currentTime - signalTime;
|
|
1764
|
+
// Check time expiration
|
|
1765
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
1766
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, averagePrice, "time_expired");
|
|
1767
|
+
}
|
|
1768
|
+
// Check take profit
|
|
1769
|
+
if (signal.position === "long" && averagePrice >= signal.priceTakeProfit) {
|
|
1770
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1771
|
+
"take_profit");
|
|
1772
|
+
}
|
|
1773
|
+
if (signal.position === "short" && averagePrice <= signal.priceTakeProfit) {
|
|
1774
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1775
|
+
"take_profit");
|
|
1776
|
+
}
|
|
1777
|
+
// Check stop loss
|
|
1778
|
+
if (signal.position === "long" && averagePrice <= signal.priceStopLoss) {
|
|
1779
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1780
|
+
"stop_loss");
|
|
1781
|
+
}
|
|
1782
|
+
if (signal.position === "short" && averagePrice >= signal.priceStopLoss) {
|
|
1783
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1784
|
+
"stop_loss");
|
|
1785
|
+
}
|
|
1786
|
+
return null;
|
|
1787
|
+
};
|
|
1788
|
+
const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
|
|
1789
|
+
const pnl = toProfitLossDto(signal, currentPrice);
|
|
1790
|
+
self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
|
|
1791
|
+
symbol: self.params.execution.context.symbol,
|
|
1792
|
+
signalId: signal.id,
|
|
1793
|
+
closeReason,
|
|
1794
|
+
priceClose: currentPrice,
|
|
1795
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
1796
|
+
});
|
|
1797
|
+
if (self.params.callbacks?.onClose) {
|
|
1798
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1799
|
+
}
|
|
1800
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1801
|
+
strategyName: self.params.method.context.strategyName,
|
|
1802
|
+
riskName: self.params.riskName,
|
|
1803
|
+
});
|
|
1804
|
+
await self.setPendingSignal(null);
|
|
1805
|
+
const result = {
|
|
1806
|
+
action: "closed",
|
|
1807
|
+
signal: signal,
|
|
1808
|
+
currentPrice: currentPrice,
|
|
1809
|
+
closeReason: closeReason,
|
|
1810
|
+
closeTimestamp: self.params.execution.context.when.getTime(),
|
|
1811
|
+
pnl: pnl,
|
|
1812
|
+
strategyName: self.params.method.context.strategyName,
|
|
1813
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1814
|
+
symbol: self.params.execution.context.symbol,
|
|
1815
|
+
};
|
|
1816
|
+
if (self.params.callbacks?.onTick) {
|
|
1817
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1818
|
+
}
|
|
1819
|
+
return result;
|
|
1820
|
+
};
|
|
1821
|
+
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
1822
|
+
const result = {
|
|
1823
|
+
action: "active",
|
|
1824
|
+
signal: signal,
|
|
1825
|
+
currentPrice: currentPrice,
|
|
1826
|
+
strategyName: self.params.method.context.strategyName,
|
|
1827
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1828
|
+
symbol: self.params.execution.context.symbol,
|
|
1829
|
+
};
|
|
1830
|
+
if (self.params.callbacks?.onTick) {
|
|
1831
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1832
|
+
}
|
|
1833
|
+
return result;
|
|
1834
|
+
};
|
|
1835
|
+
const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
1836
|
+
if (self.params.callbacks?.onIdle) {
|
|
1837
|
+
self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
|
|
1838
|
+
}
|
|
1839
|
+
const result = {
|
|
1840
|
+
action: "idle",
|
|
1841
|
+
signal: null,
|
|
1842
|
+
strategyName: self.params.method.context.strategyName,
|
|
1843
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1844
|
+
symbol: self.params.execution.context.symbol,
|
|
1845
|
+
currentPrice: currentPrice,
|
|
1846
|
+
};
|
|
1847
|
+
if (self.params.callbacks?.onTick) {
|
|
1848
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1849
|
+
}
|
|
1850
|
+
return result;
|
|
1851
|
+
};
|
|
1852
|
+
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp) => {
|
|
1853
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal cancelled", {
|
|
1854
|
+
symbol: self.params.execution.context.symbol,
|
|
1855
|
+
signalId: scheduled.id,
|
|
1856
|
+
closeTimestamp,
|
|
1857
|
+
averagePrice,
|
|
1858
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1859
|
+
});
|
|
1860
|
+
self._scheduledSignal = null;
|
|
1861
|
+
if (self.params.callbacks?.onCancel) {
|
|
1862
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
1863
|
+
}
|
|
1864
|
+
const result = {
|
|
1865
|
+
action: "cancelled",
|
|
1866
|
+
signal: scheduled,
|
|
1867
|
+
currentPrice: averagePrice,
|
|
1868
|
+
closeTimestamp: closeTimestamp,
|
|
1869
|
+
strategyName: self.params.method.context.strategyName,
|
|
1870
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1871
|
+
symbol: self.params.execution.context.symbol,
|
|
1872
|
+
};
|
|
1873
|
+
if (self.params.callbacks?.onTick) {
|
|
1874
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1875
|
+
}
|
|
1876
|
+
return result;
|
|
1877
|
+
};
|
|
1878
|
+
const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
|
|
1879
|
+
// Check if strategy was stopped
|
|
1880
|
+
if (self._isStopped) {
|
|
1881
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activation cancelled (stopped)", {
|
|
1882
|
+
symbol: self.params.execution.context.symbol,
|
|
1883
|
+
signalId: scheduled.id,
|
|
1884
|
+
});
|
|
1885
|
+
self._scheduledSignal = null;
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
// В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
|
|
1889
|
+
// (timestamp СЛЕДУЮЩЕЙ свечи после достижения priceOpen)
|
|
1890
|
+
// Это обеспечивает точный расчёт minuteEstimatedTime от момента активации
|
|
1891
|
+
const activationTime = activationTimestamp;
|
|
1892
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activated", {
|
|
1893
|
+
symbol: self.params.execution.context.symbol,
|
|
1894
|
+
signalId: scheduled.id,
|
|
1895
|
+
priceOpen: scheduled.priceOpen,
|
|
1896
|
+
scheduledAt: scheduled.scheduledAt,
|
|
1897
|
+
pendingAt: activationTime,
|
|
1898
|
+
});
|
|
1899
|
+
if (await not(self.params.risk.checkSignal({
|
|
1900
|
+
symbol: self.params.execution.context.symbol,
|
|
1901
|
+
strategyName: self.params.method.context.strategyName,
|
|
1902
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1903
|
+
currentPrice: scheduled.priceOpen,
|
|
1904
|
+
timestamp: activationTime,
|
|
1905
|
+
}))) {
|
|
1906
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
|
|
1907
|
+
symbol: self.params.execution.context.symbol,
|
|
1908
|
+
signalId: scheduled.id,
|
|
1909
|
+
});
|
|
1910
|
+
self._scheduledSignal = null;
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
self._scheduledSignal = null;
|
|
1914
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
|
|
1915
|
+
const activatedSignal = {
|
|
1916
|
+
...scheduled,
|
|
1917
|
+
pendingAt: activationTime,
|
|
1918
|
+
_isScheduled: false,
|
|
1919
|
+
};
|
|
1920
|
+
await self.setPendingSignal(activatedSignal);
|
|
1921
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1922
|
+
strategyName: self.params.method.context.strategyName,
|
|
1923
|
+
riskName: self.params.riskName,
|
|
1924
|
+
});
|
|
1925
|
+
if (self.params.callbacks?.onOpen) {
|
|
1926
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
|
|
1927
|
+
}
|
|
1928
|
+
return true;
|
|
1929
|
+
};
|
|
1930
|
+
const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
|
|
1931
|
+
const pnl = toProfitLossDto(signal, averagePrice);
|
|
1932
|
+
self.params.logger.debug(`ClientStrategy backtest ${closeReason}`, {
|
|
1933
|
+
symbol: self.params.execution.context.symbol,
|
|
1934
|
+
signalId: signal.id,
|
|
1935
|
+
reason: closeReason,
|
|
1936
|
+
priceClose: averagePrice,
|
|
1937
|
+
closeTimestamp,
|
|
1938
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
1939
|
+
});
|
|
1940
|
+
if (closeReason === "stop_loss") {
|
|
1941
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1942
|
+
}
|
|
1943
|
+
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1944
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1945
|
+
}
|
|
1946
|
+
if (self.params.callbacks?.onClose) {
|
|
1947
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
1948
|
+
}
|
|
1949
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1950
|
+
strategyName: self.params.method.context.strategyName,
|
|
1951
|
+
riskName: self.params.riskName,
|
|
1952
|
+
});
|
|
1953
|
+
await self.setPendingSignal(null);
|
|
1954
|
+
const result = {
|
|
1955
|
+
action: "closed",
|
|
1956
|
+
signal: signal,
|
|
1957
|
+
currentPrice: averagePrice,
|
|
1958
|
+
closeReason: closeReason,
|
|
1959
|
+
closeTimestamp: closeTimestamp,
|
|
1960
|
+
pnl: pnl,
|
|
1961
|
+
strategyName: self.params.method.context.strategyName,
|
|
1962
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1963
|
+
symbol: self.params.execution.context.symbol,
|
|
1964
|
+
};
|
|
1965
|
+
if (self.params.callbacks?.onTick) {
|
|
1966
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1967
|
+
}
|
|
1968
|
+
return result;
|
|
1969
|
+
};
|
|
1970
|
+
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
1971
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
1972
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
1973
|
+
for (let i = 0; i < candles.length; i++) {
|
|
1974
|
+
const candle = candles[i];
|
|
1975
|
+
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
1976
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
1977
|
+
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
1978
|
+
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
1979
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
1980
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
1981
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
1982
|
+
}
|
|
1983
|
+
let shouldActivate = false;
|
|
1984
|
+
let shouldCancel = false;
|
|
1985
|
+
if (scheduled.position === "long") {
|
|
1986
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1987
|
+
// Отмена если цена упала СЛИШКОМ низко (ниже SL)
|
|
1988
|
+
if (candle.low <= scheduled.priceStopLoss) {
|
|
1989
|
+
shouldCancel = true;
|
|
1990
|
+
}
|
|
1991
|
+
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1992
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1993
|
+
else if (candle.low <= scheduled.priceOpen) {
|
|
1994
|
+
shouldActivate = true;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (scheduled.position === "short") {
|
|
1998
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1999
|
+
// Отмена если цена выросла СЛИШКОМ высоко (выше SL)
|
|
2000
|
+
if (candle.high >= scheduled.priceStopLoss) {
|
|
2001
|
+
shouldCancel = true;
|
|
2002
|
+
}
|
|
2003
|
+
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
2004
|
+
// Активируем только если НЕ пробит StopLoss
|
|
2005
|
+
else if (candle.high >= scheduled.priceOpen) {
|
|
2006
|
+
shouldActivate = true;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (shouldCancel) {
|
|
2010
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
2011
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
2012
|
+
}
|
|
2013
|
+
if (shouldActivate) {
|
|
2014
|
+
await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
|
|
2015
|
+
return {
|
|
2016
|
+
activated: true,
|
|
2017
|
+
cancelled: false,
|
|
2018
|
+
activationIndex: i,
|
|
2019
|
+
result: null,
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
activated: false,
|
|
2025
|
+
cancelled: false,
|
|
2026
|
+
activationIndex: -1,
|
|
2027
|
+
result: null,
|
|
2028
|
+
};
|
|
2029
|
+
};
|
|
2030
|
+
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2031
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2032
|
+
for (let i = candlesCount - 1; i < candles.length; i++) {
|
|
2033
|
+
const recentCandles = candles.slice(i - (candlesCount - 1), i + 1);
|
|
2034
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2035
|
+
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2036
|
+
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2037
|
+
let shouldClose = false;
|
|
2038
|
+
let closeReason;
|
|
2039
|
+
// Check time expiration FIRST (КРИТИЧНО!)
|
|
2040
|
+
const signalTime = signal.pendingAt;
|
|
2041
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
2042
|
+
const elapsedTime = currentCandleTimestamp - signalTime;
|
|
2043
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
2044
|
+
shouldClose = true;
|
|
2045
|
+
closeReason = "time_expired";
|
|
2046
|
+
}
|
|
2047
|
+
// Check TP/SL only if not expired
|
|
2048
|
+
// КРИТИЧНО: используем candle.high/low для точной проверки достижения TP/SL
|
|
2049
|
+
if (!shouldClose && signal.position === "long") {
|
|
2050
|
+
// Для LONG: TP срабатывает если high >= TP, SL если low <= SL
|
|
2051
|
+
if (currentCandle.high >= signal.priceTakeProfit) {
|
|
2052
|
+
shouldClose = true;
|
|
2053
|
+
closeReason = "take_profit";
|
|
2054
|
+
}
|
|
2055
|
+
else if (currentCandle.low <= signal.priceStopLoss) {
|
|
2056
|
+
shouldClose = true;
|
|
2057
|
+
closeReason = "stop_loss";
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (!shouldClose && signal.position === "short") {
|
|
2061
|
+
// Для SHORT: TP срабатывает если low <= TP, SL если high >= SL
|
|
2062
|
+
if (currentCandle.low <= signal.priceTakeProfit) {
|
|
2063
|
+
shouldClose = true;
|
|
2064
|
+
closeReason = "take_profit";
|
|
2065
|
+
}
|
|
2066
|
+
else if (currentCandle.high >= signal.priceStopLoss) {
|
|
2067
|
+
shouldClose = true;
|
|
2068
|
+
closeReason = "stop_loss";
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (shouldClose) {
|
|
2072
|
+
// КРИТИЧНО: при закрытии по TP/SL используем точную цену, а не averagePrice
|
|
2073
|
+
let closePrice = averagePrice;
|
|
2074
|
+
if (closeReason === "take_profit") {
|
|
2075
|
+
closePrice = signal.priceTakeProfit;
|
|
2076
|
+
}
|
|
2077
|
+
else if (closeReason === "stop_loss") {
|
|
2078
|
+
closePrice = signal.priceStopLoss;
|
|
2079
|
+
}
|
|
2080
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return null;
|
|
1420
2084
|
};
|
|
1421
2085
|
/**
|
|
1422
2086
|
* Client implementation for trading strategy lifecycle management.
|
|
@@ -1450,6 +2114,7 @@ class ClientStrategy {
|
|
|
1450
2114
|
this.params = params;
|
|
1451
2115
|
this._isStopped = false;
|
|
1452
2116
|
this._pendingSignal = null;
|
|
2117
|
+
this._scheduledSignal = null;
|
|
1453
2118
|
this._lastSignalTimestamp = null;
|
|
1454
2119
|
/**
|
|
1455
2120
|
* Initializes strategy state by loading persisted signal from disk.
|
|
@@ -1484,14 +2149,21 @@ class ClientStrategy {
|
|
|
1484
2149
|
/**
|
|
1485
2150
|
* Performs a single tick of strategy execution.
|
|
1486
2151
|
*
|
|
1487
|
-
* Flow:
|
|
1488
|
-
* 1. If
|
|
1489
|
-
* 2. If signal
|
|
1490
|
-
* 3. If
|
|
1491
|
-
* 4. If
|
|
2152
|
+
* Flow (LIVE mode):
|
|
2153
|
+
* 1. If scheduled signal exists: check activation/cancellation
|
|
2154
|
+
* 2. If no pending/scheduled signal: call getSignal with throttling and validation
|
|
2155
|
+
* 3. If signal opened: trigger onOpen callback, persist state
|
|
2156
|
+
* 4. If pending signal exists: check VWAP against TP/SL
|
|
2157
|
+
* 5. If TP/SL/time reached: close signal, trigger onClose, persist state
|
|
2158
|
+
*
|
|
2159
|
+
* Flow (BACKTEST mode):
|
|
2160
|
+
* 1. If no pending/scheduled signal: call getSignal
|
|
2161
|
+
* 2. If scheduled signal created: return "scheduled" (backtest() will handle it)
|
|
2162
|
+
* 3. Otherwise same as LIVE
|
|
1492
2163
|
*
|
|
1493
2164
|
* @returns Promise resolving to discriminated union result:
|
|
1494
2165
|
* - idle: No signal generated
|
|
2166
|
+
* - scheduled: Scheduled signal created (backtest only)
|
|
1495
2167
|
* - opened: New signal just created
|
|
1496
2168
|
* - active: Signal monitoring in progress
|
|
1497
2169
|
* - closed: Signal completed with PNL
|
|
@@ -1506,301 +2178,195 @@ class ClientStrategy {
|
|
|
1506
2178
|
*/
|
|
1507
2179
|
async tick() {
|
|
1508
2180
|
this.params.logger.debug("ClientStrategy tick");
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
// Register signal with risk management
|
|
1514
|
-
await this.params.risk.addSignal(this.params.execution.context.symbol, {
|
|
1515
|
-
strategyName: this.params.method.context.strategyName,
|
|
1516
|
-
riskName: this.params.riskName,
|
|
1517
|
-
});
|
|
1518
|
-
if (this.params.callbacks?.onOpen) {
|
|
1519
|
-
this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
|
|
1520
|
-
}
|
|
1521
|
-
const result = {
|
|
1522
|
-
action: "opened",
|
|
1523
|
-
signal: this._pendingSignal,
|
|
1524
|
-
strategyName: this.params.method.context.strategyName,
|
|
1525
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1526
|
-
symbol: this.params.execution.context.symbol,
|
|
1527
|
-
currentPrice: this._pendingSignal.priceOpen,
|
|
1528
|
-
};
|
|
1529
|
-
if (this.params.callbacks?.onTick) {
|
|
1530
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1531
|
-
}
|
|
1532
|
-
return result;
|
|
1533
|
-
}
|
|
2181
|
+
// Получаем текущее время в начале tick для консистентности
|
|
2182
|
+
const currentTime = this.params.execution.context.when.getTime();
|
|
2183
|
+
// Early return if strategy was stopped
|
|
2184
|
+
if (this._isStopped) {
|
|
1534
2185
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1535
|
-
|
|
1536
|
-
this.params.callbacks.onIdle(this.params.execution.context.symbol, currentPrice, this.params.execution.context.backtest);
|
|
1537
|
-
}
|
|
1538
|
-
const result = {
|
|
1539
|
-
action: "idle",
|
|
1540
|
-
signal: null,
|
|
1541
|
-
strategyName: this.params.method.context.strategyName,
|
|
1542
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1543
|
-
symbol: this.params.execution.context.symbol,
|
|
1544
|
-
currentPrice,
|
|
1545
|
-
};
|
|
1546
|
-
if (this.params.callbacks?.onTick) {
|
|
1547
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1548
|
-
}
|
|
1549
|
-
return result;
|
|
1550
|
-
}
|
|
1551
|
-
const when = this.params.execution.context.when;
|
|
1552
|
-
const signal = this._pendingSignal;
|
|
1553
|
-
// Получаем среднюю цену
|
|
1554
|
-
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1555
|
-
this.params.logger.debug("ClientStrategy tick check", {
|
|
1556
|
-
symbol: this.params.execution.context.symbol,
|
|
1557
|
-
averagePrice,
|
|
1558
|
-
signalId: signal.id,
|
|
1559
|
-
position: signal.position,
|
|
1560
|
-
});
|
|
1561
|
-
let shouldClose = false;
|
|
1562
|
-
let closeReason;
|
|
1563
|
-
// Проверяем истечение времени
|
|
1564
|
-
const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
|
|
1565
|
-
if (when.getTime() >= signalEndTime) {
|
|
1566
|
-
shouldClose = true;
|
|
1567
|
-
closeReason = "time_expired";
|
|
2186
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1568
2187
|
}
|
|
1569
|
-
//
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
2188
|
+
// Monitor scheduled signal
|
|
2189
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2190
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2191
|
+
// Check timeout
|
|
2192
|
+
const timeoutResult = await CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN(this, this._scheduledSignal, currentPrice);
|
|
2193
|
+
if (timeoutResult)
|
|
2194
|
+
return timeoutResult;
|
|
2195
|
+
// Check price-based activation/cancellation
|
|
2196
|
+
const { shouldActivate, shouldCancel } = CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN(this._scheduledSignal, currentPrice);
|
|
2197
|
+
if (shouldCancel) {
|
|
2198
|
+
return await CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN(this, this._scheduledSignal, currentPrice);
|
|
1574
2199
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
2200
|
+
if (shouldActivate) {
|
|
2201
|
+
const activateResult = await ACTIVATE_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal, currentTime);
|
|
2202
|
+
if (activateResult) {
|
|
2203
|
+
return activateResult;
|
|
2204
|
+
}
|
|
2205
|
+
// Risk rejected or stopped - return idle
|
|
2206
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1578
2207
|
}
|
|
2208
|
+
return await RETURN_SCHEDULED_SIGNAL_ACTIVE_FN(this, this._scheduledSignal, currentPrice);
|
|
1579
2209
|
}
|
|
1580
|
-
//
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
2210
|
+
// Generate new signal if none exists
|
|
2211
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2212
|
+
const signal = await GET_SIGNAL_FN(this);
|
|
2213
|
+
if (signal) {
|
|
2214
|
+
if (signal._isScheduled === true) {
|
|
2215
|
+
this._scheduledSignal = signal;
|
|
2216
|
+
return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
|
|
2217
|
+
}
|
|
2218
|
+
await this.setPendingSignal(signal);
|
|
1585
2219
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2220
|
+
if (this._pendingSignal) {
|
|
2221
|
+
const openResult = await OPEN_NEW_PENDING_SIGNAL_FN(this, this._pendingSignal);
|
|
2222
|
+
if (openResult) {
|
|
2223
|
+
return openResult;
|
|
2224
|
+
}
|
|
2225
|
+
// Risk rejected - clear pending signal and return idle
|
|
2226
|
+
await this.setPendingSignal(null);
|
|
1589
2227
|
}
|
|
2228
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2229
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1590
2230
|
}
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
if (closeReason === "stop_loss") {
|
|
1597
|
-
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1598
|
-
}
|
|
1599
|
-
// Предупреждение о закрытии сигнала в убыток
|
|
1600
|
-
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1601
|
-
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1602
|
-
}
|
|
1603
|
-
this.params.logger.debug("ClientStrategy closing", {
|
|
1604
|
-
symbol: this.params.execution.context.symbol,
|
|
1605
|
-
signalId: signal.id,
|
|
1606
|
-
reason: closeReason,
|
|
1607
|
-
priceClose: averagePrice,
|
|
1608
|
-
closeTimestamp,
|
|
1609
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1610
|
-
});
|
|
1611
|
-
if (this.params.callbacks?.onClose) {
|
|
1612
|
-
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1613
|
-
}
|
|
1614
|
-
// Remove signal from risk management
|
|
1615
|
-
await this.params.risk.removeSignal(this.params.execution.context.symbol, {
|
|
1616
|
-
strategyName: this.params.method.context.strategyName,
|
|
1617
|
-
riskName: this.params.riskName,
|
|
1618
|
-
});
|
|
1619
|
-
await this.setPendingSignal(null);
|
|
1620
|
-
const result = {
|
|
1621
|
-
action: "closed",
|
|
1622
|
-
signal: signal,
|
|
1623
|
-
currentPrice: averagePrice,
|
|
1624
|
-
closeReason: closeReason,
|
|
1625
|
-
closeTimestamp: closeTimestamp,
|
|
1626
|
-
pnl: pnl,
|
|
1627
|
-
strategyName: this.params.method.context.strategyName,
|
|
1628
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1629
|
-
symbol: this.params.execution.context.symbol,
|
|
1630
|
-
};
|
|
1631
|
-
if (this.params.callbacks?.onTick) {
|
|
1632
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1633
|
-
}
|
|
1634
|
-
return result;
|
|
1635
|
-
}
|
|
1636
|
-
if (this.params.callbacks?.onActive) {
|
|
1637
|
-
this.params.callbacks.onActive(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1638
|
-
}
|
|
1639
|
-
const result = {
|
|
1640
|
-
action: "active",
|
|
1641
|
-
signal: signal,
|
|
1642
|
-
currentPrice: averagePrice,
|
|
1643
|
-
strategyName: this.params.method.context.strategyName,
|
|
1644
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1645
|
-
symbol: this.params.execution.context.symbol,
|
|
1646
|
-
};
|
|
1647
|
-
if (this.params.callbacks?.onTick) {
|
|
1648
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2231
|
+
// Monitor pending signal
|
|
2232
|
+
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2233
|
+
const closedResult = await CHECK_PENDING_SIGNAL_COMPLETION_FN(this, this._pendingSignal, averagePrice);
|
|
2234
|
+
if (closedResult) {
|
|
2235
|
+
return closedResult;
|
|
1649
2236
|
}
|
|
1650
|
-
return
|
|
2237
|
+
return await RETURN_PENDING_SIGNAL_ACTIVE_FN(this, this._pendingSignal, averagePrice);
|
|
1651
2238
|
}
|
|
1652
2239
|
/**
|
|
1653
|
-
* Fast backtests a
|
|
2240
|
+
* Fast backtests a signal using historical candle data.
|
|
1654
2241
|
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
2242
|
+
* For scheduled signals:
|
|
2243
|
+
* 1. Iterates through candles checking for activation (price reaches priceOpen)
|
|
2244
|
+
* 2. Or cancellation (price hits StopLoss before activation)
|
|
2245
|
+
* 3. If activated: converts to pending signal and continues with TP/SL monitoring
|
|
2246
|
+
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
1658
2247
|
*
|
|
1659
|
-
*
|
|
2248
|
+
* For pending signals:
|
|
2249
|
+
* 1. Iterates through candles checking VWAP against TP/SL on each timeframe
|
|
2250
|
+
* 2. Starts from index 4 (needs 5 candles for VWAP calculation)
|
|
2251
|
+
* 3. Returns closed result (either TP/SL or time_expired)
|
|
2252
|
+
*
|
|
2253
|
+
* @param candles - Array of candles to process
|
|
1660
2254
|
* @returns Promise resolving to closed signal result with PNL
|
|
1661
|
-
* @throws Error if no pending signal or not in backtest mode
|
|
2255
|
+
* @throws Error if no pending/scheduled signal or not in backtest mode
|
|
1662
2256
|
*
|
|
1663
2257
|
* @example
|
|
1664
2258
|
* ```typescript
|
|
1665
2259
|
* // After signal opened in backtest
|
|
1666
2260
|
* const candles = await exchange.getNextCandles("BTCUSDT", "1m", signal.minuteEstimatedTime);
|
|
1667
2261
|
* const result = await strategy.backtest(candles);
|
|
1668
|
-
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
|
|
2262
|
+
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
|
|
1669
2263
|
* ```
|
|
1670
2264
|
*/
|
|
1671
2265
|
async backtest(candles) {
|
|
1672
2266
|
this.params.logger.debug("ClientStrategy backtest", {
|
|
1673
2267
|
symbol: this.params.execution.context.symbol,
|
|
1674
2268
|
candlesCount: candles.length,
|
|
2269
|
+
hasScheduled: !!this._scheduledSignal,
|
|
2270
|
+
hasPending: !!this._pendingSignal,
|
|
1675
2271
|
});
|
|
1676
|
-
const signal = this._pendingSignal;
|
|
1677
|
-
if (!signal) {
|
|
1678
|
-
throw new Error("ClientStrategy backtest: no pending signal");
|
|
1679
|
-
}
|
|
1680
2272
|
if (!this.params.execution.context.backtest) {
|
|
1681
2273
|
throw new Error("ClientStrategy backtest: running in live context");
|
|
1682
2274
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (
|
|
1697
|
-
|
|
1698
|
-
shouldClose = true;
|
|
1699
|
-
closeReason = "take_profit";
|
|
1700
|
-
}
|
|
1701
|
-
else if (averagePrice <= signal.priceStopLoss) {
|
|
1702
|
-
shouldClose = true;
|
|
1703
|
-
closeReason = "stop_loss";
|
|
1704
|
-
}
|
|
2275
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2276
|
+
throw new Error("ClientStrategy backtest: no pending or scheduled signal");
|
|
2277
|
+
}
|
|
2278
|
+
// Process scheduled signal
|
|
2279
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2280
|
+
const scheduled = this._scheduledSignal;
|
|
2281
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal", {
|
|
2282
|
+
symbol: this.params.execution.context.symbol,
|
|
2283
|
+
signalId: scheduled.id,
|
|
2284
|
+
priceOpen: scheduled.priceOpen,
|
|
2285
|
+
position: scheduled.position,
|
|
2286
|
+
});
|
|
2287
|
+
const { activated, cancelled, activationIndex, result } = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
2288
|
+
if (cancelled && result) {
|
|
2289
|
+
return result;
|
|
1705
2290
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
if (
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
closeReason = "stop_loss";
|
|
2291
|
+
if (activated) {
|
|
2292
|
+
const remainingCandles = candles.slice(activationIndex + 1);
|
|
2293
|
+
if (remainingCandles.length === 0) {
|
|
2294
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2295
|
+
const recentCandles = candles.slice(Math.max(0, activationIndex - (candlesCount - 1)), activationIndex + 1);
|
|
2296
|
+
const lastPrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2297
|
+
const closeTimestamp = candles[activationIndex].timestamp;
|
|
2298
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, "time_expired", closeTimestamp);
|
|
1715
2299
|
}
|
|
2300
|
+
candles = remainingCandles;
|
|
1716
2301
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2302
|
+
if (this._scheduledSignal) {
|
|
2303
|
+
// Check if timeout reached (CC_SCHEDULE_AWAIT_MINUTES from scheduledAt)
|
|
2304
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2305
|
+
const lastCandleTimestamp = candles[candles.length - 1].timestamp;
|
|
2306
|
+
const elapsedTime = lastCandleTimestamp - scheduled.scheduledAt;
|
|
2307
|
+
if (elapsedTime < maxTimeToWait) {
|
|
2308
|
+
// Timeout NOT reached yet - signal is still active (waiting for price)
|
|
2309
|
+
// Return active result to continue monitoring in next backtest() call
|
|
2310
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2311
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2312
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2313
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal still waiting (not expired)", {
|
|
2314
|
+
symbol: this.params.execution.context.symbol,
|
|
2315
|
+
signalId: scheduled.id,
|
|
2316
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2317
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2318
|
+
});
|
|
2319
|
+
// Don't cancel - just return last active state
|
|
2320
|
+
// In real backtest flow this won't happen as we process all candles at once,
|
|
2321
|
+
// but this is correct behavior if someone calls backtest() with partial data
|
|
2322
|
+
const result = {
|
|
2323
|
+
action: "active",
|
|
2324
|
+
signal: scheduled,
|
|
2325
|
+
currentPrice: lastPrice,
|
|
2326
|
+
strategyName: this.params.method.context.strategyName,
|
|
2327
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
2328
|
+
symbol: this.params.execution.context.symbol,
|
|
2329
|
+
};
|
|
2330
|
+
return result; // Cast to IStrategyBacktestResult (which includes Active)
|
|
1732
2331
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
strategyName: this.params.method.context.strategyName,
|
|
1739
|
-
riskName: this.params.riskName,
|
|
1740
|
-
});
|
|
1741
|
-
await this.setPendingSignal(null);
|
|
1742
|
-
const result = {
|
|
1743
|
-
action: "closed",
|
|
1744
|
-
signal: signal,
|
|
1745
|
-
currentPrice: averagePrice,
|
|
1746
|
-
closeReason: closeReason,
|
|
1747
|
-
closeTimestamp: closeTimestamp,
|
|
1748
|
-
pnl: pnl,
|
|
1749
|
-
strategyName: this.params.method.context.strategyName,
|
|
1750
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
2332
|
+
// Timeout reached - cancel the scheduled signal
|
|
2333
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2334
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2335
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2336
|
+
this.params.logger.info("ClientStrategy backtest scheduled signal cancelled by timeout", {
|
|
1751
2337
|
symbol: this.params.execution.context.symbol,
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2338
|
+
signalId: scheduled.id,
|
|
2339
|
+
closeTimestamp: lastCandleTimestamp,
|
|
2340
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2341
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2342
|
+
reason: "timeout - price never reached priceOpen",
|
|
2343
|
+
});
|
|
2344
|
+
return await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, lastCandleTimestamp);
|
|
1757
2345
|
}
|
|
1758
2346
|
}
|
|
1759
|
-
//
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
const pnl = toProfitLossDto(signal, lastPrice);
|
|
1764
|
-
this.params.logger.debug("ClientStrategy backtest time_expired", {
|
|
1765
|
-
symbol: this.params.execution.context.symbol,
|
|
1766
|
-
signalId: signal.id,
|
|
1767
|
-
priceClose: lastPrice,
|
|
1768
|
-
closeTimestamp,
|
|
1769
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1770
|
-
});
|
|
1771
|
-
// Предупреждение при убытке от time_expired
|
|
1772
|
-
if (pnl.pnlPercentage < 0) {
|
|
1773
|
-
this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2347
|
+
// Process pending signal
|
|
2348
|
+
const signal = this._pendingSignal;
|
|
2349
|
+
if (!signal) {
|
|
2350
|
+
throw new Error("ClientStrategy backtest: no pending signal after scheduled activation");
|
|
1774
2351
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
2352
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2353
|
+
if (candles.length < candlesCount) {
|
|
2354
|
+
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
1777
2355
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
riskName: this.params.riskName,
|
|
1782
|
-
});
|
|
1783
|
-
await this.setPendingSignal(null);
|
|
1784
|
-
const result = {
|
|
1785
|
-
action: "closed",
|
|
1786
|
-
signal: signal,
|
|
1787
|
-
currentPrice: lastPrice,
|
|
1788
|
-
closeReason: "time_expired",
|
|
1789
|
-
closeTimestamp: closeTimestamp,
|
|
1790
|
-
pnl: pnl,
|
|
1791
|
-
strategyName: this.params.method.context.strategyName,
|
|
1792
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1793
|
-
symbol: this.params.execution.context.symbol,
|
|
1794
|
-
};
|
|
1795
|
-
if (this.params.callbacks?.onTick) {
|
|
1796
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2356
|
+
const closedResult = await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
2357
|
+
if (closedResult) {
|
|
2358
|
+
return closedResult;
|
|
1797
2359
|
}
|
|
1798
|
-
|
|
2360
|
+
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
2361
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2362
|
+
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
2363
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
|
|
1799
2364
|
}
|
|
1800
2365
|
/**
|
|
1801
2366
|
* Stops the strategy from generating new signals.
|
|
1802
2367
|
*
|
|
1803
2368
|
* Sets internal flag to prevent getSignal from being called.
|
|
2369
|
+
* Clears any scheduled signals (not yet activated).
|
|
1804
2370
|
* Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
|
|
1805
2371
|
*
|
|
1806
2372
|
* Use case: Graceful shutdown in live trading without forcing position closure.
|
|
@@ -1814,12 +2380,16 @@ class ClientStrategy {
|
|
|
1814
2380
|
* // Existing signal will continue until natural close
|
|
1815
2381
|
* ```
|
|
1816
2382
|
*/
|
|
1817
|
-
stop() {
|
|
2383
|
+
async stop() {
|
|
1818
2384
|
this.params.logger.debug("ClientStrategy stop", {
|
|
1819
2385
|
hasPendingSignal: this._pendingSignal !== null,
|
|
2386
|
+
hasScheduledSignal: this._scheduledSignal !== null,
|
|
1820
2387
|
});
|
|
1821
2388
|
this._isStopped = true;
|
|
1822
|
-
|
|
2389
|
+
// Clear scheduled signal if exists
|
|
2390
|
+
if (this._scheduledSignal) {
|
|
2391
|
+
this._scheduledSignal = null;
|
|
2392
|
+
}
|
|
1823
2393
|
}
|
|
1824
2394
|
}
|
|
1825
2395
|
|
|
@@ -2092,9 +2662,10 @@ class FrameConnectionService {
|
|
|
2092
2662
|
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2093
2663
|
* @returns Promise resolving to { startDate: Date, endDate: Date }
|
|
2094
2664
|
*/
|
|
2095
|
-
this.getTimeframe = async (symbol) => {
|
|
2665
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
2096
2666
|
this.loggerService.log("frameConnectionService getTimeframe", {
|
|
2097
2667
|
symbol,
|
|
2668
|
+
frameName,
|
|
2098
2669
|
});
|
|
2099
2670
|
return await this.getFrame(this.methodContextService.context.frameName).getTimeframe(symbol);
|
|
2100
2671
|
};
|
|
@@ -2575,9 +3146,21 @@ class RiskConnectionService {
|
|
|
2575
3146
|
});
|
|
2576
3147
|
await this.getRisk(context.riskName).removeSignal(symbol, context);
|
|
2577
3148
|
};
|
|
3149
|
+
/**
|
|
3150
|
+
* Clears the cached ClientRisk instance for the given risk name.
|
|
3151
|
+
*
|
|
3152
|
+
* @param riskName - Name of the risk schema to clear from cache
|
|
3153
|
+
*/
|
|
3154
|
+
this.clear = async (riskName) => {
|
|
3155
|
+
this.loggerService.log("riskConnectionService clear", {
|
|
3156
|
+
riskName,
|
|
3157
|
+
});
|
|
3158
|
+
this.getRisk.clear(riskName);
|
|
3159
|
+
};
|
|
2578
3160
|
}
|
|
2579
3161
|
}
|
|
2580
3162
|
|
|
3163
|
+
const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
|
|
2581
3164
|
/**
|
|
2582
3165
|
* Global service for exchange operations with execution context injection.
|
|
2583
3166
|
*
|
|
@@ -2590,6 +3173,21 @@ class ExchangeGlobalService {
|
|
|
2590
3173
|
constructor() {
|
|
2591
3174
|
this.loggerService = inject(TYPES.loggerService);
|
|
2592
3175
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
3176
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3177
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
3178
|
+
/**
|
|
3179
|
+
* Validates exchange configuration.
|
|
3180
|
+
* Memoized to avoid redundant validations for the same exchange.
|
|
3181
|
+
* Logs validation activity.
|
|
3182
|
+
* @param exchangeName - Name of the exchange to validate
|
|
3183
|
+
* @returns Promise that resolves when validation is complete
|
|
3184
|
+
*/
|
|
3185
|
+
this.validate = memoize(([exchangeName]) => `${exchangeName}`, async (exchangeName) => {
|
|
3186
|
+
this.loggerService.log(METHOD_NAME_VALIDATE$1, {
|
|
3187
|
+
exchangeName,
|
|
3188
|
+
});
|
|
3189
|
+
this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$1);
|
|
3190
|
+
});
|
|
2593
3191
|
/**
|
|
2594
3192
|
* Fetches historical candles with execution context.
|
|
2595
3193
|
*
|
|
@@ -2608,6 +3206,7 @@ class ExchangeGlobalService {
|
|
|
2608
3206
|
when,
|
|
2609
3207
|
backtest,
|
|
2610
3208
|
});
|
|
3209
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2611
3210
|
return await ExecutionContextService.runInContext(async () => {
|
|
2612
3211
|
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
2613
3212
|
}, {
|
|
@@ -2634,6 +3233,7 @@ class ExchangeGlobalService {
|
|
|
2634
3233
|
when,
|
|
2635
3234
|
backtest,
|
|
2636
3235
|
});
|
|
3236
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2637
3237
|
return await ExecutionContextService.runInContext(async () => {
|
|
2638
3238
|
return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
2639
3239
|
}, {
|
|
@@ -2656,6 +3256,7 @@ class ExchangeGlobalService {
|
|
|
2656
3256
|
when,
|
|
2657
3257
|
backtest,
|
|
2658
3258
|
});
|
|
3259
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2659
3260
|
return await ExecutionContextService.runInContext(async () => {
|
|
2660
3261
|
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
2661
3262
|
}, {
|
|
@@ -2680,6 +3281,7 @@ class ExchangeGlobalService {
|
|
|
2680
3281
|
when,
|
|
2681
3282
|
backtest,
|
|
2682
3283
|
});
|
|
3284
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2683
3285
|
return await ExecutionContextService.runInContext(async () => {
|
|
2684
3286
|
return await this.exchangeConnectionService.formatPrice(symbol, price);
|
|
2685
3287
|
}, {
|
|
@@ -2704,6 +3306,7 @@ class ExchangeGlobalService {
|
|
|
2704
3306
|
when,
|
|
2705
3307
|
backtest,
|
|
2706
3308
|
});
|
|
3309
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2707
3310
|
return await ExecutionContextService.runInContext(async () => {
|
|
2708
3311
|
return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
2709
3312
|
}, {
|
|
@@ -2715,6 +3318,7 @@ class ExchangeGlobalService {
|
|
|
2715
3318
|
}
|
|
2716
3319
|
}
|
|
2717
3320
|
|
|
3321
|
+
const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
|
|
2718
3322
|
/**
|
|
2719
3323
|
* Global service for strategy operations with execution context injection.
|
|
2720
3324
|
*
|
|
@@ -2727,6 +3331,28 @@ class StrategyGlobalService {
|
|
|
2727
3331
|
constructor() {
|
|
2728
3332
|
this.loggerService = inject(TYPES.loggerService);
|
|
2729
3333
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
3334
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
3335
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3336
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3337
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3338
|
+
/**
|
|
3339
|
+
* Validates strategy and associated risk configuration.
|
|
3340
|
+
*
|
|
3341
|
+
* Memoized to avoid redundant validations for the same strategy.
|
|
3342
|
+
* Logs validation activity.
|
|
3343
|
+
* @param strategyName - Name of the strategy to validate
|
|
3344
|
+
* @returns Promise that resolves when validation is complete
|
|
3345
|
+
*/
|
|
3346
|
+
this.validate = memoize(([strategyName]) => `${strategyName}`, async (strategyName) => {
|
|
3347
|
+
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
3348
|
+
strategyName,
|
|
3349
|
+
});
|
|
3350
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
3351
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_VALIDATE);
|
|
3352
|
+
const riskName = strategySchema.riskName;
|
|
3353
|
+
riskName &&
|
|
3354
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
|
|
3355
|
+
});
|
|
2730
3356
|
/**
|
|
2731
3357
|
* Checks signal status at a specific timestamp.
|
|
2732
3358
|
*
|
|
@@ -2744,6 +3370,7 @@ class StrategyGlobalService {
|
|
|
2744
3370
|
when,
|
|
2745
3371
|
backtest,
|
|
2746
3372
|
});
|
|
3373
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2747
3374
|
return await ExecutionContextService.runInContext(async () => {
|
|
2748
3375
|
return await this.strategyConnectionService.tick();
|
|
2749
3376
|
}, {
|
|
@@ -2771,6 +3398,7 @@ class StrategyGlobalService {
|
|
|
2771
3398
|
when,
|
|
2772
3399
|
backtest,
|
|
2773
3400
|
});
|
|
3401
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2774
3402
|
return await ExecutionContextService.runInContext(async () => {
|
|
2775
3403
|
return await this.strategyConnectionService.backtest(candles);
|
|
2776
3404
|
}, {
|
|
@@ -2792,6 +3420,7 @@ class StrategyGlobalService {
|
|
|
2792
3420
|
this.loggerService.log("strategyGlobalService stop", {
|
|
2793
3421
|
strategyName,
|
|
2794
3422
|
});
|
|
3423
|
+
await this.validate(strategyName);
|
|
2795
3424
|
return await this.strategyConnectionService.stop(strategyName);
|
|
2796
3425
|
};
|
|
2797
3426
|
/**
|
|
@@ -2806,11 +3435,15 @@ class StrategyGlobalService {
|
|
|
2806
3435
|
this.loggerService.log("strategyGlobalService clear", {
|
|
2807
3436
|
strategyName,
|
|
2808
3437
|
});
|
|
3438
|
+
if (strategyName) {
|
|
3439
|
+
await this.validate(strategyName);
|
|
3440
|
+
}
|
|
2809
3441
|
return await this.strategyConnectionService.clear(strategyName);
|
|
2810
3442
|
};
|
|
2811
3443
|
}
|
|
2812
3444
|
}
|
|
2813
3445
|
|
|
3446
|
+
const METHOD_NAME_GET_TIMEFRAME = "frameGlobalService getTimeframe";
|
|
2814
3447
|
/**
|
|
2815
3448
|
* Global service for frame operations.
|
|
2816
3449
|
*
|
|
@@ -2821,21 +3454,25 @@ class FrameGlobalService {
|
|
|
2821
3454
|
constructor() {
|
|
2822
3455
|
this.loggerService = inject(TYPES.loggerService);
|
|
2823
3456
|
this.frameConnectionService = inject(TYPES.frameConnectionService);
|
|
3457
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
2824
3458
|
/**
|
|
2825
3459
|
* Generates timeframe array for backtest iteration.
|
|
2826
3460
|
*
|
|
2827
|
-
* @param
|
|
3461
|
+
* @param frameName - Target frame name (e.g., "1m", "1h")
|
|
2828
3462
|
* @returns Promise resolving to array of Date objects
|
|
2829
3463
|
*/
|
|
2830
|
-
this.getTimeframe = async (symbol) => {
|
|
2831
|
-
this.loggerService.log(
|
|
3464
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
3465
|
+
this.loggerService.log(METHOD_NAME_GET_TIMEFRAME, {
|
|
3466
|
+
frameName,
|
|
2832
3467
|
symbol,
|
|
2833
3468
|
});
|
|
2834
|
-
|
|
3469
|
+
this.frameValidationService.validate(frameName, METHOD_NAME_GET_TIMEFRAME);
|
|
3470
|
+
return await this.frameConnectionService.getTimeframe(symbol, frameName);
|
|
2835
3471
|
};
|
|
2836
3472
|
}
|
|
2837
3473
|
}
|
|
2838
3474
|
|
|
3475
|
+
const METHOD_NAME_CALCULATE = "sizingGlobalService calculate";
|
|
2839
3476
|
/**
|
|
2840
3477
|
* Global service for sizing operations.
|
|
2841
3478
|
*
|
|
@@ -2846,6 +3483,7 @@ class SizingGlobalService {
|
|
|
2846
3483
|
constructor() {
|
|
2847
3484
|
this.loggerService = inject(TYPES.loggerService);
|
|
2848
3485
|
this.sizingConnectionService = inject(TYPES.sizingConnectionService);
|
|
3486
|
+
this.sizingValidationService = inject(TYPES.sizingValidationService);
|
|
2849
3487
|
/**
|
|
2850
3488
|
* Calculates position size based on risk parameters.
|
|
2851
3489
|
*
|
|
@@ -2854,11 +3492,12 @@ class SizingGlobalService {
|
|
|
2854
3492
|
* @returns Promise resolving to calculated position size
|
|
2855
3493
|
*/
|
|
2856
3494
|
this.calculate = async (params, context) => {
|
|
2857
|
-
this.loggerService.log(
|
|
3495
|
+
this.loggerService.log(METHOD_NAME_CALCULATE, {
|
|
2858
3496
|
symbol: params.symbol,
|
|
2859
3497
|
method: params.method,
|
|
2860
3498
|
context,
|
|
2861
3499
|
});
|
|
3500
|
+
this.sizingValidationService.validate(context.sizingName, METHOD_NAME_CALCULATE);
|
|
2862
3501
|
return await this.sizingConnectionService.calculate(params, context);
|
|
2863
3502
|
};
|
|
2864
3503
|
}
|
|
@@ -2874,6 +3513,20 @@ class RiskGlobalService {
|
|
|
2874
3513
|
constructor() {
|
|
2875
3514
|
this.loggerService = inject(TYPES.loggerService);
|
|
2876
3515
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
3516
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3517
|
+
/**
|
|
3518
|
+
* Validates risk configuration.
|
|
3519
|
+
* Memoized to avoid redundant validations for the same risk instance.
|
|
3520
|
+
* Logs validation activity.
|
|
3521
|
+
* @param riskName - Name of the risk instance to validate
|
|
3522
|
+
* @returns Promise that resolves when validation is complete
|
|
3523
|
+
*/
|
|
3524
|
+
this.validate = memoize(([riskName]) => `${riskName}`, async (riskName) => {
|
|
3525
|
+
this.loggerService.log("riskGlobalService validate", {
|
|
3526
|
+
riskName,
|
|
3527
|
+
});
|
|
3528
|
+
this.riskValidationService.validate(riskName, "riskGlobalService validate");
|
|
3529
|
+
});
|
|
2877
3530
|
/**
|
|
2878
3531
|
* Checks if a signal should be allowed based on risk limits.
|
|
2879
3532
|
*
|
|
@@ -2886,6 +3539,7 @@ class RiskGlobalService {
|
|
|
2886
3539
|
symbol: params.symbol,
|
|
2887
3540
|
context,
|
|
2888
3541
|
});
|
|
3542
|
+
await this.validate(context.riskName);
|
|
2889
3543
|
return await this.riskConnectionService.checkSignal(params, context);
|
|
2890
3544
|
};
|
|
2891
3545
|
/**
|
|
@@ -2899,6 +3553,7 @@ class RiskGlobalService {
|
|
|
2899
3553
|
symbol,
|
|
2900
3554
|
context,
|
|
2901
3555
|
});
|
|
3556
|
+
await this.validate(context.riskName);
|
|
2902
3557
|
await this.riskConnectionService.addSignal(symbol, context);
|
|
2903
3558
|
};
|
|
2904
3559
|
/**
|
|
@@ -2912,8 +3567,24 @@ class RiskGlobalService {
|
|
|
2912
3567
|
symbol,
|
|
2913
3568
|
context,
|
|
2914
3569
|
});
|
|
3570
|
+
await this.validate(context.riskName);
|
|
2915
3571
|
await this.riskConnectionService.removeSignal(symbol, context);
|
|
2916
3572
|
};
|
|
3573
|
+
/**
|
|
3574
|
+
* Clears risk data.
|
|
3575
|
+
* If riskName is provided, clears data for that specific risk instance.
|
|
3576
|
+
* If no riskName is provided, clears all risk data.
|
|
3577
|
+
* @param riskName - Optional name of the risk instance to clear
|
|
3578
|
+
*/
|
|
3579
|
+
this.clear = async (riskName) => {
|
|
3580
|
+
this.loggerService.log("riskGlobalService clear", {
|
|
3581
|
+
riskName,
|
|
3582
|
+
});
|
|
3583
|
+
if (riskName) {
|
|
3584
|
+
await this.validate(riskName);
|
|
3585
|
+
}
|
|
3586
|
+
return await this.riskConnectionService.clear(riskName);
|
|
3587
|
+
};
|
|
2917
3588
|
}
|
|
2918
3589
|
}
|
|
2919
3590
|
|
|
@@ -3424,7 +4095,7 @@ class BacktestLogicPrivateService {
|
|
|
3424
4095
|
symbol,
|
|
3425
4096
|
});
|
|
3426
4097
|
const backtestStartTime = performance.now();
|
|
3427
|
-
const timeframes = await this.frameGlobalService.getTimeframe(symbol);
|
|
4098
|
+
const timeframes = await this.frameGlobalService.getTimeframe(symbol, this.methodContextService.context.frameName);
|
|
3428
4099
|
const totalFrames = timeframes.length;
|
|
3429
4100
|
let i = 0;
|
|
3430
4101
|
let previousEventTimestamp = null;
|
|
@@ -3443,7 +4114,66 @@ class BacktestLogicPrivateService {
|
|
|
3443
4114
|
});
|
|
3444
4115
|
}
|
|
3445
4116
|
const result = await this.strategyGlobalService.tick(symbol, when, true);
|
|
3446
|
-
// Если
|
|
4117
|
+
// Если scheduled signal создан - обрабатываем через backtest()
|
|
4118
|
+
if (result.action === "scheduled") {
|
|
4119
|
+
const signalStartTime = performance.now();
|
|
4120
|
+
const signal = result.signal;
|
|
4121
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
|
|
4122
|
+
symbol,
|
|
4123
|
+
signalId: signal.id,
|
|
4124
|
+
priceOpen: signal.priceOpen,
|
|
4125
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4126
|
+
});
|
|
4127
|
+
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4128
|
+
// КРИТИЧНО: запрашиваем CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4129
|
+
// + minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4130
|
+
// +1 потому что when включается как первая свеча (timestamp начинается с when, а не after when)
|
|
4131
|
+
const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4132
|
+
const candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, when, true);
|
|
4133
|
+
if (!candles.length) {
|
|
4134
|
+
i++;
|
|
4135
|
+
continue;
|
|
4136
|
+
}
|
|
4137
|
+
this.loggerService.info("backtestLogicPrivateService candles fetched for scheduled", {
|
|
4138
|
+
symbol,
|
|
4139
|
+
signalId: signal.id,
|
|
4140
|
+
candlesCount: candles.length,
|
|
4141
|
+
candlesNeeded,
|
|
4142
|
+
});
|
|
4143
|
+
// backtest() сам обработает scheduled signal: найдет активацию/отмену
|
|
4144
|
+
// и если активируется - продолжит с TP/SL мониторингом
|
|
4145
|
+
const backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
|
|
4146
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal closed", {
|
|
4147
|
+
symbol,
|
|
4148
|
+
signalId: backtestResult.signal.id,
|
|
4149
|
+
closeTimestamp: backtestResult.closeTimestamp,
|
|
4150
|
+
action: backtestResult.action,
|
|
4151
|
+
closeReason: backtestResult.action === "closed"
|
|
4152
|
+
? backtestResult.closeReason
|
|
4153
|
+
: undefined,
|
|
4154
|
+
});
|
|
4155
|
+
// Track signal processing duration
|
|
4156
|
+
const signalEndTime = performance.now();
|
|
4157
|
+
const currentTimestamp = Date.now();
|
|
4158
|
+
await performanceEmitter.next({
|
|
4159
|
+
timestamp: currentTimestamp,
|
|
4160
|
+
previousTimestamp: previousEventTimestamp,
|
|
4161
|
+
metricType: "backtest_signal",
|
|
4162
|
+
duration: signalEndTime - signalStartTime,
|
|
4163
|
+
strategyName: this.methodContextService.context.strategyName,
|
|
4164
|
+
exchangeName: this.methodContextService.context.exchangeName,
|
|
4165
|
+
symbol,
|
|
4166
|
+
backtest: true,
|
|
4167
|
+
});
|
|
4168
|
+
previousEventTimestamp = currentTimestamp;
|
|
4169
|
+
// Пропускаем timeframes до closeTimestamp
|
|
4170
|
+
while (i < timeframes.length &&
|
|
4171
|
+
timeframes[i].getTime() < backtestResult.closeTimestamp) {
|
|
4172
|
+
i++;
|
|
4173
|
+
}
|
|
4174
|
+
yield backtestResult;
|
|
4175
|
+
}
|
|
4176
|
+
// Если обычный сигнал открыт, вызываем backtest
|
|
3447
4177
|
if (result.action === "opened") {
|
|
3448
4178
|
const signalStartTime = performance.now();
|
|
3449
4179
|
const signal = result.signal;
|
|
@@ -3468,7 +4198,6 @@ class BacktestLogicPrivateService {
|
|
|
3468
4198
|
symbol,
|
|
3469
4199
|
signalId: backtestResult.signal.id,
|
|
3470
4200
|
closeTimestamp: backtestResult.closeTimestamp,
|
|
3471
|
-
closeReason: backtestResult.closeReason,
|
|
3472
4201
|
});
|
|
3473
4202
|
// Track signal processing duration
|
|
3474
4203
|
const signalEndTime = performance.now();
|
|
@@ -3615,6 +4344,11 @@ class LiveLogicPrivateService {
|
|
|
3615
4344
|
await sleep(TICK_TTL);
|
|
3616
4345
|
continue;
|
|
3617
4346
|
}
|
|
4347
|
+
if (result.action === "scheduled") {
|
|
4348
|
+
await sleep(TICK_TTL);
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
// Yield opened, closed, cancelled results
|
|
3618
4352
|
yield result;
|
|
3619
4353
|
await sleep(TICK_TTL);
|
|
3620
4354
|
}
|
|
@@ -3935,6 +4669,8 @@ class LiveGlobalService {
|
|
|
3935
4669
|
this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
|
|
3936
4670
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3937
4671
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4672
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4673
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3938
4674
|
/**
|
|
3939
4675
|
* Runs live trading for a symbol with context propagation.
|
|
3940
4676
|
*
|
|
@@ -3949,8 +4685,16 @@ class LiveGlobalService {
|
|
|
3949
4685
|
symbol,
|
|
3950
4686
|
context,
|
|
3951
4687
|
});
|
|
3952
|
-
|
|
3953
|
-
|
|
4688
|
+
{
|
|
4689
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
|
|
4690
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
|
|
4691
|
+
}
|
|
4692
|
+
{
|
|
4693
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4694
|
+
const riskName = strategySchema.riskName;
|
|
4695
|
+
riskName &&
|
|
4696
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
|
|
4697
|
+
}
|
|
3954
4698
|
return this.liveLogicPublicService.run(symbol, context);
|
|
3955
4699
|
};
|
|
3956
4700
|
}
|
|
@@ -3966,6 +4710,8 @@ const METHOD_NAME_RUN$1 = "backtestGlobalService run";
|
|
|
3966
4710
|
class BacktestGlobalService {
|
|
3967
4711
|
constructor() {
|
|
3968
4712
|
this.loggerService = inject(TYPES.loggerService);
|
|
4713
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4714
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3969
4715
|
this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
3970
4716
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3971
4717
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
@@ -3982,9 +4728,16 @@ class BacktestGlobalService {
|
|
|
3982
4728
|
symbol,
|
|
3983
4729
|
context,
|
|
3984
4730
|
});
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4731
|
+
{
|
|
4732
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
|
|
4733
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
|
|
4734
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
|
|
4735
|
+
}
|
|
4736
|
+
{
|
|
4737
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4738
|
+
const riskName = strategySchema.riskName;
|
|
4739
|
+
riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
|
|
4740
|
+
}
|
|
3988
4741
|
return this.backtestLogicPublicService.run(symbol, context);
|
|
3989
4742
|
};
|
|
3990
4743
|
}
|
|
@@ -4001,6 +4754,13 @@ class WalkerGlobalService {
|
|
|
4001
4754
|
constructor() {
|
|
4002
4755
|
this.loggerService = inject(TYPES.loggerService);
|
|
4003
4756
|
this.walkerLogicPublicService = inject(TYPES.walkerLogicPublicService);
|
|
4757
|
+
this.walkerSchemaService = inject(TYPES.walkerSchemaService);
|
|
4758
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
4759
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4760
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
4761
|
+
this.walkerValidationService = inject(TYPES.walkerValidationService);
|
|
4762
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4763
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
4004
4764
|
/**
|
|
4005
4765
|
* Runs walker comparison for a symbol with context propagation.
|
|
4006
4766
|
*
|
|
@@ -4012,6 +4772,21 @@ class WalkerGlobalService {
|
|
|
4012
4772
|
symbol,
|
|
4013
4773
|
context,
|
|
4014
4774
|
});
|
|
4775
|
+
{
|
|
4776
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
|
|
4777
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
|
|
4778
|
+
this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
|
|
4779
|
+
}
|
|
4780
|
+
{
|
|
4781
|
+
const walkerSchema = this.walkerSchemaService.get(context.walkerName);
|
|
4782
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
4783
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
4784
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
|
|
4785
|
+
const riskName = strategySchema.riskName;
|
|
4786
|
+
riskName &&
|
|
4787
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4015
4790
|
return this.walkerLogicPublicService.run(symbol, context);
|
|
4016
4791
|
};
|
|
4017
4792
|
}
|
|
@@ -4035,7 +4810,7 @@ function isUnsafe$3(value) {
|
|
|
4035
4810
|
}
|
|
4036
4811
|
return false;
|
|
4037
4812
|
}
|
|
4038
|
-
const columns$
|
|
4813
|
+
const columns$3 = [
|
|
4039
4814
|
{
|
|
4040
4815
|
key: "signalId",
|
|
4041
4816
|
label: "Signal ID",
|
|
@@ -4093,7 +4868,7 @@ const columns$2 = [
|
|
|
4093
4868
|
key: "duration",
|
|
4094
4869
|
label: "Duration (min)",
|
|
4095
4870
|
format: (data) => {
|
|
4096
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
4871
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
4097
4872
|
const durationMin = Math.round(durationMs / 60000);
|
|
4098
4873
|
return `${durationMin}`;
|
|
4099
4874
|
},
|
|
@@ -4101,7 +4876,7 @@ const columns$2 = [
|
|
|
4101
4876
|
{
|
|
4102
4877
|
key: "openTimestamp",
|
|
4103
4878
|
label: "Open Time",
|
|
4104
|
-
format: (data) => new Date(data.signal.
|
|
4879
|
+
format: (data) => new Date(data.signal.pendingAt).toISOString(),
|
|
4105
4880
|
},
|
|
4106
4881
|
{
|
|
4107
4882
|
key: "closeTimestamp",
|
|
@@ -4113,7 +4888,7 @@ const columns$2 = [
|
|
|
4113
4888
|
* Storage class for accumulating closed signals per strategy.
|
|
4114
4889
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
4115
4890
|
*/
|
|
4116
|
-
let ReportStorage$
|
|
4891
|
+
let ReportStorage$3 = class ReportStorage {
|
|
4117
4892
|
constructor() {
|
|
4118
4893
|
/** Internal list of all closed signals for this strategy */
|
|
4119
4894
|
this._signalList = [];
|
|
@@ -4173,7 +4948,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4173
4948
|
: 0;
|
|
4174
4949
|
const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4175
4950
|
// Calculate Expected Yearly Returns
|
|
4176
|
-
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.
|
|
4951
|
+
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / totalSignals;
|
|
4177
4952
|
const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
|
|
4178
4953
|
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4179
4954
|
const expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
@@ -4203,9 +4978,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4203
4978
|
if (stats.totalSignals === 0) {
|
|
4204
4979
|
return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
4205
4980
|
}
|
|
4206
|
-
const header = columns$
|
|
4207
|
-
const separator = columns$
|
|
4208
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
4981
|
+
const header = columns$3.map((col) => col.label);
|
|
4982
|
+
const separator = columns$3.map(() => "---");
|
|
4983
|
+
const rows = this._signalList.map((closedSignal) => columns$3.map((col) => col.format(closedSignal)));
|
|
4209
4984
|
const tableData = [header, separator, ...rows];
|
|
4210
4985
|
const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4211
4986
|
return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
@@ -4266,7 +5041,7 @@ class BacktestMarkdownService {
|
|
|
4266
5041
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
4267
5042
|
* Each strategy gets its own isolated storage instance.
|
|
4268
5043
|
*/
|
|
4269
|
-
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
5044
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
|
|
4270
5045
|
/**
|
|
4271
5046
|
* Processes tick events and accumulates closed signals.
|
|
4272
5047
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -4425,7 +5200,7 @@ function isUnsafe$2(value) {
|
|
|
4425
5200
|
}
|
|
4426
5201
|
return false;
|
|
4427
5202
|
}
|
|
4428
|
-
const columns$
|
|
5203
|
+
const columns$2 = [
|
|
4429
5204
|
{
|
|
4430
5205
|
key: "timestamp",
|
|
4431
5206
|
label: "Timestamp",
|
|
@@ -4499,12 +5274,12 @@ const columns$1 = [
|
|
|
4499
5274
|
},
|
|
4500
5275
|
];
|
|
4501
5276
|
/** Maximum number of events to store in live trading reports */
|
|
4502
|
-
const MAX_EVENTS$
|
|
5277
|
+
const MAX_EVENTS$2 = 250;
|
|
4503
5278
|
/**
|
|
4504
5279
|
* Storage class for accumulating all tick events per strategy.
|
|
4505
5280
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
4506
5281
|
*/
|
|
4507
|
-
let ReportStorage$
|
|
5282
|
+
let ReportStorage$2 = class ReportStorage {
|
|
4508
5283
|
constructor() {
|
|
4509
5284
|
/** Internal list of all tick events for this strategy */
|
|
4510
5285
|
this._eventList = [];
|
|
@@ -4532,7 +5307,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4532
5307
|
}
|
|
4533
5308
|
{
|
|
4534
5309
|
this._eventList.push(newEvent);
|
|
4535
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5310
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4536
5311
|
this._eventList.shift();
|
|
4537
5312
|
}
|
|
4538
5313
|
}
|
|
@@ -4544,7 +5319,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4544
5319
|
*/
|
|
4545
5320
|
addOpenedEvent(data) {
|
|
4546
5321
|
this._eventList.push({
|
|
4547
|
-
timestamp: data.signal.
|
|
5322
|
+
timestamp: data.signal.pendingAt,
|
|
4548
5323
|
action: "opened",
|
|
4549
5324
|
symbol: data.signal.symbol,
|
|
4550
5325
|
signalId: data.signal.id,
|
|
@@ -4556,7 +5331,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4556
5331
|
stopLoss: data.signal.priceStopLoss,
|
|
4557
5332
|
});
|
|
4558
5333
|
// Trim queue if exceeded MAX_EVENTS
|
|
4559
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5334
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4560
5335
|
this._eventList.shift();
|
|
4561
5336
|
}
|
|
4562
5337
|
}
|
|
@@ -4588,35 +5363,472 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4588
5363
|
else {
|
|
4589
5364
|
this._eventList.push(newEvent);
|
|
4590
5365
|
// Trim queue if exceeded MAX_EVENTS
|
|
4591
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5366
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
5367
|
+
this._eventList.shift();
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
/**
|
|
5372
|
+
* Updates or adds a closed event to the storage.
|
|
5373
|
+
* Replaces the previous event with the same signalId.
|
|
5374
|
+
*
|
|
5375
|
+
* @param data - Closed tick result
|
|
5376
|
+
*/
|
|
5377
|
+
addClosedEvent(data) {
|
|
5378
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
5379
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
5380
|
+
// Find existing event with the same signalId
|
|
5381
|
+
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
5382
|
+
const newEvent = {
|
|
5383
|
+
timestamp: data.closeTimestamp,
|
|
5384
|
+
action: "closed",
|
|
5385
|
+
symbol: data.signal.symbol,
|
|
5386
|
+
signalId: data.signal.id,
|
|
5387
|
+
position: data.signal.position,
|
|
5388
|
+
note: data.signal.note,
|
|
5389
|
+
currentPrice: data.currentPrice,
|
|
5390
|
+
openPrice: data.signal.priceOpen,
|
|
5391
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5392
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5393
|
+
pnl: data.pnl.pnlPercentage,
|
|
5394
|
+
closeReason: data.closeReason,
|
|
5395
|
+
duration: durationMin,
|
|
5396
|
+
};
|
|
5397
|
+
// Replace existing event or add new one
|
|
5398
|
+
if (existingIndex !== -1) {
|
|
5399
|
+
this._eventList[existingIndex] = newEvent;
|
|
5400
|
+
}
|
|
5401
|
+
else {
|
|
5402
|
+
this._eventList.push(newEvent);
|
|
5403
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5404
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4592
5405
|
this._eventList.shift();
|
|
4593
5406
|
}
|
|
4594
5407
|
}
|
|
4595
5408
|
}
|
|
4596
5409
|
/**
|
|
4597
|
-
*
|
|
5410
|
+
* Calculates statistical data from live trading events (Controller).
|
|
5411
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5412
|
+
*
|
|
5413
|
+
* @returns Statistical data (empty object if no events)
|
|
5414
|
+
*/
|
|
5415
|
+
async getData() {
|
|
5416
|
+
if (this._eventList.length === 0) {
|
|
5417
|
+
return {
|
|
5418
|
+
eventList: [],
|
|
5419
|
+
totalEvents: 0,
|
|
5420
|
+
totalClosed: 0,
|
|
5421
|
+
winCount: 0,
|
|
5422
|
+
lossCount: 0,
|
|
5423
|
+
winRate: null,
|
|
5424
|
+
avgPnl: null,
|
|
5425
|
+
totalPnl: null,
|
|
5426
|
+
stdDev: null,
|
|
5427
|
+
sharpeRatio: null,
|
|
5428
|
+
annualizedSharpeRatio: null,
|
|
5429
|
+
certaintyRatio: null,
|
|
5430
|
+
expectedYearlyReturns: null,
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
5434
|
+
const totalClosed = closedEvents.length;
|
|
5435
|
+
const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
|
|
5436
|
+
const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
|
|
5437
|
+
// Calculate basic statistics
|
|
5438
|
+
const avgPnl = totalClosed > 0
|
|
5439
|
+
? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
|
|
5440
|
+
: 0;
|
|
5441
|
+
const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
|
|
5442
|
+
const winRate = (winCount / totalClosed) * 100;
|
|
5443
|
+
// Calculate Sharpe Ratio (risk-free rate = 0)
|
|
5444
|
+
let sharpeRatio = 0;
|
|
5445
|
+
let stdDev = 0;
|
|
5446
|
+
if (totalClosed > 0) {
|
|
5447
|
+
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
5448
|
+
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
5449
|
+
stdDev = Math.sqrt(variance);
|
|
5450
|
+
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
5451
|
+
}
|
|
5452
|
+
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
5453
|
+
// Calculate Certainty Ratio
|
|
5454
|
+
let certaintyRatio = 0;
|
|
5455
|
+
if (totalClosed > 0) {
|
|
5456
|
+
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
5457
|
+
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
5458
|
+
const avgWin = wins.length > 0
|
|
5459
|
+
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
5460
|
+
: 0;
|
|
5461
|
+
const avgLoss = losses.length > 0
|
|
5462
|
+
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
5463
|
+
: 0;
|
|
5464
|
+
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
5465
|
+
}
|
|
5466
|
+
// Calculate Expected Yearly Returns
|
|
5467
|
+
let expectedYearlyReturns = 0;
|
|
5468
|
+
if (totalClosed > 0) {
|
|
5469
|
+
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
5470
|
+
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
5471
|
+
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
5472
|
+
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
5473
|
+
}
|
|
5474
|
+
return {
|
|
5475
|
+
eventList: this._eventList,
|
|
5476
|
+
totalEvents: this._eventList.length,
|
|
5477
|
+
totalClosed,
|
|
5478
|
+
winCount,
|
|
5479
|
+
lossCount,
|
|
5480
|
+
winRate: isUnsafe$2(winRate) ? null : winRate,
|
|
5481
|
+
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
5482
|
+
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
5483
|
+
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
5484
|
+
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
5485
|
+
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
5486
|
+
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
5487
|
+
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5490
|
+
/**
|
|
5491
|
+
* Generates markdown report with all tick events for a strategy (View).
|
|
5492
|
+
*
|
|
5493
|
+
* @param strategyName - Strategy name
|
|
5494
|
+
* @returns Markdown formatted report with all events
|
|
5495
|
+
*/
|
|
5496
|
+
async getReport(strategyName) {
|
|
5497
|
+
const stats = await this.getData();
|
|
5498
|
+
if (stats.totalEvents === 0) {
|
|
5499
|
+
return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
5500
|
+
}
|
|
5501
|
+
const header = columns$2.map((col) => col.label);
|
|
5502
|
+
const separator = columns$2.map(() => "---");
|
|
5503
|
+
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
5504
|
+
const tableData = [header, separator, ...rows];
|
|
5505
|
+
const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5506
|
+
return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
5507
|
+
}
|
|
5508
|
+
/**
|
|
5509
|
+
* Saves strategy report to disk.
|
|
5510
|
+
*
|
|
5511
|
+
* @param strategyName - Strategy name
|
|
5512
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5513
|
+
*/
|
|
5514
|
+
async dump(strategyName, path = "./logs/live") {
|
|
5515
|
+
const markdown = await this.getReport(strategyName);
|
|
5516
|
+
try {
|
|
5517
|
+
const dir = join(process.cwd(), path);
|
|
5518
|
+
await mkdir(dir, { recursive: true });
|
|
5519
|
+
const filename = `${strategyName}.md`;
|
|
5520
|
+
const filepath = join(dir, filename);
|
|
5521
|
+
await writeFile(filepath, markdown, "utf-8");
|
|
5522
|
+
console.log(`Live trading report saved: ${filepath}`);
|
|
5523
|
+
}
|
|
5524
|
+
catch (error) {
|
|
5525
|
+
console.error(`Failed to save markdown report:`, error);
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
};
|
|
5529
|
+
/**
|
|
5530
|
+
* Service for generating and saving live trading markdown reports.
|
|
5531
|
+
*
|
|
5532
|
+
* Features:
|
|
5533
|
+
* - Listens to all signal events via onTick callback
|
|
5534
|
+
* - Accumulates all events (idle, opened, active, closed) per strategy
|
|
5535
|
+
* - Generates markdown tables with detailed event information
|
|
5536
|
+
* - Provides trading statistics (win rate, average PNL)
|
|
5537
|
+
* - Saves reports to disk in logs/live/{strategyName}.md
|
|
5538
|
+
*
|
|
5539
|
+
* @example
|
|
5540
|
+
* ```typescript
|
|
5541
|
+
* const service = new LiveMarkdownService();
|
|
5542
|
+
*
|
|
5543
|
+
* // Add to strategy callbacks
|
|
5544
|
+
* addStrategy({
|
|
5545
|
+
* strategyName: "my-strategy",
|
|
5546
|
+
* callbacks: {
|
|
5547
|
+
* onTick: (symbol, result, backtest) => {
|
|
5548
|
+
* if (!backtest) {
|
|
5549
|
+
* service.tick(result);
|
|
5550
|
+
* }
|
|
5551
|
+
* }
|
|
5552
|
+
* }
|
|
5553
|
+
* });
|
|
5554
|
+
*
|
|
5555
|
+
* // Later: generate and save report
|
|
5556
|
+
* await service.dump("my-strategy");
|
|
5557
|
+
* ```
|
|
5558
|
+
*/
|
|
5559
|
+
class LiveMarkdownService {
|
|
5560
|
+
constructor() {
|
|
5561
|
+
/** Logger service for debug output */
|
|
5562
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
5563
|
+
/**
|
|
5564
|
+
* Memoized function to get or create ReportStorage for a strategy.
|
|
5565
|
+
* Each strategy gets its own isolated storage instance.
|
|
5566
|
+
*/
|
|
5567
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
5568
|
+
/**
|
|
5569
|
+
* Processes tick events and accumulates all event types.
|
|
5570
|
+
* Should be called from IStrategyCallbacks.onTick.
|
|
5571
|
+
*
|
|
5572
|
+
* Processes all event types: idle, opened, active, closed.
|
|
5573
|
+
*
|
|
5574
|
+
* @param data - Tick result from strategy execution
|
|
5575
|
+
*
|
|
5576
|
+
* @example
|
|
5577
|
+
* ```typescript
|
|
5578
|
+
* const service = new LiveMarkdownService();
|
|
5579
|
+
*
|
|
5580
|
+
* callbacks: {
|
|
5581
|
+
* onTick: (symbol, result, backtest) => {
|
|
5582
|
+
* if (!backtest) {
|
|
5583
|
+
* service.tick(result);
|
|
5584
|
+
* }
|
|
5585
|
+
* }
|
|
5586
|
+
* }
|
|
5587
|
+
* ```
|
|
5588
|
+
*/
|
|
5589
|
+
this.tick = async (data) => {
|
|
5590
|
+
this.loggerService.log("liveMarkdownService tick", {
|
|
5591
|
+
data,
|
|
5592
|
+
});
|
|
5593
|
+
const storage = this.getStorage(data.strategyName);
|
|
5594
|
+
if (data.action === "idle") {
|
|
5595
|
+
storage.addIdleEvent(data.currentPrice);
|
|
5596
|
+
}
|
|
5597
|
+
else if (data.action === "opened") {
|
|
5598
|
+
storage.addOpenedEvent(data);
|
|
5599
|
+
}
|
|
5600
|
+
else if (data.action === "active") {
|
|
5601
|
+
storage.addActiveEvent(data);
|
|
5602
|
+
}
|
|
5603
|
+
else if (data.action === "closed") {
|
|
5604
|
+
storage.addClosedEvent(data);
|
|
5605
|
+
}
|
|
5606
|
+
};
|
|
5607
|
+
/**
|
|
5608
|
+
* Gets statistical data from all live trading events for a strategy.
|
|
5609
|
+
* Delegates to ReportStorage.getData().
|
|
5610
|
+
*
|
|
5611
|
+
* @param strategyName - Strategy name to get data for
|
|
5612
|
+
* @returns Statistical data object with all metrics
|
|
5613
|
+
*
|
|
5614
|
+
* @example
|
|
5615
|
+
* ```typescript
|
|
5616
|
+
* const service = new LiveMarkdownService();
|
|
5617
|
+
* const stats = await service.getData("my-strategy");
|
|
5618
|
+
* console.log(stats.sharpeRatio, stats.winRate);
|
|
5619
|
+
* ```
|
|
5620
|
+
*/
|
|
5621
|
+
this.getData = async (strategyName) => {
|
|
5622
|
+
this.loggerService.log("liveMarkdownService getData", {
|
|
5623
|
+
strategyName,
|
|
5624
|
+
});
|
|
5625
|
+
const storage = this.getStorage(strategyName);
|
|
5626
|
+
return storage.getData();
|
|
5627
|
+
};
|
|
5628
|
+
/**
|
|
5629
|
+
* Generates markdown report with all events for a strategy.
|
|
5630
|
+
* Delegates to ReportStorage.getReport().
|
|
5631
|
+
*
|
|
5632
|
+
* @param strategyName - Strategy name to generate report for
|
|
5633
|
+
* @returns Markdown formatted report string with table of all events
|
|
5634
|
+
*
|
|
5635
|
+
* @example
|
|
5636
|
+
* ```typescript
|
|
5637
|
+
* const service = new LiveMarkdownService();
|
|
5638
|
+
* const markdown = await service.getReport("my-strategy");
|
|
5639
|
+
* console.log(markdown);
|
|
5640
|
+
* ```
|
|
5641
|
+
*/
|
|
5642
|
+
this.getReport = async (strategyName) => {
|
|
5643
|
+
this.loggerService.log("liveMarkdownService getReport", {
|
|
5644
|
+
strategyName,
|
|
5645
|
+
});
|
|
5646
|
+
const storage = this.getStorage(strategyName);
|
|
5647
|
+
return storage.getReport(strategyName);
|
|
5648
|
+
};
|
|
5649
|
+
/**
|
|
5650
|
+
* Saves strategy report to disk.
|
|
5651
|
+
* Creates directory if it doesn't exist.
|
|
5652
|
+
* Delegates to ReportStorage.dump().
|
|
5653
|
+
*
|
|
5654
|
+
* @param strategyName - Strategy name to save report for
|
|
5655
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5656
|
+
*
|
|
5657
|
+
* @example
|
|
5658
|
+
* ```typescript
|
|
5659
|
+
* const service = new LiveMarkdownService();
|
|
5660
|
+
*
|
|
5661
|
+
* // Save to default path: ./logs/live/my-strategy.md
|
|
5662
|
+
* await service.dump("my-strategy");
|
|
5663
|
+
*
|
|
5664
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5665
|
+
* await service.dump("my-strategy", "./custom/path");
|
|
5666
|
+
* ```
|
|
5667
|
+
*/
|
|
5668
|
+
this.dump = async (strategyName, path = "./logs/live") => {
|
|
5669
|
+
this.loggerService.log("liveMarkdownService dump", {
|
|
5670
|
+
strategyName,
|
|
5671
|
+
path,
|
|
5672
|
+
});
|
|
5673
|
+
const storage = this.getStorage(strategyName);
|
|
5674
|
+
await storage.dump(strategyName, path);
|
|
5675
|
+
};
|
|
5676
|
+
/**
|
|
5677
|
+
* Clears accumulated event data from storage.
|
|
5678
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
5679
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
5680
|
+
*
|
|
5681
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
5682
|
+
*
|
|
5683
|
+
* @example
|
|
5684
|
+
* ```typescript
|
|
5685
|
+
* const service = new LiveMarkdownService();
|
|
5686
|
+
*
|
|
5687
|
+
* // Clear specific strategy data
|
|
5688
|
+
* await service.clear("my-strategy");
|
|
5689
|
+
*
|
|
5690
|
+
* // Clear all strategies' data
|
|
5691
|
+
* await service.clear();
|
|
5692
|
+
* ```
|
|
5693
|
+
*/
|
|
5694
|
+
this.clear = async (strategyName) => {
|
|
5695
|
+
this.loggerService.log("liveMarkdownService clear", {
|
|
5696
|
+
strategyName,
|
|
5697
|
+
});
|
|
5698
|
+
this.getStorage.clear(strategyName);
|
|
5699
|
+
};
|
|
5700
|
+
/**
|
|
5701
|
+
* Initializes the service by subscribing to live signal events.
|
|
5702
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
5703
|
+
* Automatically called on first use.
|
|
5704
|
+
*
|
|
5705
|
+
* @example
|
|
5706
|
+
* ```typescript
|
|
5707
|
+
* const service = new LiveMarkdownService();
|
|
5708
|
+
* await service.init(); // Subscribe to live events
|
|
5709
|
+
* ```
|
|
5710
|
+
*/
|
|
5711
|
+
this.init = singleshot(async () => {
|
|
5712
|
+
this.loggerService.log("liveMarkdownService init");
|
|
5713
|
+
signalLiveEmitter.subscribe(this.tick);
|
|
5714
|
+
});
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
const columns$1 = [
|
|
5719
|
+
{
|
|
5720
|
+
key: "timestamp",
|
|
5721
|
+
label: "Timestamp",
|
|
5722
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
5723
|
+
},
|
|
5724
|
+
{
|
|
5725
|
+
key: "action",
|
|
5726
|
+
label: "Action",
|
|
5727
|
+
format: (data) => data.action.toUpperCase(),
|
|
5728
|
+
},
|
|
5729
|
+
{
|
|
5730
|
+
key: "symbol",
|
|
5731
|
+
label: "Symbol",
|
|
5732
|
+
format: (data) => data.symbol,
|
|
5733
|
+
},
|
|
5734
|
+
{
|
|
5735
|
+
key: "signalId",
|
|
5736
|
+
label: "Signal ID",
|
|
5737
|
+
format: (data) => data.signalId,
|
|
5738
|
+
},
|
|
5739
|
+
{
|
|
5740
|
+
key: "position",
|
|
5741
|
+
label: "Position",
|
|
5742
|
+
format: (data) => data.position.toUpperCase(),
|
|
5743
|
+
},
|
|
5744
|
+
{
|
|
5745
|
+
key: "note",
|
|
5746
|
+
label: "Note",
|
|
5747
|
+
format: (data) => data.note ?? "N/A",
|
|
5748
|
+
},
|
|
5749
|
+
{
|
|
5750
|
+
key: "currentPrice",
|
|
5751
|
+
label: "Current Price",
|
|
5752
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
5753
|
+
},
|
|
5754
|
+
{
|
|
5755
|
+
key: "priceOpen",
|
|
5756
|
+
label: "Entry Price",
|
|
5757
|
+
format: (data) => `${data.priceOpen.toFixed(8)} USD`,
|
|
5758
|
+
},
|
|
5759
|
+
{
|
|
5760
|
+
key: "takeProfit",
|
|
5761
|
+
label: "Take Profit",
|
|
5762
|
+
format: (data) => `${data.takeProfit.toFixed(8)} USD`,
|
|
5763
|
+
},
|
|
5764
|
+
{
|
|
5765
|
+
key: "stopLoss",
|
|
5766
|
+
label: "Stop Loss",
|
|
5767
|
+
format: (data) => `${data.stopLoss.toFixed(8)} USD`,
|
|
5768
|
+
},
|
|
5769
|
+
{
|
|
5770
|
+
key: "duration",
|
|
5771
|
+
label: "Wait Time (min)",
|
|
5772
|
+
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
5773
|
+
},
|
|
5774
|
+
];
|
|
5775
|
+
/** Maximum number of events to store in schedule reports */
|
|
5776
|
+
const MAX_EVENTS$1 = 250;
|
|
5777
|
+
/**
|
|
5778
|
+
* Storage class for accumulating scheduled signal events per strategy.
|
|
5779
|
+
* Maintains a chronological list of scheduled and cancelled events.
|
|
5780
|
+
*/
|
|
5781
|
+
let ReportStorage$1 = class ReportStorage {
|
|
5782
|
+
constructor() {
|
|
5783
|
+
/** Internal list of all scheduled events for this strategy */
|
|
5784
|
+
this._eventList = [];
|
|
5785
|
+
}
|
|
5786
|
+
/**
|
|
5787
|
+
* Adds a scheduled event to the storage.
|
|
5788
|
+
*
|
|
5789
|
+
* @param data - Scheduled tick result
|
|
5790
|
+
*/
|
|
5791
|
+
addScheduledEvent(data) {
|
|
5792
|
+
this._eventList.push({
|
|
5793
|
+
timestamp: data.signal.scheduledAt,
|
|
5794
|
+
action: "scheduled",
|
|
5795
|
+
symbol: data.signal.symbol,
|
|
5796
|
+
signalId: data.signal.id,
|
|
5797
|
+
position: data.signal.position,
|
|
5798
|
+
note: data.signal.note,
|
|
5799
|
+
currentPrice: data.currentPrice,
|
|
5800
|
+
priceOpen: data.signal.priceOpen,
|
|
5801
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5802
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5803
|
+
});
|
|
5804
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5805
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
5806
|
+
this._eventList.shift();
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
/**
|
|
5810
|
+
* Updates or adds a cancelled event to the storage.
|
|
4598
5811
|
* Replaces the previous event with the same signalId.
|
|
4599
5812
|
*
|
|
4600
|
-
* @param data -
|
|
5813
|
+
* @param data - Cancelled tick result
|
|
4601
5814
|
*/
|
|
4602
|
-
|
|
4603
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
5815
|
+
addCancelledEvent(data) {
|
|
5816
|
+
const durationMs = data.closeTimestamp - data.signal.scheduledAt;
|
|
4604
5817
|
const durationMin = Math.round(durationMs / 60000);
|
|
4605
5818
|
// Find existing event with the same signalId
|
|
4606
5819
|
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
4607
5820
|
const newEvent = {
|
|
4608
5821
|
timestamp: data.closeTimestamp,
|
|
4609
|
-
action: "
|
|
5822
|
+
action: "cancelled",
|
|
4610
5823
|
symbol: data.signal.symbol,
|
|
4611
5824
|
signalId: data.signal.id,
|
|
4612
5825
|
position: data.signal.position,
|
|
4613
5826
|
note: data.signal.note,
|
|
4614
5827
|
currentPrice: data.currentPrice,
|
|
4615
|
-
|
|
5828
|
+
priceOpen: data.signal.priceOpen,
|
|
4616
5829
|
takeProfit: data.signal.priceTakeProfit,
|
|
4617
5830
|
stopLoss: data.signal.priceStopLoss,
|
|
4618
|
-
|
|
4619
|
-
closeReason: data.closeReason,
|
|
5831
|
+
closeTimestamp: data.closeTimestamp,
|
|
4620
5832
|
duration: durationMin,
|
|
4621
5833
|
};
|
|
4622
5834
|
// Replace existing event or add new one
|
|
@@ -4632,8 +5844,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4632
5844
|
}
|
|
4633
5845
|
}
|
|
4634
5846
|
/**
|
|
4635
|
-
* Calculates statistical data from
|
|
4636
|
-
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5847
|
+
* Calculates statistical data from scheduled signal events (Controller).
|
|
4637
5848
|
*
|
|
4638
5849
|
* @returns Statistical data (empty object if no events)
|
|
4639
5850
|
*/
|
|
@@ -4642,78 +5853,34 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4642
5853
|
return {
|
|
4643
5854
|
eventList: [],
|
|
4644
5855
|
totalEvents: 0,
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
avgPnl: null,
|
|
4650
|
-
totalPnl: null,
|
|
4651
|
-
stdDev: null,
|
|
4652
|
-
sharpeRatio: null,
|
|
4653
|
-
annualizedSharpeRatio: null,
|
|
4654
|
-
certaintyRatio: null,
|
|
4655
|
-
expectedYearlyReturns: null,
|
|
5856
|
+
totalScheduled: 0,
|
|
5857
|
+
totalCancelled: 0,
|
|
5858
|
+
cancellationRate: null,
|
|
5859
|
+
avgWaitTime: null,
|
|
4656
5860
|
};
|
|
4657
5861
|
}
|
|
4658
|
-
const
|
|
4659
|
-
const
|
|
4660
|
-
const
|
|
4661
|
-
const
|
|
4662
|
-
// Calculate
|
|
4663
|
-
const
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
let sharpeRatio = 0;
|
|
4670
|
-
let stdDev = 0;
|
|
4671
|
-
if (totalClosed > 0) {
|
|
4672
|
-
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
4673
|
-
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
4674
|
-
stdDev = Math.sqrt(variance);
|
|
4675
|
-
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
4676
|
-
}
|
|
4677
|
-
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
4678
|
-
// Calculate Certainty Ratio
|
|
4679
|
-
let certaintyRatio = 0;
|
|
4680
|
-
if (totalClosed > 0) {
|
|
4681
|
-
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
4682
|
-
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
4683
|
-
const avgWin = wins.length > 0
|
|
4684
|
-
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
4685
|
-
: 0;
|
|
4686
|
-
const avgLoss = losses.length > 0
|
|
4687
|
-
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
4688
|
-
: 0;
|
|
4689
|
-
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4690
|
-
}
|
|
4691
|
-
// Calculate Expected Yearly Returns
|
|
4692
|
-
let expectedYearlyReturns = 0;
|
|
4693
|
-
if (totalClosed > 0) {
|
|
4694
|
-
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
4695
|
-
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
4696
|
-
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4697
|
-
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
4698
|
-
}
|
|
5862
|
+
const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
|
|
5863
|
+
const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
|
|
5864
|
+
const totalScheduled = scheduledEvents.length;
|
|
5865
|
+
const totalCancelled = cancelledEvents.length;
|
|
5866
|
+
// Calculate cancellation rate
|
|
5867
|
+
const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
|
|
5868
|
+
// Calculate average wait time for cancelled signals
|
|
5869
|
+
const avgWaitTime = totalCancelled > 0
|
|
5870
|
+
? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
5871
|
+
totalCancelled
|
|
5872
|
+
: null;
|
|
4699
5873
|
return {
|
|
4700
5874
|
eventList: this._eventList,
|
|
4701
5875
|
totalEvents: this._eventList.length,
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
4707
|
-
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
4708
|
-
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
4709
|
-
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
4710
|
-
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
4711
|
-
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
4712
|
-
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
5876
|
+
totalScheduled,
|
|
5877
|
+
totalCancelled,
|
|
5878
|
+
cancellationRate,
|
|
5879
|
+
avgWaitTime,
|
|
4713
5880
|
};
|
|
4714
5881
|
}
|
|
4715
5882
|
/**
|
|
4716
|
-
* Generates markdown report with all
|
|
5883
|
+
* Generates markdown report with all scheduled events for a strategy (View).
|
|
4717
5884
|
*
|
|
4718
5885
|
* @param strategyName - Strategy name
|
|
4719
5886
|
* @returns Markdown formatted report with all events
|
|
@@ -4721,22 +5888,22 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4721
5888
|
async getReport(strategyName) {
|
|
4722
5889
|
const stats = await this.getData();
|
|
4723
5890
|
if (stats.totalEvents === 0) {
|
|
4724
|
-
return str.newline(`#
|
|
5891
|
+
return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
|
|
4725
5892
|
}
|
|
4726
5893
|
const header = columns$1.map((col) => col.label);
|
|
4727
5894
|
const separator = columns$1.map(() => "---");
|
|
4728
5895
|
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
4729
5896
|
const tableData = [header, separator, ...rows];
|
|
4730
|
-
const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4731
|
-
return str.newline(`#
|
|
5897
|
+
const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
5898
|
+
return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
|
|
4732
5899
|
}
|
|
4733
5900
|
/**
|
|
4734
5901
|
* Saves strategy report to disk.
|
|
4735
5902
|
*
|
|
4736
5903
|
* @param strategyName - Strategy name
|
|
4737
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
5904
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4738
5905
|
*/
|
|
4739
|
-
async dump(strategyName, path = "./logs/
|
|
5906
|
+
async dump(strategyName, path = "./logs/schedule") {
|
|
4740
5907
|
const markdown = await this.getReport(strategyName);
|
|
4741
5908
|
try {
|
|
4742
5909
|
const dir = join(process.cwd(), path);
|
|
@@ -4744,7 +5911,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4744
5911
|
const filename = `${strategyName}.md`;
|
|
4745
5912
|
const filepath = join(dir, filename);
|
|
4746
5913
|
await writeFile(filepath, markdown, "utf-8");
|
|
4747
|
-
console.log(`
|
|
5914
|
+
console.log(`Scheduled signals report saved: ${filepath}`);
|
|
4748
5915
|
}
|
|
4749
5916
|
catch (error) {
|
|
4750
5917
|
console.error(`Failed to save markdown report:`, error);
|
|
@@ -4752,36 +5919,27 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4752
5919
|
}
|
|
4753
5920
|
};
|
|
4754
5921
|
/**
|
|
4755
|
-
* Service for generating and saving
|
|
5922
|
+
* Service for generating and saving scheduled signals markdown reports.
|
|
4756
5923
|
*
|
|
4757
5924
|
* Features:
|
|
4758
|
-
* - Listens to
|
|
4759
|
-
* - Accumulates all events (
|
|
5925
|
+
* - Listens to scheduled and cancelled signal events via signalLiveEmitter
|
|
5926
|
+
* - Accumulates all events (scheduled, cancelled) per strategy
|
|
4760
5927
|
* - Generates markdown tables with detailed event information
|
|
4761
|
-
* - Provides
|
|
4762
|
-
* - Saves reports to disk in logs/
|
|
5928
|
+
* - Provides statistics (cancellation rate, average wait time)
|
|
5929
|
+
* - Saves reports to disk in logs/schedule/{strategyName}.md
|
|
4763
5930
|
*
|
|
4764
5931
|
* @example
|
|
4765
5932
|
* ```typescript
|
|
4766
|
-
* const service = new
|
|
5933
|
+
* const service = new ScheduleMarkdownService();
|
|
4767
5934
|
*
|
|
4768
|
-
* //
|
|
4769
|
-
*
|
|
4770
|
-
* strategyName: "my-strategy",
|
|
4771
|
-
* callbacks: {
|
|
4772
|
-
* onTick: (symbol, result, backtest) => {
|
|
4773
|
-
* if (!backtest) {
|
|
4774
|
-
* service.tick(result);
|
|
4775
|
-
* }
|
|
4776
|
-
* }
|
|
4777
|
-
* }
|
|
4778
|
-
* });
|
|
5935
|
+
* // Service automatically subscribes to signalLiveEmitter on init
|
|
5936
|
+
* // No manual callback setup needed
|
|
4779
5937
|
*
|
|
4780
5938
|
* // Later: generate and save report
|
|
4781
5939
|
* await service.dump("my-strategy");
|
|
4782
5940
|
* ```
|
|
4783
5941
|
*/
|
|
4784
|
-
class
|
|
5942
|
+
class ScheduleMarkdownService {
|
|
4785
5943
|
constructor() {
|
|
4786
5944
|
/** Logger service for debug output */
|
|
4787
5945
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -4791,46 +5949,33 @@ class LiveMarkdownService {
|
|
|
4791
5949
|
*/
|
|
4792
5950
|
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
|
|
4793
5951
|
/**
|
|
4794
|
-
* Processes tick events and accumulates
|
|
4795
|
-
* Should be called from
|
|
5952
|
+
* Processes tick events and accumulates scheduled/cancelled events.
|
|
5953
|
+
* Should be called from signalLiveEmitter subscription.
|
|
4796
5954
|
*
|
|
4797
|
-
* Processes
|
|
5955
|
+
* Processes only scheduled and cancelled event types.
|
|
4798
5956
|
*
|
|
4799
5957
|
* @param data - Tick result from strategy execution
|
|
4800
5958
|
*
|
|
4801
5959
|
* @example
|
|
4802
5960
|
* ```typescript
|
|
4803
|
-
* const service = new
|
|
4804
|
-
*
|
|
4805
|
-
* callbacks: {
|
|
4806
|
-
* onTick: (symbol, result, backtest) => {
|
|
4807
|
-
* if (!backtest) {
|
|
4808
|
-
* service.tick(result);
|
|
4809
|
-
* }
|
|
4810
|
-
* }
|
|
4811
|
-
* }
|
|
5961
|
+
* const service = new ScheduleMarkdownService();
|
|
5962
|
+
* // Service automatically subscribes in init()
|
|
4812
5963
|
* ```
|
|
4813
5964
|
*/
|
|
4814
5965
|
this.tick = async (data) => {
|
|
4815
|
-
this.loggerService.log("
|
|
5966
|
+
this.loggerService.log("scheduleMarkdownService tick", {
|
|
4816
5967
|
data,
|
|
4817
5968
|
});
|
|
4818
5969
|
const storage = this.getStorage(data.strategyName);
|
|
4819
|
-
if (data.action === "
|
|
4820
|
-
storage.
|
|
4821
|
-
}
|
|
4822
|
-
else if (data.action === "opened") {
|
|
4823
|
-
storage.addOpenedEvent(data);
|
|
4824
|
-
}
|
|
4825
|
-
else if (data.action === "active") {
|
|
4826
|
-
storage.addActiveEvent(data);
|
|
5970
|
+
if (data.action === "scheduled") {
|
|
5971
|
+
storage.addScheduledEvent(data);
|
|
4827
5972
|
}
|
|
4828
|
-
else if (data.action === "
|
|
4829
|
-
storage.
|
|
5973
|
+
else if (data.action === "cancelled") {
|
|
5974
|
+
storage.addCancelledEvent(data);
|
|
4830
5975
|
}
|
|
4831
5976
|
};
|
|
4832
5977
|
/**
|
|
4833
|
-
* Gets statistical data from all
|
|
5978
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
4834
5979
|
* Delegates to ReportStorage.getData().
|
|
4835
5980
|
*
|
|
4836
5981
|
* @param strategyName - Strategy name to get data for
|
|
@@ -4838,20 +5983,20 @@ class LiveMarkdownService {
|
|
|
4838
5983
|
*
|
|
4839
5984
|
* @example
|
|
4840
5985
|
* ```typescript
|
|
4841
|
-
* const service = new
|
|
5986
|
+
* const service = new ScheduleMarkdownService();
|
|
4842
5987
|
* const stats = await service.getData("my-strategy");
|
|
4843
|
-
* console.log(stats.
|
|
5988
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
4844
5989
|
* ```
|
|
4845
5990
|
*/
|
|
4846
5991
|
this.getData = async (strategyName) => {
|
|
4847
|
-
this.loggerService.log("
|
|
5992
|
+
this.loggerService.log("scheduleMarkdownService getData", {
|
|
4848
5993
|
strategyName,
|
|
4849
5994
|
});
|
|
4850
5995
|
const storage = this.getStorage(strategyName);
|
|
4851
5996
|
return storage.getData();
|
|
4852
5997
|
};
|
|
4853
5998
|
/**
|
|
4854
|
-
* Generates markdown report with all events for a strategy.
|
|
5999
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
4855
6000
|
* Delegates to ReportStorage.getReport().
|
|
4856
6001
|
*
|
|
4857
6002
|
* @param strategyName - Strategy name to generate report for
|
|
@@ -4859,13 +6004,13 @@ class LiveMarkdownService {
|
|
|
4859
6004
|
*
|
|
4860
6005
|
* @example
|
|
4861
6006
|
* ```typescript
|
|
4862
|
-
* const service = new
|
|
6007
|
+
* const service = new ScheduleMarkdownService();
|
|
4863
6008
|
* const markdown = await service.getReport("my-strategy");
|
|
4864
6009
|
* console.log(markdown);
|
|
4865
6010
|
* ```
|
|
4866
6011
|
*/
|
|
4867
6012
|
this.getReport = async (strategyName) => {
|
|
4868
|
-
this.loggerService.log("
|
|
6013
|
+
this.loggerService.log("scheduleMarkdownService getReport", {
|
|
4869
6014
|
strategyName,
|
|
4870
6015
|
});
|
|
4871
6016
|
const storage = this.getStorage(strategyName);
|
|
@@ -4877,21 +6022,21 @@ class LiveMarkdownService {
|
|
|
4877
6022
|
* Delegates to ReportStorage.dump().
|
|
4878
6023
|
*
|
|
4879
6024
|
* @param strategyName - Strategy name to save report for
|
|
4880
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
6025
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4881
6026
|
*
|
|
4882
6027
|
* @example
|
|
4883
6028
|
* ```typescript
|
|
4884
|
-
* const service = new
|
|
6029
|
+
* const service = new ScheduleMarkdownService();
|
|
4885
6030
|
*
|
|
4886
|
-
* // Save to default path: ./logs/
|
|
6031
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
4887
6032
|
* await service.dump("my-strategy");
|
|
4888
6033
|
*
|
|
4889
6034
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
4890
6035
|
* await service.dump("my-strategy", "./custom/path");
|
|
4891
6036
|
* ```
|
|
4892
6037
|
*/
|
|
4893
|
-
this.dump = async (strategyName, path = "./logs/
|
|
4894
|
-
this.loggerService.log("
|
|
6038
|
+
this.dump = async (strategyName, path = "./logs/schedule") => {
|
|
6039
|
+
this.loggerService.log("scheduleMarkdownService dump", {
|
|
4895
6040
|
strategyName,
|
|
4896
6041
|
path,
|
|
4897
6042
|
});
|
|
@@ -4907,7 +6052,7 @@ class LiveMarkdownService {
|
|
|
4907
6052
|
*
|
|
4908
6053
|
* @example
|
|
4909
6054
|
* ```typescript
|
|
4910
|
-
* const service = new
|
|
6055
|
+
* const service = new ScheduleMarkdownService();
|
|
4911
6056
|
*
|
|
4912
6057
|
* // Clear specific strategy data
|
|
4913
6058
|
* await service.clear("my-strategy");
|
|
@@ -4917,7 +6062,7 @@ class LiveMarkdownService {
|
|
|
4917
6062
|
* ```
|
|
4918
6063
|
*/
|
|
4919
6064
|
this.clear = async (strategyName) => {
|
|
4920
|
-
this.loggerService.log("
|
|
6065
|
+
this.loggerService.log("scheduleMarkdownService clear", {
|
|
4921
6066
|
strategyName,
|
|
4922
6067
|
});
|
|
4923
6068
|
this.getStorage.clear(strategyName);
|
|
@@ -4929,13 +6074,13 @@ class LiveMarkdownService {
|
|
|
4929
6074
|
*
|
|
4930
6075
|
* @example
|
|
4931
6076
|
* ```typescript
|
|
4932
|
-
* const service = new
|
|
6077
|
+
* const service = new ScheduleMarkdownService();
|
|
4933
6078
|
* await service.init(); // Subscribe to live events
|
|
4934
6079
|
* ```
|
|
4935
6080
|
*/
|
|
4936
6081
|
this.init = singleshot(async () => {
|
|
4937
|
-
this.loggerService.log("
|
|
4938
|
-
|
|
6082
|
+
this.loggerService.log("scheduleMarkdownService init");
|
|
6083
|
+
signalEmitter.subscribe(this.tick);
|
|
4939
6084
|
});
|
|
4940
6085
|
}
|
|
4941
6086
|
}
|
|
@@ -6499,6 +7644,7 @@ class RiskValidationService {
|
|
|
6499
7644
|
{
|
|
6500
7645
|
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
|
|
6501
7646
|
provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
|
|
7647
|
+
provide(TYPES.scheduleMarkdownService, () => new ScheduleMarkdownService());
|
|
6502
7648
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
6503
7649
|
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
6504
7650
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
@@ -6557,6 +7703,7 @@ const logicPublicServices = {
|
|
|
6557
7703
|
const markdownServices = {
|
|
6558
7704
|
backtestMarkdownService: inject(TYPES.backtestMarkdownService),
|
|
6559
7705
|
liveMarkdownService: inject(TYPES.liveMarkdownService),
|
|
7706
|
+
scheduleMarkdownService: inject(TYPES.scheduleMarkdownService),
|
|
6560
7707
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
6561
7708
|
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
6562
7709
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
@@ -6603,6 +7750,20 @@ var backtest$1 = backtest;
|
|
|
6603
7750
|
async function setLogger(logger) {
|
|
6604
7751
|
backtest$1.loggerService.setLogger(logger);
|
|
6605
7752
|
}
|
|
7753
|
+
/**
|
|
7754
|
+
* Sets global configuration parameters for the framework.
|
|
7755
|
+
* @param config - Partial configuration object to override default settings
|
|
7756
|
+
*
|
|
7757
|
+
* @example
|
|
7758
|
+
* ```typescript
|
|
7759
|
+
* setConfig({
|
|
7760
|
+
* CC_SCHEDULE_AWAIT_MINUTES: 90,
|
|
7761
|
+
* });
|
|
7762
|
+
* ```
|
|
7763
|
+
*/
|
|
7764
|
+
async function setConfig(config) {
|
|
7765
|
+
Object.assign(GLOBAL_CONFIG, config);
|
|
7766
|
+
}
|
|
6606
7767
|
|
|
6607
7768
|
const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
|
|
6608
7769
|
const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
|
|
@@ -7885,8 +9046,17 @@ class BacktestUtils {
|
|
|
7885
9046
|
symbol,
|
|
7886
9047
|
context,
|
|
7887
9048
|
});
|
|
7888
|
-
|
|
7889
|
-
|
|
9049
|
+
{
|
|
9050
|
+
backtest$1.backtestMarkdownService.clear(context.strategyName);
|
|
9051
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9052
|
+
}
|
|
9053
|
+
{
|
|
9054
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9055
|
+
}
|
|
9056
|
+
{
|
|
9057
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9058
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9059
|
+
}
|
|
7890
9060
|
return backtest$1.backtestGlobalService.run(symbol, context);
|
|
7891
9061
|
};
|
|
7892
9062
|
/**
|
|
@@ -8065,8 +9235,17 @@ class LiveUtils {
|
|
|
8065
9235
|
symbol,
|
|
8066
9236
|
context,
|
|
8067
9237
|
});
|
|
8068
|
-
|
|
8069
|
-
|
|
9238
|
+
{
|
|
9239
|
+
backtest$1.liveMarkdownService.clear(context.strategyName);
|
|
9240
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9241
|
+
}
|
|
9242
|
+
{
|
|
9243
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9244
|
+
}
|
|
9245
|
+
{
|
|
9246
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9247
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9248
|
+
}
|
|
8070
9249
|
return backtest$1.liveGlobalService.run(symbol, context);
|
|
8071
9250
|
};
|
|
8072
9251
|
/**
|
|
@@ -8192,6 +9371,132 @@ class LiveUtils {
|
|
|
8192
9371
|
*/
|
|
8193
9372
|
const Live = new LiveUtils();
|
|
8194
9373
|
|
|
9374
|
+
const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
|
|
9375
|
+
const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
|
|
9376
|
+
const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
|
|
9377
|
+
const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
|
|
9378
|
+
/**
|
|
9379
|
+
* Utility class for scheduled signals reporting operations.
|
|
9380
|
+
*
|
|
9381
|
+
* Provides simplified access to scheduleMarkdownService with logging.
|
|
9382
|
+
* Exported as singleton instance for convenient usage.
|
|
9383
|
+
*
|
|
9384
|
+
* Features:
|
|
9385
|
+
* - Track scheduled signals in queue
|
|
9386
|
+
* - Track cancelled signals
|
|
9387
|
+
* - Calculate cancellation rate and average wait time
|
|
9388
|
+
* - Generate markdown reports
|
|
9389
|
+
*
|
|
9390
|
+
* @example
|
|
9391
|
+
* ```typescript
|
|
9392
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9393
|
+
*
|
|
9394
|
+
* // Get scheduled signals statistics
|
|
9395
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9396
|
+
* console.log(`Cancellation rate: ${stats.cancellationRate}%`);
|
|
9397
|
+
* console.log(`Average wait time: ${stats.avgWaitTime} minutes`);
|
|
9398
|
+
*
|
|
9399
|
+
* // Generate and save report
|
|
9400
|
+
* await Schedule.dump("my-strategy");
|
|
9401
|
+
* ```
|
|
9402
|
+
*/
|
|
9403
|
+
class ScheduleUtils {
|
|
9404
|
+
constructor() {
|
|
9405
|
+
/**
|
|
9406
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
9407
|
+
*
|
|
9408
|
+
* @param strategyName - Strategy name to get data for
|
|
9409
|
+
* @returns Promise resolving to statistical data object
|
|
9410
|
+
*
|
|
9411
|
+
* @example
|
|
9412
|
+
* ```typescript
|
|
9413
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9414
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
9415
|
+
* ```
|
|
9416
|
+
*/
|
|
9417
|
+
this.getData = async (strategyName) => {
|
|
9418
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_DATA, {
|
|
9419
|
+
strategyName,
|
|
9420
|
+
});
|
|
9421
|
+
return await backtest$1.scheduleMarkdownService.getData(strategyName);
|
|
9422
|
+
};
|
|
9423
|
+
/**
|
|
9424
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
9425
|
+
*
|
|
9426
|
+
* @param strategyName - Strategy name to generate report for
|
|
9427
|
+
* @returns Promise resolving to markdown formatted report string
|
|
9428
|
+
*
|
|
9429
|
+
* @example
|
|
9430
|
+
* ```typescript
|
|
9431
|
+
* const markdown = await Schedule.getReport("my-strategy");
|
|
9432
|
+
* console.log(markdown);
|
|
9433
|
+
* ```
|
|
9434
|
+
*/
|
|
9435
|
+
this.getReport = async (strategyName) => {
|
|
9436
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_REPORT, {
|
|
9437
|
+
strategyName,
|
|
9438
|
+
});
|
|
9439
|
+
return await backtest$1.scheduleMarkdownService.getReport(strategyName);
|
|
9440
|
+
};
|
|
9441
|
+
/**
|
|
9442
|
+
* Saves strategy report to disk.
|
|
9443
|
+
*
|
|
9444
|
+
* @param strategyName - Strategy name to save report for
|
|
9445
|
+
* @param path - Optional directory path to save report (default: "./logs/schedule")
|
|
9446
|
+
*
|
|
9447
|
+
* @example
|
|
9448
|
+
* ```typescript
|
|
9449
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
9450
|
+
* await Schedule.dump("my-strategy");
|
|
9451
|
+
*
|
|
9452
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
9453
|
+
* await Schedule.dump("my-strategy", "./custom/path");
|
|
9454
|
+
* ```
|
|
9455
|
+
*/
|
|
9456
|
+
this.dump = async (strategyName, path) => {
|
|
9457
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_DUMP, {
|
|
9458
|
+
strategyName,
|
|
9459
|
+
path,
|
|
9460
|
+
});
|
|
9461
|
+
await backtest$1.scheduleMarkdownService.dump(strategyName, path);
|
|
9462
|
+
};
|
|
9463
|
+
/**
|
|
9464
|
+
* Clears accumulated scheduled signal data from storage.
|
|
9465
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
9466
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
9467
|
+
*
|
|
9468
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
9469
|
+
*
|
|
9470
|
+
* @example
|
|
9471
|
+
* ```typescript
|
|
9472
|
+
* // Clear specific strategy data
|
|
9473
|
+
* await Schedule.clear("my-strategy");
|
|
9474
|
+
*
|
|
9475
|
+
* // Clear all strategies' data
|
|
9476
|
+
* await Schedule.clear();
|
|
9477
|
+
* ```
|
|
9478
|
+
*/
|
|
9479
|
+
this.clear = async (strategyName) => {
|
|
9480
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
|
|
9481
|
+
strategyName,
|
|
9482
|
+
});
|
|
9483
|
+
await backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9484
|
+
};
|
|
9485
|
+
}
|
|
9486
|
+
}
|
|
9487
|
+
/**
|
|
9488
|
+
* Singleton instance of ScheduleUtils for convenient scheduled signals reporting.
|
|
9489
|
+
*
|
|
9490
|
+
* @example
|
|
9491
|
+
* ```typescript
|
|
9492
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9493
|
+
*
|
|
9494
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9495
|
+
* console.log("Cancellation rate:", stats.cancellationRate);
|
|
9496
|
+
* ```
|
|
9497
|
+
*/
|
|
9498
|
+
const Schedule = new ScheduleUtils();
|
|
9499
|
+
|
|
8195
9500
|
/**
|
|
8196
9501
|
* Performance class provides static methods for performance metrics analysis.
|
|
8197
9502
|
*
|
|
@@ -8367,8 +9672,17 @@ class WalkerUtils {
|
|
|
8367
9672
|
backtest$1.walkerMarkdownService.clear(context.walkerName);
|
|
8368
9673
|
// Clear backtest data for all strategies
|
|
8369
9674
|
for (const strategyName of walkerSchema.strategies) {
|
|
8370
|
-
|
|
8371
|
-
|
|
9675
|
+
{
|
|
9676
|
+
backtest$1.backtestMarkdownService.clear(strategyName);
|
|
9677
|
+
backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9678
|
+
}
|
|
9679
|
+
{
|
|
9680
|
+
backtest$1.strategyGlobalService.clear(strategyName);
|
|
9681
|
+
}
|
|
9682
|
+
{
|
|
9683
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
9684
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9685
|
+
}
|
|
8372
9686
|
}
|
|
8373
9687
|
return backtest$1.walkerGlobalService.run(symbol, {
|
|
8374
9688
|
walkerName: context.walkerName,
|
|
@@ -8418,6 +9732,9 @@ class WalkerUtils {
|
|
|
8418
9732
|
task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
|
|
8419
9733
|
return () => {
|
|
8420
9734
|
isStopped = true;
|
|
9735
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
9736
|
+
backtest$1.strategyGlobalService.stop(strategyName);
|
|
9737
|
+
}
|
|
8421
9738
|
};
|
|
8422
9739
|
};
|
|
8423
9740
|
/**
|
|
@@ -8773,4 +10090,4 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
|
|
|
8773
10090
|
};
|
|
8774
10091
|
const PositionSize = PositionSizeUtils;
|
|
8775
10092
|
|
|
8776
|
-
export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Performance, PersistBase, PersistRiskAdapter, PersistSignalAdaper, PositionSize, Walker, addExchange, addFrame, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listRisks, listSizings, listStrategies, listWalkers, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPerformance, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, setLogger };
|
|
10093
|
+
export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Performance, PersistBase, PersistRiskAdapter, PersistSignalAdaper, PositionSize, Schedule, Walker, addExchange, addFrame, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listRisks, listSizings, listStrategies, listWalkers, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPerformance, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, setConfig, setLogger };
|