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