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