backtest-kit 9.8.5 → 10.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.
Files changed (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1995 -1898
  3. package/build/index.cjs +1387 -412
  4. package/build/index.mjs +1386 -413
  5. package/package.json +86 -86
  6. package/types.d.ts +448 -8
package/build/index.mjs CHANGED
@@ -942,7 +942,7 @@ async function writeFileAtomic(file, data, options = {}) {
942
942
 
943
943
  var _a$3;
944
944
  /** Logger service injected as DI singleton */
945
- const LOGGER_SERVICE$8 = new LoggerService();
945
+ const LOGGER_SERVICE$9 = new LoggerService();
946
946
  /** Symbol key for the singleshot waitForInit function on PersistBase instances. */
947
947
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
948
948
  // Calculate step in milliseconds for candle close time validation
@@ -1065,7 +1065,7 @@ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
1065
1065
  const BASE_UNLINK_RETRY_COUNT = 5;
1066
1066
  const BASE_UNLINK_RETRY_DELAY = 1000;
1067
1067
  const BASE_WAIT_FOR_INIT_FN = async (self) => {
1068
- LOGGER_SERVICE$8.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
1068
+ LOGGER_SERVICE$9.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
1069
1069
  entityName: self.entityName,
1070
1070
  directory: self._directory,
1071
1071
  });
@@ -1123,7 +1123,7 @@ class PersistBase {
1123
1123
  this.entityName = entityName;
1124
1124
  this.baseDir = baseDir;
1125
1125
  this[_a$3] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1126
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1126
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1127
1127
  entityName: this.entityName,
1128
1128
  baseDir,
1129
1129
  });
@@ -1139,14 +1139,14 @@ class PersistBase {
1139
1139
  return join(this.baseDir, this.entityName, `${entityId}.json`);
1140
1140
  }
1141
1141
  async waitForInit(initial) {
1142
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
1142
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
1143
1143
  entityName: this.entityName,
1144
1144
  initial,
1145
1145
  });
1146
1146
  await this[BASE_WAIT_FOR_INIT_SYMBOL]();
1147
1147
  }
1148
1148
  async readValue(entityId) {
1149
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
1149
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
1150
1150
  entityName: this.entityName,
1151
1151
  entityId,
1152
1152
  });
@@ -1163,7 +1163,7 @@ class PersistBase {
1163
1163
  }
1164
1164
  }
1165
1165
  async hasValue(entityId) {
1166
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1166
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1167
1167
  entityName: this.entityName,
1168
1168
  entityId,
1169
1169
  });
@@ -1180,7 +1180,7 @@ class PersistBase {
1180
1180
  }
1181
1181
  }
1182
1182
  async writeValue(entityId, entity) {
1183
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
1183
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
1184
1184
  entityName: this.entityName,
1185
1185
  entityId,
1186
1186
  });
@@ -1202,7 +1202,7 @@ class PersistBase {
1202
1202
  * @throws Error if reading fails
1203
1203
  */
1204
1204
  async *keys() {
1205
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
1205
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
1206
1206
  entityName: this.entityName,
1207
1207
  });
1208
1208
  try {
@@ -1347,7 +1347,7 @@ class PersistSignalUtils {
1347
1347
  * @returns Promise resolving to signal or null if none persisted
1348
1348
  */
1349
1349
  this.readSignalData = async (symbol, strategyName, exchangeName) => {
1350
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1350
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1351
1351
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1352
1352
  const isInitial = !this.getStorage.has(key);
1353
1353
  const instance = this.getStorage(symbol, strategyName, exchangeName);
@@ -1365,7 +1365,7 @@ class PersistSignalUtils {
1365
1365
  * @returns Promise that resolves when write is complete
1366
1366
  */
1367
1367
  this.writeSignalData = async (signalRow, symbol, strategyName, exchangeName) => {
1368
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1368
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1369
1369
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1370
1370
  const isInitial = !this.getStorage.has(key);
1371
1371
  const instance = this.getStorage(symbol, strategyName, exchangeName);
@@ -1380,7 +1380,7 @@ class PersistSignalUtils {
1380
1380
  * @param Ctor - Custom IPersistSignalInstance constructor
1381
1381
  */
1382
1382
  usePersistSignalAdapter(Ctor) {
1383
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1383
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1384
1384
  this.PersistSignalInstanceCtor = Ctor;
1385
1385
  this.getStorage.clear();
1386
1386
  }
@@ -1389,21 +1389,21 @@ class PersistSignalUtils {
1389
1389
  * Call when process.cwd() changes between strategy iterations.
1390
1390
  */
1391
1391
  clear() {
1392
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
1392
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
1393
1393
  this.getStorage.clear();
1394
1394
  }
1395
1395
  /**
1396
1396
  * Switches to the default file-based PersistSignalInstance.
1397
1397
  */
1398
1398
  useJson() {
1399
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1399
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1400
1400
  this.usePersistSignalAdapter(PersistSignalInstance);
1401
1401
  }
1402
1402
  /**
1403
1403
  * Switches to PersistSignalDummyInstance (all operations are no-ops).
1404
1404
  */
1405
1405
  useDummy() {
1406
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1406
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1407
1407
  this.usePersistSignalAdapter(PersistSignalDummyInstance);
1408
1408
  }
1409
1409
  }
@@ -1543,7 +1543,7 @@ class PersistRiskUtils {
1543
1543
  * @returns Promise resolving to position entries (empty array if none)
1544
1544
  */
1545
1545
  this.readPositionData = async (riskName, exchangeName, when) => {
1546
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1546
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1547
1547
  const key = `${riskName}:${exchangeName}`;
1548
1548
  const isInitial = !this.getRiskStorage.has(key);
1549
1549
  const instance = this.getRiskStorage(riskName, exchangeName);
@@ -1561,7 +1561,7 @@ class PersistRiskUtils {
1561
1561
  * @returns Promise that resolves when write is complete
1562
1562
  */
1563
1563
  this.writePositionData = async (riskRow, riskName, exchangeName, when) => {
1564
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1564
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1565
1565
  const key = `${riskName}:${exchangeName}`;
1566
1566
  const isInitial = !this.getRiskStorage.has(key);
1567
1567
  const instance = this.getRiskStorage(riskName, exchangeName);
@@ -1576,7 +1576,7 @@ class PersistRiskUtils {
1576
1576
  * @param Ctor - Custom IPersistRiskInstance constructor
1577
1577
  */
1578
1578
  usePersistRiskAdapter(Ctor) {
1579
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1579
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1580
1580
  this.PersistRiskInstanceCtor = Ctor;
1581
1581
  this.getRiskStorage.clear();
1582
1582
  }
@@ -1585,21 +1585,21 @@ class PersistRiskUtils {
1585
1585
  * Call when process.cwd() changes between strategy iterations.
1586
1586
  */
1587
1587
  clear() {
1588
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
1588
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
1589
1589
  this.getRiskStorage.clear();
1590
1590
  }
1591
1591
  /**
1592
1592
  * Switches to the default file-based PersistRiskInstance.
1593
1593
  */
1594
1594
  useJson() {
1595
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1595
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1596
1596
  this.usePersistRiskAdapter(PersistRiskInstance);
1597
1597
  }
1598
1598
  /**
1599
1599
  * Switches to PersistRiskDummyInstance (all operations are no-ops).
1600
1600
  */
1601
1601
  useDummy() {
1602
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1602
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1603
1603
  this.usePersistRiskAdapter(PersistRiskDummyInstance);
1604
1604
  }
1605
1605
  }
@@ -1738,7 +1738,7 @@ class PersistScheduleUtils {
1738
1738
  * @returns Promise resolving to scheduled signal or null if none persisted
1739
1739
  */
1740
1740
  this.readScheduleData = async (symbol, strategyName, exchangeName) => {
1741
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1741
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1742
1742
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1743
1743
  const isInitial = !this.getScheduleStorage.has(key);
1744
1744
  const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
@@ -1756,7 +1756,7 @@ class PersistScheduleUtils {
1756
1756
  * @returns Promise that resolves when write is complete
1757
1757
  */
1758
1758
  this.writeScheduleData = async (scheduledSignalRow, symbol, strategyName, exchangeName) => {
1759
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1759
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1760
1760
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1761
1761
  const isInitial = !this.getScheduleStorage.has(key);
1762
1762
  const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
@@ -1771,7 +1771,7 @@ class PersistScheduleUtils {
1771
1771
  * @param Ctor - Custom IPersistScheduleInstance constructor
1772
1772
  */
1773
1773
  usePersistScheduleAdapter(Ctor) {
1774
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1774
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1775
1775
  this.PersistScheduleInstanceCtor = Ctor;
1776
1776
  this.getScheduleStorage.clear();
1777
1777
  }
@@ -1780,21 +1780,21 @@ class PersistScheduleUtils {
1780
1780
  * Call when process.cwd() changes between strategy iterations.
1781
1781
  */
1782
1782
  clear() {
1783
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
1783
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
1784
1784
  this.getScheduleStorage.clear();
1785
1785
  }
1786
1786
  /**
1787
1787
  * Switches to the default file-based PersistScheduleInstance.
1788
1788
  */
1789
1789
  useJson() {
1790
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1790
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1791
1791
  this.usePersistScheduleAdapter(PersistScheduleInstance);
1792
1792
  }
1793
1793
  /**
1794
1794
  * Switches to PersistScheduleDummyInstance (all operations are no-ops).
1795
1795
  */
1796
1796
  useDummy() {
1797
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1797
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1798
1798
  this.usePersistScheduleAdapter(PersistScheduleDummyInstance);
1799
1799
  }
1800
1800
  }
@@ -1939,7 +1939,7 @@ class PersistPartialUtils {
1939
1939
  * @returns Promise resolving to partial data record (empty object if none)
1940
1940
  */
1941
1941
  this.readPartialData = async (symbol, strategyName, signalId, exchangeName, when) => {
1942
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1942
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1943
1943
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1944
1944
  const isInitial = !this.getPartialStorage.has(key);
1945
1945
  const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
@@ -1959,7 +1959,7 @@ class PersistPartialUtils {
1959
1959
  * @returns Promise that resolves when write is complete
1960
1960
  */
1961
1961
  this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName, when) => {
1962
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1962
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1963
1963
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1964
1964
  const isInitial = !this.getPartialStorage.has(key);
1965
1965
  const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
@@ -1974,7 +1974,7 @@ class PersistPartialUtils {
1974
1974
  * @param Ctor - Custom IPersistPartialInstance constructor
1975
1975
  */
1976
1976
  usePersistPartialAdapter(Ctor) {
1977
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1977
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1978
1978
  this.PersistPartialInstanceCtor = Ctor;
1979
1979
  this.getPartialStorage.clear();
1980
1980
  }
@@ -1983,21 +1983,21 @@ class PersistPartialUtils {
1983
1983
  * Call when process.cwd() changes between strategy iterations.
1984
1984
  */
1985
1985
  clear() {
1986
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
1986
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
1987
1987
  this.getPartialStorage.clear();
1988
1988
  }
1989
1989
  /**
1990
1990
  * Switches to the default file-based PersistPartialInstance.
1991
1991
  */
1992
1992
  useJson() {
1993
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
1993
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
1994
1994
  this.usePersistPartialAdapter(PersistPartialInstance);
1995
1995
  }
1996
1996
  /**
1997
1997
  * Switches to PersistPartialDummyInstance (all operations are no-ops).
1998
1998
  */
1999
1999
  useDummy() {
2000
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
2000
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
2001
2001
  this.usePersistPartialAdapter(PersistPartialDummyInstance);
2002
2002
  }
2003
2003
  }
@@ -2162,7 +2162,7 @@ class PersistBreakevenUtils {
2162
2162
  * @returns Promise resolving to breakeven data record (empty object if none)
2163
2163
  */
2164
2164
  this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName, when) => {
2165
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
2165
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
2166
2166
  const key = `${symbol}:${strategyName}:${exchangeName}`;
2167
2167
  const isInitial = !this.getBreakevenStorage.has(key);
2168
2168
  const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
@@ -2182,7 +2182,7 @@ class PersistBreakevenUtils {
2182
2182
  * @returns Promise that resolves when write is complete
2183
2183
  */
2184
2184
  this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName, when) => {
2185
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
2185
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
2186
2186
  const key = `${symbol}:${strategyName}:${exchangeName}`;
2187
2187
  const isInitial = !this.getBreakevenStorage.has(key);
2188
2188
  const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
@@ -2197,7 +2197,7 @@ class PersistBreakevenUtils {
2197
2197
  * @param Ctor - Custom IPersistBreakevenInstance constructor
2198
2198
  */
2199
2199
  usePersistBreakevenAdapter(Ctor) {
2200
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
2200
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
2201
2201
  this.PersistBreakevenInstanceCtor = Ctor;
2202
2202
  this.getBreakevenStorage.clear();
2203
2203
  }
@@ -2206,21 +2206,21 @@ class PersistBreakevenUtils {
2206
2206
  * Call when process.cwd() changes between strategy iterations.
2207
2207
  */
2208
2208
  clear() {
2209
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
2209
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
2210
2210
  this.getBreakevenStorage.clear();
2211
2211
  }
2212
2212
  /**
2213
2213
  * Switches to the default file-based PersistBreakevenInstance.
2214
2214
  */
2215
2215
  useJson() {
2216
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
2216
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
2217
2217
  this.usePersistBreakevenAdapter(PersistBreakevenInstance);
2218
2218
  }
2219
2219
  /**
2220
2220
  * Switches to PersistBreakevenDummyInstance (all operations are no-ops).
2221
2221
  */
2222
2222
  useDummy() {
2223
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
2223
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
2224
2224
  this.usePersistBreakevenAdapter(PersistBreakevenDummyInstance);
2225
2225
  }
2226
2226
  }
@@ -2311,7 +2311,7 @@ class PersistCandleInstance {
2311
2311
  error: errorData(error),
2312
2312
  message: getErrorMessage(error),
2313
2313
  };
2314
- LOGGER_SERVICE$8.warn(message, payload);
2314
+ LOGGER_SERVICE$9.warn(message, payload);
2315
2315
  console.warn(message, payload);
2316
2316
  errorEmitter.next(error);
2317
2317
  return null;
@@ -2333,7 +2333,7 @@ class PersistCandleInstance {
2333
2333
  for (const candle of candles) {
2334
2334
  const candleCloseTime = candle.timestamp + stepMs;
2335
2335
  if (candleCloseTime > now) {
2336
- LOGGER_SERVICE$8.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
2336
+ LOGGER_SERVICE$9.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
2337
2337
  symbol: this.symbol,
2338
2338
  interval: this.interval,
2339
2339
  exchangeName: this.exchangeName,
@@ -2410,7 +2410,7 @@ class PersistCandleUtils {
2410
2410
  * @returns Promise resolving to candles in order, or null on cache miss
2411
2411
  */
2412
2412
  this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
2413
- LOGGER_SERVICE$8.info("PersistCandleUtils.readCandlesData", {
2413
+ LOGGER_SERVICE$9.info("PersistCandleUtils.readCandlesData", {
2414
2414
  symbol,
2415
2415
  interval,
2416
2416
  exchangeName,
@@ -2434,7 +2434,7 @@ class PersistCandleUtils {
2434
2434
  * @returns Promise that resolves when all writes are complete
2435
2435
  */
2436
2436
  this.writeCandlesData = async (candles, symbol, interval, exchangeName) => {
2437
- LOGGER_SERVICE$8.info("PersistCandleUtils.writeCandlesData", {
2437
+ LOGGER_SERVICE$9.info("PersistCandleUtils.writeCandlesData", {
2438
2438
  symbol,
2439
2439
  interval,
2440
2440
  exchangeName,
@@ -2454,7 +2454,7 @@ class PersistCandleUtils {
2454
2454
  * @param Ctor - Custom IPersistCandleInstance constructor
2455
2455
  */
2456
2456
  usePersistCandleAdapter(Ctor) {
2457
- LOGGER_SERVICE$8.info("PersistCandleUtils.usePersistCandleAdapter");
2457
+ LOGGER_SERVICE$9.info("PersistCandleUtils.usePersistCandleAdapter");
2458
2458
  this.PersistCandleInstanceCtor = Ctor;
2459
2459
  this.getCandlesStorage.clear();
2460
2460
  }
@@ -2463,21 +2463,21 @@ class PersistCandleUtils {
2463
2463
  * Call when process.cwd() changes between strategy iterations.
2464
2464
  */
2465
2465
  clear() {
2466
- LOGGER_SERVICE$8.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
2466
+ LOGGER_SERVICE$9.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
2467
2467
  this.getCandlesStorage.clear();
2468
2468
  }
2469
2469
  /**
2470
2470
  * Switches to the default file-based PersistCandleInstance.
2471
2471
  */
2472
2472
  useJson() {
2473
- LOGGER_SERVICE$8.log("PersistCandleUtils.useJson");
2473
+ LOGGER_SERVICE$9.log("PersistCandleUtils.useJson");
2474
2474
  this.usePersistCandleAdapter(PersistCandleInstance);
2475
2475
  }
2476
2476
  /**
2477
2477
  * Switches to PersistCandleDummyInstance (always returns null on read, discards writes).
2478
2478
  */
2479
2479
  useDummy() {
2480
- LOGGER_SERVICE$8.log("PersistCandleUtils.useDummy");
2480
+ LOGGER_SERVICE$9.log("PersistCandleUtils.useDummy");
2481
2481
  this.usePersistCandleAdapter(PersistCandleDummyInstance);
2482
2482
  }
2483
2483
  }
@@ -2615,7 +2615,7 @@ class PersistStorageUtils {
2615
2615
  * @returns Promise resolving to array of signal entries
2616
2616
  */
2617
2617
  this.readStorageData = async (backtest) => {
2618
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
2618
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
2619
2619
  const key = backtest ? `backtest` : `live`;
2620
2620
  const isInitial = !this.getStorage.has(key);
2621
2621
  const instance = this.getStorage(backtest);
@@ -2631,7 +2631,7 @@ class PersistStorageUtils {
2631
2631
  * @returns Promise that resolves when write is complete
2632
2632
  */
2633
2633
  this.writeStorageData = async (signalData, backtest) => {
2634
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
2634
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
2635
2635
  const key = backtest ? `backtest` : `live`;
2636
2636
  const isInitial = !this.getStorage.has(key);
2637
2637
  const instance = this.getStorage(backtest);
@@ -2646,7 +2646,7 @@ class PersistStorageUtils {
2646
2646
  * @param Ctor - Custom IPersistStorageInstance constructor
2647
2647
  */
2648
2648
  usePersistStorageAdapter(Ctor) {
2649
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
2649
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
2650
2650
  this.PersistStorageInstanceCtor = Ctor;
2651
2651
  this.getStorage.clear();
2652
2652
  }
@@ -2655,21 +2655,21 @@ class PersistStorageUtils {
2655
2655
  * Call when process.cwd() changes between strategy iterations.
2656
2656
  */
2657
2657
  clear() {
2658
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
2658
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
2659
2659
  this.getStorage.clear();
2660
2660
  }
2661
2661
  /**
2662
2662
  * Switches to the default file-based PersistStorageInstance.
2663
2663
  */
2664
2664
  useJson() {
2665
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
2665
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
2666
2666
  this.usePersistStorageAdapter(PersistStorageInstance);
2667
2667
  }
2668
2668
  /**
2669
2669
  * Switches to PersistStorageDummyInstance (all operations are no-ops).
2670
2670
  */
2671
2671
  useDummy() {
2672
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
2672
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
2673
2673
  this.usePersistStorageAdapter(PersistStorageDummyInstance);
2674
2674
  }
2675
2675
  }
@@ -2796,7 +2796,7 @@ class PersistNotificationUtils {
2796
2796
  * @returns Promise resolving to array of notification entries
2797
2797
  */
2798
2798
  this.readNotificationData = async (backtest) => {
2799
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
2799
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
2800
2800
  const key = backtest ? `backtest` : `live`;
2801
2801
  const isInitial = !this.getNotificationStorage.has(key);
2802
2802
  const instance = this.getNotificationStorage(backtest);
@@ -2812,7 +2812,7 @@ class PersistNotificationUtils {
2812
2812
  * @returns Promise that resolves when write is complete
2813
2813
  */
2814
2814
  this.writeNotificationData = async (notificationData, backtest) => {
2815
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
2815
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
2816
2816
  const key = backtest ? `backtest` : `live`;
2817
2817
  const isInitial = !this.getNotificationStorage.has(key);
2818
2818
  const instance = this.getNotificationStorage(backtest);
@@ -2827,7 +2827,7 @@ class PersistNotificationUtils {
2827
2827
  * @param Ctor - Custom IPersistNotificationInstance constructor
2828
2828
  */
2829
2829
  usePersistNotificationAdapter(Ctor) {
2830
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
2830
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
2831
2831
  this.PersistNotificationInstanceCtor = Ctor;
2832
2832
  this.getNotificationStorage.clear();
2833
2833
  }
@@ -2837,21 +2837,21 @@ class PersistNotificationUtils {
2837
2837
  * instances are created with the updated base path.
2838
2838
  */
2839
2839
  clear() {
2840
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
2840
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
2841
2841
  this.getNotificationStorage.clear();
2842
2842
  }
2843
2843
  /**
2844
2844
  * Switches to the default file-based PersistNotificationInstance.
2845
2845
  */
2846
2846
  useJson() {
2847
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
2847
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
2848
2848
  this.usePersistNotificationAdapter(PersistNotificationInstance);
2849
2849
  }
2850
2850
  /**
2851
2851
  * Switches to PersistNotificationDummyInstance (all operations are no-ops).
2852
2852
  */
2853
2853
  useDummy() {
2854
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
2854
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
2855
2855
  this.usePersistNotificationAdapter(PersistNotificationDummyInstance);
2856
2856
  }
2857
2857
  }
@@ -2975,7 +2975,7 @@ class PersistLogUtils {
2975
2975
  * @returns Promise resolving to array of log entries
2976
2976
  */
2977
2977
  this.readLogData = async () => {
2978
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
2978
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
2979
2979
  const isInitial = !this._logInstance;
2980
2980
  const instance = this.getLogInstance();
2981
2981
  await instance.waitForInit(isInitial);
@@ -2989,7 +2989,7 @@ class PersistLogUtils {
2989
2989
  * @returns Promise that resolves when write is complete
2990
2990
  */
2991
2991
  this.writeLogData = async (logData) => {
2992
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
2992
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
2993
2993
  const isInitial = !this._logInstance;
2994
2994
  const instance = this.getLogInstance();
2995
2995
  await instance.waitForInit(isInitial);
@@ -3014,7 +3014,7 @@ class PersistLogUtils {
3014
3014
  * @param Ctor - Custom IPersistLogInstance constructor
3015
3015
  */
3016
3016
  usePersistLogAdapter(Ctor) {
3017
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
3017
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
3018
3018
  this.PersistLogInstanceCtor = Ctor;
3019
3019
  this._logInstance = null;
3020
3020
  }
@@ -3023,21 +3023,21 @@ class PersistLogUtils {
3023
3023
  * Call when process.cwd() changes between strategy iterations.
3024
3024
  */
3025
3025
  clear() {
3026
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
3026
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
3027
3027
  this._logInstance = null;
3028
3028
  }
3029
3029
  /**
3030
3030
  * Switches to the default file-based PersistLogInstance.
3031
3031
  */
3032
3032
  useJson() {
3033
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
3033
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
3034
3034
  this.usePersistLogAdapter(PersistLogInstance);
3035
3035
  }
3036
3036
  /**
3037
3037
  * Switches to PersistLogDummyInstance (all operations are no-ops).
3038
3038
  */
3039
3039
  useDummy() {
3040
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
3040
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
3041
3041
  this.usePersistLogAdapter(PersistLogDummyInstance);
3042
3042
  }
3043
3043
  }
@@ -3199,7 +3199,7 @@ class PersistMeasureUtils {
3199
3199
  * @returns Promise resolving to cached value, or null if not found / soft-deleted
3200
3200
  */
3201
3201
  this.readMeasureData = async (bucket, key) => {
3202
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3202
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3203
3203
  const isInitial = !this.getMeasureStorage.has(bucket);
3204
3204
  const instance = this.getMeasureStorage(bucket);
3205
3205
  await instance.waitForInit(isInitial);
@@ -3215,7 +3215,7 @@ class PersistMeasureUtils {
3215
3215
  * @returns Promise that resolves when write is complete
3216
3216
  */
3217
3217
  this.writeMeasureData = async (data, bucket, key, when) => {
3218
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3218
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3219
3219
  const isInitial = !this.getMeasureStorage.has(bucket);
3220
3220
  const instance = this.getMeasureStorage(bucket);
3221
3221
  await instance.waitForInit(isInitial);
@@ -3230,7 +3230,7 @@ class PersistMeasureUtils {
3230
3230
  * @returns Promise that resolves when removal is complete
3231
3231
  */
3232
3232
  this.removeMeasureData = async (bucket, key) => {
3233
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3233
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3234
3234
  const isInitial = !this.getMeasureStorage.has(bucket);
3235
3235
  const instance = this.getMeasureStorage(bucket);
3236
3236
  await instance.waitForInit(isInitial);
@@ -3244,7 +3244,7 @@ class PersistMeasureUtils {
3244
3244
  * @param Ctor - Custom IPersistMeasureInstance constructor
3245
3245
  */
3246
3246
  usePersistMeasureAdapter(Ctor) {
3247
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
3247
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
3248
3248
  this.PersistMeasureInstanceCtor = Ctor;
3249
3249
  this.getMeasureStorage.clear();
3250
3250
  }
@@ -3256,7 +3256,7 @@ class PersistMeasureUtils {
3256
3256
  * @returns AsyncGenerator yielding entry keys
3257
3257
  */
3258
3258
  async *listMeasureData(bucket) {
3259
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3259
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3260
3260
  const isInitial = !this.getMeasureStorage.has(bucket);
3261
3261
  const instance = this.getMeasureStorage(bucket);
3262
3262
  await instance.waitForInit(isInitial);
@@ -3267,21 +3267,21 @@ class PersistMeasureUtils {
3267
3267
  * Call when process.cwd() changes between strategy iterations.
3268
3268
  */
3269
3269
  clear() {
3270
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
3270
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
3271
3271
  this.getMeasureStorage.clear();
3272
3272
  }
3273
3273
  /**
3274
3274
  * Switches to the default file-based PersistMeasureInstance.
3275
3275
  */
3276
3276
  useJson() {
3277
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
3277
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
3278
3278
  this.usePersistMeasureAdapter(PersistMeasureInstance);
3279
3279
  }
3280
3280
  /**
3281
3281
  * Switches to PersistMeasureDummyInstance (all operations are no-ops).
3282
3282
  */
3283
3283
  useDummy() {
3284
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
3284
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
3285
3285
  this.usePersistMeasureAdapter(PersistMeasureDummyInstance);
3286
3286
  }
3287
3287
  }
@@ -3440,7 +3440,7 @@ class PersistIntervalUtils {
3440
3440
  * @returns Promise resolving to marker data, or null if not found / soft-deleted
3441
3441
  */
3442
3442
  this.readIntervalData = async (bucket, key) => {
3443
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3443
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3444
3444
  const isInitial = !this.getIntervalStorage.has(bucket);
3445
3445
  const instance = this.getIntervalStorage(bucket);
3446
3446
  await instance.waitForInit(isInitial);
@@ -3456,7 +3456,7 @@ class PersistIntervalUtils {
3456
3456
  * @returns Promise that resolves when write is complete
3457
3457
  */
3458
3458
  this.writeIntervalData = async (data, bucket, key, when) => {
3459
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3459
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3460
3460
  const isInitial = !this.getIntervalStorage.has(bucket);
3461
3461
  const instance = this.getIntervalStorage(bucket);
3462
3462
  await instance.waitForInit(isInitial);
@@ -3471,7 +3471,7 @@ class PersistIntervalUtils {
3471
3471
  * @returns Promise that resolves when removal is complete
3472
3472
  */
3473
3473
  this.removeIntervalData = async (bucket, key) => {
3474
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3474
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3475
3475
  const isInitial = !this.getIntervalStorage.has(bucket);
3476
3476
  const instance = this.getIntervalStorage(bucket);
3477
3477
  await instance.waitForInit(isInitial);
@@ -3485,7 +3485,7 @@ class PersistIntervalUtils {
3485
3485
  * @param Ctor - Custom IPersistIntervalInstance constructor
3486
3486
  */
3487
3487
  usePersistIntervalAdapter(Ctor) {
3488
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
3488
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
3489
3489
  this.PersistIntervalInstanceCtor = Ctor;
3490
3490
  this.getIntervalStorage.clear();
3491
3491
  }
@@ -3497,7 +3497,7 @@ class PersistIntervalUtils {
3497
3497
  * @returns AsyncGenerator yielding marker keys
3498
3498
  */
3499
3499
  async *listIntervalData(bucket) {
3500
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3500
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3501
3501
  const isInitial = !this.getIntervalStorage.has(bucket);
3502
3502
  const instance = this.getIntervalStorage(bucket);
3503
3503
  await instance.waitForInit(isInitial);
@@ -3508,21 +3508,21 @@ class PersistIntervalUtils {
3508
3508
  * Call when process.cwd() changes between strategy iterations.
3509
3509
  */
3510
3510
  clear() {
3511
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
3511
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
3512
3512
  this.getIntervalStorage.clear();
3513
3513
  }
3514
3514
  /**
3515
3515
  * Switches to the default file-based PersistIntervalInstance.
3516
3516
  */
3517
3517
  useJson() {
3518
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
3518
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
3519
3519
  this.usePersistIntervalAdapter(PersistIntervalInstance);
3520
3520
  }
3521
3521
  /**
3522
3522
  * Switches to PersistIntervalDummyInstance (all operations are no-ops).
3523
3523
  */
3524
3524
  useDummy() {
3525
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
3525
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
3526
3526
  this.usePersistIntervalAdapter(PersistIntervalDummyInstance);
3527
3527
  }
3528
3528
  }
@@ -3728,7 +3728,7 @@ class PersistMemoryUtils {
3728
3728
  * @returns Promise resolving to entry data, or null if not found / soft-deleted
3729
3729
  */
3730
3730
  this.readMemoryData = async (signalId, bucketName, memoryId) => {
3731
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
3731
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
3732
3732
  const key = `${signalId}:${bucketName}`;
3733
3733
  const isInitial = !this.getMemoryStorage.has(key);
3734
3734
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3745,7 +3745,7 @@ class PersistMemoryUtils {
3745
3745
  * @returns Promise resolving to true if entry exists
3746
3746
  */
3747
3747
  this.hasMemoryData = async (signalId, bucketName, memoryId) => {
3748
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
3748
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
3749
3749
  const key = `${signalId}:${bucketName}`;
3750
3750
  const isInitial = !this.getMemoryStorage.has(key);
3751
3751
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3764,7 +3764,7 @@ class PersistMemoryUtils {
3764
3764
  * @returns Promise that resolves when write is complete
3765
3765
  */
3766
3766
  this.writeMemoryData = async (data, signalId, bucketName, memoryId, when) => {
3767
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
3767
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
3768
3768
  const key = `${signalId}:${bucketName}`;
3769
3769
  const isInitial = !this.getMemoryStorage.has(key);
3770
3770
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3781,7 +3781,7 @@ class PersistMemoryUtils {
3781
3781
  * @returns Promise that resolves when removal is complete
3782
3782
  */
3783
3783
  this.removeMemoryData = async (signalId, bucketName, memoryId) => {
3784
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
3784
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
3785
3785
  const key = `${signalId}:${bucketName}`;
3786
3786
  const isInitial = !this.getMemoryStorage.has(key);
3787
3787
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3793,7 +3793,7 @@ class PersistMemoryUtils {
3793
3793
  * Call when process.cwd() changes between strategy iterations.
3794
3794
  */
3795
3795
  this.clear = () => {
3796
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
3796
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
3797
3797
  this.getMemoryStorage.clear();
3798
3798
  };
3799
3799
  /**
@@ -3804,7 +3804,7 @@ class PersistMemoryUtils {
3804
3804
  * @param bucketName - Bucket name
3805
3805
  */
3806
3806
  this.dispose = (signalId, bucketName) => {
3807
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
3807
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
3808
3808
  const key = `${signalId}:${bucketName}`;
3809
3809
  this.getMemoryStorage.clear(key);
3810
3810
  };
@@ -3816,7 +3816,7 @@ class PersistMemoryUtils {
3816
3816
  * @param Ctor - Custom IPersistMemoryInstance constructor
3817
3817
  */
3818
3818
  usePersistMemoryAdapter(Ctor) {
3819
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
3819
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
3820
3820
  this.PersistMemoryInstanceCtor = Ctor;
3821
3821
  this.getMemoryStorage.clear();
3822
3822
  }
@@ -3830,7 +3830,7 @@ class PersistMemoryUtils {
3830
3830
  * @returns AsyncGenerator yielding `{ memoryId, data }` tuples
3831
3831
  */
3832
3832
  async *listMemoryData(signalId, bucketName) {
3833
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
3833
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
3834
3834
  const key = `${signalId}:${bucketName}`;
3835
3835
  const isInitial = !this.getMemoryStorage.has(key);
3836
3836
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3841,14 +3841,14 @@ class PersistMemoryUtils {
3841
3841
  * Switches to the default file-based PersistMemoryInstance.
3842
3842
  */
3843
3843
  useJson() {
3844
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
3844
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
3845
3845
  this.usePersistMemoryAdapter(PersistMemoryInstance);
3846
3846
  }
3847
3847
  /**
3848
3848
  * Switches to PersistMemoryDummyInstance (all operations are no-ops).
3849
3849
  */
3850
3850
  useDummy() {
3851
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
3851
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
3852
3852
  this.usePersistMemoryAdapter(PersistMemoryDummyInstance);
3853
3853
  }
3854
3854
  }
@@ -3997,7 +3997,7 @@ class PersistRecentUtils {
3997
3997
  * @returns Promise resolving to recent signal or null if none persisted
3998
3998
  */
3999
3999
  this.readRecentData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
4000
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
4000
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
4001
4001
  const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
4002
4002
  const isInitial = !this.getStorage.has(key);
4003
4003
  const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
@@ -4018,7 +4018,7 @@ class PersistRecentUtils {
4018
4018
  * @returns Promise that resolves when write is complete
4019
4019
  */
4020
4020
  this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest, when) => {
4021
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
4021
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
4022
4022
  const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
4023
4023
  const isInitial = !this.getStorage.has(key);
4024
4024
  const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
@@ -4051,7 +4051,7 @@ class PersistRecentUtils {
4051
4051
  * @param Ctor - Custom IPersistRecentInstance constructor
4052
4052
  */
4053
4053
  usePersistRecentAdapter(Ctor) {
4054
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
4054
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
4055
4055
  this.PersistRecentInstanceCtor = Ctor;
4056
4056
  this.getStorage.clear();
4057
4057
  }
@@ -4060,21 +4060,21 @@ class PersistRecentUtils {
4060
4060
  * Call when process.cwd() changes between strategy iterations.
4061
4061
  */
4062
4062
  clear() {
4063
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
4063
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
4064
4064
  this.getStorage.clear();
4065
4065
  }
4066
4066
  /**
4067
4067
  * Switches to the default file-based PersistRecentInstance.
4068
4068
  */
4069
4069
  useJson() {
4070
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
4070
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
4071
4071
  this.usePersistRecentAdapter(PersistRecentInstance);
4072
4072
  }
4073
4073
  /**
4074
4074
  * Switches to PersistRecentDummyInstance (all operations are no-ops).
4075
4075
  */
4076
4076
  useDummy() {
4077
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
4077
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
4078
4078
  this.usePersistRecentAdapter(PersistRecentDummyInstance);
4079
4079
  }
4080
4080
  }
@@ -4209,7 +4209,7 @@ class PersistStateUtils {
4209
4209
  * @returns Promise that resolves when initialization is complete
4210
4210
  */
4211
4211
  this.waitForInit = async (signalId, bucketName, initial) => {
4212
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
4212
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
4213
4213
  const key = `${signalId}:${bucketName}`;
4214
4214
  const isInitial = initial && !this.getStateStorage.has(key);
4215
4215
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4224,7 +4224,7 @@ class PersistStateUtils {
4224
4224
  * @returns Promise resolving to state data or null if none persisted
4225
4225
  */
4226
4226
  this.readStateData = async (signalId, bucketName) => {
4227
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
4227
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
4228
4228
  const key = `${signalId}:${bucketName}`;
4229
4229
  const isInitial = !this.getStateStorage.has(key);
4230
4230
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4242,7 +4242,7 @@ class PersistStateUtils {
4242
4242
  * @returns Promise that resolves when write is complete
4243
4243
  */
4244
4244
  this.writeStateData = async (data, signalId, bucketName, when) => {
4245
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
4245
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
4246
4246
  const key = `${signalId}:${bucketName}`;
4247
4247
  const isInitial = !this.getStateStorage.has(key);
4248
4248
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4253,14 +4253,14 @@ class PersistStateUtils {
4253
4253
  * Switches to PersistStateDummyInstance (all operations are no-ops).
4254
4254
  */
4255
4255
  this.useDummy = () => {
4256
- LOGGER_SERVICE$8.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
4256
+ LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
4257
4257
  this.usePersistStateAdapter(PersistStateDummyInstance);
4258
4258
  };
4259
4259
  /**
4260
4260
  * Switches to the default file-based PersistStateInstance.
4261
4261
  */
4262
4262
  this.useJson = () => {
4263
- LOGGER_SERVICE$8.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
4263
+ LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
4264
4264
  this.usePersistStateAdapter(PersistStateInstance);
4265
4265
  };
4266
4266
  /**
@@ -4268,7 +4268,7 @@ class PersistStateUtils {
4268
4268
  * Call when process.cwd() changes between strategy iterations.
4269
4269
  */
4270
4270
  this.clear = () => {
4271
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
4271
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
4272
4272
  this.getStateStorage.clear();
4273
4273
  };
4274
4274
  /**
@@ -4279,7 +4279,7 @@ class PersistStateUtils {
4279
4279
  * @param bucketName - Bucket name
4280
4280
  */
4281
4281
  this.dispose = (signalId, bucketName) => {
4282
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
4282
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
4283
4283
  const key = `${signalId}:${bucketName}`;
4284
4284
  this.getStateStorage.clear(key);
4285
4285
  };
@@ -4291,7 +4291,7 @@ class PersistStateUtils {
4291
4291
  * @param Ctor - Custom IPersistStateInstance constructor
4292
4292
  */
4293
4293
  usePersistStateAdapter(Ctor) {
4294
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
4294
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
4295
4295
  this.PersistStateInstanceCtor = Ctor;
4296
4296
  this.getStateStorage.clear();
4297
4297
  }
@@ -4431,7 +4431,7 @@ class PersistSessionUtils {
4431
4431
  * @returns Promise that resolves when initialization is complete
4432
4432
  */
4433
4433
  this.waitForInit = async (strategyName, exchangeName, frameName, initial) => {
4434
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
4434
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
4435
4435
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4436
4436
  const isInitial = initial && !this.getSessionStorage.has(key);
4437
4437
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4447,7 +4447,7 @@ class PersistSessionUtils {
4447
4447
  * @returns Promise resolving to session data or null if none persisted
4448
4448
  */
4449
4449
  this.readSessionData = async (strategyName, exchangeName, frameName) => {
4450
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
4450
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
4451
4451
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4452
4452
  const isInitial = !this.getSessionStorage.has(key);
4453
4453
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4466,7 +4466,7 @@ class PersistSessionUtils {
4466
4466
  * @returns Promise that resolves when write is complete
4467
4467
  */
4468
4468
  this.writeSessionData = async (data, strategyName, exchangeName, frameName, when) => {
4469
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
4469
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
4470
4470
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4471
4471
  const isInitial = !this.getSessionStorage.has(key);
4472
4472
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4477,14 +4477,14 @@ class PersistSessionUtils {
4477
4477
  * Switches to PersistSessionDummyInstance (all operations are no-ops).
4478
4478
  */
4479
4479
  this.useDummy = () => {
4480
- LOGGER_SERVICE$8.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
4480
+ LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
4481
4481
  this.usePersistSessionAdapter(PersistSessionDummyInstance);
4482
4482
  };
4483
4483
  /**
4484
4484
  * Switches to the default file-based PersistSessionInstance.
4485
4485
  */
4486
4486
  this.useJson = () => {
4487
- LOGGER_SERVICE$8.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
4487
+ LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
4488
4488
  this.usePersistSessionAdapter(PersistSessionInstance);
4489
4489
  };
4490
4490
  /**
@@ -4492,7 +4492,7 @@ class PersistSessionUtils {
4492
4492
  * Call when process.cwd() changes between strategy iterations.
4493
4493
  */
4494
4494
  this.clear = () => {
4495
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
4495
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
4496
4496
  this.getSessionStorage.clear();
4497
4497
  };
4498
4498
  /**
@@ -4504,7 +4504,7 @@ class PersistSessionUtils {
4504
4504
  * @param frameName - Frame identifier
4505
4505
  */
4506
4506
  this.dispose = (strategyName, exchangeName, frameName) => {
4507
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
4507
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
4508
4508
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4509
4509
  this.getSessionStorage.clear(key);
4510
4510
  };
@@ -4516,7 +4516,7 @@ class PersistSessionUtils {
4516
4516
  * @param Ctor - Custom IPersistSessionInstance constructor
4517
4517
  */
4518
4518
  usePersistSessionAdapter(Ctor) {
4519
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
4519
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
4520
4520
  this.PersistSessionInstanceCtor = Ctor;
4521
4521
  this.getSessionStorage.clear();
4522
4522
  }
@@ -4638,7 +4638,7 @@ const METHOD_NAME_ADD_ACTIVITY = "LookupUtils.addActivity";
4638
4638
  const METHOD_NAME_REMOVE_ACTIVITY = "LookupUtils.removeActivity";
4639
4639
  const METHOD_NAME_LIST_ACTIVITY = "LookupUtils.listActivity";
4640
4640
  /** Logger service injected as DI singleton */
4641
- const LOGGER_SERVICE$7 = new LoggerService();
4641
+ const LOGGER_SERVICE$8 = new LoggerService();
4642
4642
  /**
4643
4643
  * Builds the composite {@link Key} used to register an activity in `_lookupMap`.
4644
4644
  *
@@ -4692,7 +4692,7 @@ class LookupUtils {
4692
4692
  * @param activity - Activity descriptor identifying the running workload.
4693
4693
  */
4694
4694
  this.addActivity = (activity) => {
4695
- LOGGER_SERVICE$7.info(METHOD_NAME_ADD_ACTIVITY, {
4695
+ LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
4696
4696
  activity,
4697
4697
  });
4698
4698
  const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
@@ -4706,7 +4706,7 @@ class LookupUtils {
4706
4706
  * @param activity - Activity descriptor matching the one passed to {@link addActivity}.
4707
4707
  */
4708
4708
  this.removeActivity = (activity) => {
4709
- LOGGER_SERVICE$7.info(METHOD_NAME_REMOVE_ACTIVITY, {
4709
+ LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
4710
4710
  activity,
4711
4711
  });
4712
4712
  const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
@@ -4718,7 +4718,7 @@ class LookupUtils {
4718
4718
  * @returns Array of all activities present in the lookup map at call time.
4719
4719
  */
4720
4720
  this.listActivity = () => {
4721
- LOGGER_SERVICE$7.info(METHOD_NAME_LIST_ACTIVITY);
4721
+ LOGGER_SERVICE$8.info(METHOD_NAME_LIST_ACTIVITY);
4722
4722
  return Array.from(this._lookupMap.values());
4723
4723
  };
4724
4724
  }
@@ -4747,7 +4747,7 @@ const METHOD_NAME_SPIN_LOCK = "CandleUtils.spinLock";
4747
4747
  */
4748
4748
  const ROTATE_DELAY = 50;
4749
4749
  /** Logger service injected as DI singleton */
4750
- const LOGGER_SERVICE$6 = new LoggerService();
4750
+ const LOGGER_SERVICE$7 = new LoggerService();
4751
4751
  /**
4752
4752
  * Process-wide coordinator for candle-fetch serialization and cooperative
4753
4753
  * yielding between parallel backtests.
@@ -4796,7 +4796,7 @@ class CandleUtils {
4796
4796
  * @param source - Caller identifier for logging.
4797
4797
  */
4798
4798
  this.acquireLock = async (source) => {
4799
- LOGGER_SERVICE$6.info(METHOD_NAME_ACQUIRE_LOCK, {
4799
+ LOGGER_SERVICE$7.info(METHOD_NAME_ACQUIRE_LOCK, {
4800
4800
  source,
4801
4801
  });
4802
4802
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -4812,7 +4812,7 @@ class CandleUtils {
4812
4812
  * @param source - Caller identifier for logging.
4813
4813
  */
4814
4814
  this.releaseLock = async (source) => {
4815
- LOGGER_SERVICE$6.info(METHOD_NAME_RELEASE_LOCK, {
4815
+ LOGGER_SERVICE$7.info(METHOD_NAME_RELEASE_LOCK, {
4816
4816
  source,
4817
4817
  });
4818
4818
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -4837,7 +4837,7 @@ class CandleUtils {
4837
4837
  * @param source - Caller identifier for logging.
4838
4838
  */
4839
4839
  this.spinLock = async (source) => {
4840
- LOGGER_SERVICE$6.info(METHOD_NAME_SPIN_LOCK, {
4840
+ LOGGER_SERVICE$7.info(METHOD_NAME_SPIN_LOCK, {
4841
4841
  source,
4842
4842
  });
4843
4843
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -6347,7 +6347,7 @@ const validateCommonSignal = (signal) => {
6347
6347
  }
6348
6348
  // Кидаем ошибку если есть проблемы
6349
6349
  if (errors.length > 0) {
6350
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6350
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6351
6351
  }
6352
6352
  };
6353
6353
 
@@ -6400,7 +6400,7 @@ const validatePendingSignal = (signal, currentPrice) => {
6400
6400
  }
6401
6401
  }
6402
6402
  if (errors.length > 0) {
6403
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6403
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6404
6404
  }
6405
6405
  validateCommonSignal(signal);
6406
6406
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
@@ -6448,7 +6448,7 @@ const validatePendingSignal = (signal, currentPrice) => {
6448
6448
  }
6449
6449
  }
6450
6450
  if (errors.length > 0) {
6451
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6451
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6452
6452
  }
6453
6453
  };
6454
6454
 
@@ -6501,7 +6501,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
6501
6501
  }
6502
6502
  }
6503
6503
  if (errors.length > 0) {
6504
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6504
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6505
6505
  }
6506
6506
  validateCommonSignal(signal);
6507
6507
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
@@ -6547,7 +6547,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
6547
6547
  // pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
6548
6548
  }
6549
6549
  if (errors.length > 0) {
6550
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6550
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6551
6551
  }
6552
6552
  };
6553
6553
 
@@ -6994,6 +6994,13 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
6994
6994
  if (!signal) {
6995
6995
  return null;
6996
6996
  }
6997
+ if (signal?.symbol && signal?.symbol !== self.params.execution.context.symbol) {
6998
+ throw new Error(`Symbol mismatch: expected ${self.params.execution.context.symbol}, got ${signal.symbol}`);
6999
+ }
7000
+ // Whipsaw protection: skip signal if its id matches the last accepted pending id
7001
+ if (signal.id && signal.id === self._lastPendingId) {
7002
+ return null;
7003
+ }
6997
7004
  if (self._isStopped) {
6998
7005
  return null;
6999
7006
  }
@@ -7037,6 +7044,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
7037
7044
  }
7038
7045
  // Валидируем сигнал перед возвратом
7039
7046
  validatePendingSignal(signalRow, currentPrice);
7047
+ if (signal.id) {
7048
+ self._lastPendingId = signal.id;
7049
+ }
7040
7050
  return signalRow;
7041
7051
  }
7042
7052
  // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
@@ -7063,6 +7073,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
7063
7073
  };
7064
7074
  // Валидируем сигнал перед возвратом
7065
7075
  validateScheduledSignal(scheduledSignalRow, currentPrice);
7076
+ if (signal.id) {
7077
+ self._lastPendingId = signal.id;
7078
+ }
7066
7079
  return scheduledSignalRow;
7067
7080
  }
7068
7081
  const signalRow = {
@@ -7090,6 +7103,9 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
7090
7103
  }
7091
7104
  // Валидируем сигнал перед возвратом
7092
7105
  validatePendingSignal(signalRow, currentPrice);
7106
+ if (signal.id) {
7107
+ self._lastPendingId = signal.id;
7108
+ }
7093
7109
  return signalRow;
7094
7110
  }, {
7095
7111
  defaultValue: null,
@@ -7119,6 +7135,13 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7119
7135
  if (self.params.execution.context.backtest) {
7120
7136
  return;
7121
7137
  }
7138
+ // Restore last pending signal id for whipsaw protection in GET_SIGNAL_FN
7139
+ {
7140
+ const recentSignal = await PersistRecentAdapter.readRecentData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, false);
7141
+ if (recentSignal?.id) {
7142
+ self._lastPendingId = recentSignal.id;
7143
+ }
7144
+ }
7122
7145
  // Restore pending signal
7123
7146
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7124
7147
  if (pendingSignal) {
@@ -9259,6 +9282,7 @@ class ClientStrategy {
9259
9282
  this._isStopped = false;
9260
9283
  this._pendingSignal = null;
9261
9284
  this._lastSignalTimestamp = null;
9285
+ this._lastPendingId = null;
9262
9286
  this._scheduledSignal = null;
9263
9287
  this._cancelledSignal = null;
9264
9288
  this._closedSignal = null;
@@ -12048,7 +12072,7 @@ const RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE = "MergeRisk.checkSignalAndReser
12048
12072
  const RISK_METHOD_NAME_ADD_SIGNAL = "MergeRisk.addSignal";
12049
12073
  const RISK_METHOD_NAME_REMOVE_SIGNAL = "MergeRisk.removeSignal";
12050
12074
  /** Logger service injected as DI singleton */
12051
- const LOGGER_SERVICE$5 = new LoggerService();
12075
+ const LOGGER_SERVICE$6 = new LoggerService();
12052
12076
  /**
12053
12077
  * Composite risk management class that combines multiple risk profiles.
12054
12078
  *
@@ -12104,7 +12128,7 @@ class MergeRisk {
12104
12128
  * @returns Promise resolving to true if all risks approve, false if any risk rejects
12105
12129
  */
12106
12130
  async checkSignal(params, options = {}) {
12107
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
12131
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
12108
12132
  params,
12109
12133
  });
12110
12134
  for (const [riskName, risk] of Object.entries(this._riskMap)) {
@@ -12134,7 +12158,7 @@ class MergeRisk {
12134
12158
  * @returns Promise resolving to true if all risks approve (and reserved), false if any risk rejects
12135
12159
  */
12136
12160
  async checkSignalAndReserve(params) {
12137
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
12161
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
12138
12162
  params,
12139
12163
  });
12140
12164
  for (const [riskName, risk] of Object.entries(this._riskMap)) {
@@ -12158,7 +12182,7 @@ class MergeRisk {
12158
12182
  * @returns Promise that resolves when all risks have registered the signal
12159
12183
  */
12160
12184
  async addSignal(symbol, context, positionData) {
12161
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_ADD_SIGNAL, {
12185
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_ADD_SIGNAL, {
12162
12186
  symbol,
12163
12187
  context,
12164
12188
  });
@@ -12175,7 +12199,7 @@ class MergeRisk {
12175
12199
  * @returns Promise that resolves when all risks have removed the signal
12176
12200
  */
12177
12201
  async removeSignal(symbol, context) {
12178
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
12202
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
12179
12203
  symbol,
12180
12204
  context,
12181
12205
  });
@@ -14968,7 +14992,7 @@ class RiskConnectionService {
14968
14992
  }
14969
14993
 
14970
14994
  /** Logger service injected as DI singleton */
14971
- const LOGGER_SERVICE$4 = new LoggerService();
14995
+ const LOGGER_SERVICE$5 = new LoggerService();
14972
14996
  /**
14973
14997
  * Wrapper to call init method with error capture.
14974
14998
  */
@@ -14983,7 +15007,7 @@ const CALL_INIT_FN = trycatch(async (self) => {
14983
15007
  error: errorData(error),
14984
15008
  message: getErrorMessage(error),
14985
15009
  };
14986
- LOGGER_SERVICE$4.warn(message, payload);
15010
+ LOGGER_SERVICE$5.warn(message, payload);
14987
15011
  console.warn(message, payload);
14988
15012
  errorEmitter.next(error);
14989
15013
  },
@@ -15003,7 +15027,7 @@ const CALL_SIGNAL_FN = trycatch(async (event, self) => {
15003
15027
  error: errorData(error),
15004
15028
  message: getErrorMessage(error),
15005
15029
  };
15006
- LOGGER_SERVICE$4.warn(message, payload);
15030
+ LOGGER_SERVICE$5.warn(message, payload);
15007
15031
  console.warn(message, payload);
15008
15032
  errorEmitter.next(error);
15009
15033
  },
@@ -15023,7 +15047,7 @@ const CALL_SIGNAL_LIVE_FN = trycatch(async (event, self) => {
15023
15047
  error: errorData(error),
15024
15048
  message: getErrorMessage(error),
15025
15049
  };
15026
- LOGGER_SERVICE$4.warn(message, payload);
15050
+ LOGGER_SERVICE$5.warn(message, payload);
15027
15051
  console.warn(message, payload);
15028
15052
  errorEmitter.next(error);
15029
15053
  },
@@ -15043,7 +15067,7 @@ const CALL_SIGNAL_BACKTEST_FN = trycatch(async (event, self) => {
15043
15067
  error: errorData(error),
15044
15068
  message: getErrorMessage(error),
15045
15069
  };
15046
- LOGGER_SERVICE$4.warn(message, payload);
15070
+ LOGGER_SERVICE$5.warn(message, payload);
15047
15071
  console.warn(message, payload);
15048
15072
  errorEmitter.next(error);
15049
15073
  },
@@ -15071,7 +15095,7 @@ const CALL_BREAKEVEN_AVAILABLE_FN = trycatch(async (event, self) => {
15071
15095
  error: errorData(error),
15072
15096
  message: getErrorMessage(error),
15073
15097
  };
15074
- LOGGER_SERVICE$4.warn(message, payload);
15098
+ LOGGER_SERVICE$5.warn(message, payload);
15075
15099
  console.warn(message, payload);
15076
15100
  errorEmitter.next(error);
15077
15101
  },
@@ -15099,7 +15123,7 @@ const CALL_PARTIAL_PROFIT_AVAILABLE_FN = trycatch(async (event, self) => {
15099
15123
  error: errorData(error),
15100
15124
  message: getErrorMessage(error),
15101
15125
  };
15102
- LOGGER_SERVICE$4.warn(message, payload);
15126
+ LOGGER_SERVICE$5.warn(message, payload);
15103
15127
  console.warn(message, payload);
15104
15128
  errorEmitter.next(error);
15105
15129
  },
@@ -15127,7 +15151,7 @@ const CALL_PARTIAL_LOSS_AVAILABLE_FN = trycatch(async (event, self) => {
15127
15151
  error: errorData(error),
15128
15152
  message: getErrorMessage(error),
15129
15153
  };
15130
- LOGGER_SERVICE$4.warn(message, payload);
15154
+ LOGGER_SERVICE$5.warn(message, payload);
15131
15155
  console.warn(message, payload);
15132
15156
  errorEmitter.next(error);
15133
15157
  },
@@ -15155,7 +15179,7 @@ const CALL_PING_SCHEDULED_FN = trycatch(async (event, self) => {
15155
15179
  error: errorData(error),
15156
15180
  message: getErrorMessage(error),
15157
15181
  };
15158
- LOGGER_SERVICE$4.warn(message, payload);
15182
+ LOGGER_SERVICE$5.warn(message, payload);
15159
15183
  console.warn(message, payload);
15160
15184
  errorEmitter.next(error);
15161
15185
  },
@@ -15183,7 +15207,7 @@ const CALL_PING_IDLE_FN = trycatch(async (event, self) => {
15183
15207
  error: errorData(error),
15184
15208
  message: getErrorMessage(error),
15185
15209
  };
15186
- LOGGER_SERVICE$4.warn(message, payload);
15210
+ LOGGER_SERVICE$5.warn(message, payload);
15187
15211
  console.warn(message, payload);
15188
15212
  errorEmitter.next(error);
15189
15213
  },
@@ -15211,7 +15235,7 @@ const CALL_PING_ACTIVE_FN = trycatch(async (event, self) => {
15211
15235
  error: errorData(error),
15212
15236
  message: getErrorMessage(error),
15213
15237
  };
15214
- LOGGER_SERVICE$4.warn(message, payload);
15238
+ LOGGER_SERVICE$5.warn(message, payload);
15215
15239
  console.warn(message, payload);
15216
15240
  errorEmitter.next(error);
15217
15241
  },
@@ -15231,7 +15255,7 @@ const CALL_RISK_REJECTION_FN = trycatch(async (event, self) => {
15231
15255
  error: errorData(error),
15232
15256
  message: getErrorMessage(error),
15233
15257
  };
15234
- LOGGER_SERVICE$4.warn(message, payload);
15258
+ LOGGER_SERVICE$5.warn(message, payload);
15235
15259
  console.warn(message, payload);
15236
15260
  errorEmitter.next(error);
15237
15261
  },
@@ -15251,7 +15275,7 @@ const CALL_DISPOSE_FN = trycatch(async (self) => {
15251
15275
  error: errorData(error),
15252
15276
  message: getErrorMessage(error),
15253
15277
  };
15254
- LOGGER_SERVICE$4.warn(message, payload);
15278
+ LOGGER_SERVICE$5.warn(message, payload);
15255
15279
  console.warn(message, payload);
15256
15280
  errorEmitter.next(error);
15257
15281
  },
@@ -23115,7 +23139,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY$1 = "ReportUtils.useDummy";
23115
23139
  const REPORT_UTILS_METHOD_NAME_USE_JSONL$1 = "ReportUtils.useJsonl";
23116
23140
  const REPORT_UTILS_METHOD_NAME_CLEAR$1 = "ReportUtils.clear";
23117
23141
  /** Logger service injected as DI singleton */
23118
- const LOGGER_SERVICE$3 = new LoggerService();
23142
+ const LOGGER_SERVICE$4 = new LoggerService();
23119
23143
  /** Symbol key for the singleshot waitForInit function on MarkdownFileBase instances. */
23120
23144
  const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
23121
23145
  /** Symbol key for the timeout-protected write function on MarkdownFileBase instances. */
@@ -23196,7 +23220,7 @@ class MarkdownFileBase {
23196
23220
  * @throws Error if stream not initialized or write timeout exceeded
23197
23221
  */
23198
23222
  async dump(data, options) {
23199
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
23223
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
23200
23224
  markdownName: this.markdownName,
23201
23225
  options,
23202
23226
  });
@@ -23276,7 +23300,7 @@ class MarkdownFolderBase {
23276
23300
  * @throws Error if directory creation or file write fails
23277
23301
  */
23278
23302
  async dump(content, options) {
23279
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
23303
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
23280
23304
  markdownName: this.markdownName,
23281
23305
  options,
23282
23306
  });
@@ -23341,7 +23365,7 @@ class MarkdownWriterAdapter {
23341
23365
  * @param Ctor - Constructor for markdown storage adapter
23342
23366
  */
23343
23367
  useMarkdownAdapter(Ctor) {
23344
- LOGGER_SERVICE$3.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
23368
+ LOGGER_SERVICE$4.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
23345
23369
  this.MarkdownFactory = Ctor;
23346
23370
  }
23347
23371
  /**
@@ -23355,7 +23379,7 @@ class MarkdownWriterAdapter {
23355
23379
  * @throws Error if write fails or storage initialization fails
23356
23380
  */
23357
23381
  async writeData(markdownName, content, options) {
23358
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
23382
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
23359
23383
  markdownName,
23360
23384
  options,
23361
23385
  });
@@ -23369,7 +23393,7 @@ class MarkdownWriterAdapter {
23369
23393
  * Each report is written as a separate .md file.
23370
23394
  */
23371
23395
  useMd() {
23372
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
23396
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
23373
23397
  this.useMarkdownAdapter(MarkdownFolderBase);
23374
23398
  }
23375
23399
  /**
@@ -23377,7 +23401,7 @@ class MarkdownWriterAdapter {
23377
23401
  * All reports are appended to a single .jsonl file per markdown type.
23378
23402
  */
23379
23403
  useJsonl() {
23380
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
23404
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
23381
23405
  this.useMarkdownAdapter(MarkdownFileBase);
23382
23406
  }
23383
23407
  /**
@@ -23386,7 +23410,7 @@ class MarkdownWriterAdapter {
23386
23410
  * so new storage instances are created with the updated base path.
23387
23411
  */
23388
23412
  clear() {
23389
- LOGGER_SERVICE$3.log(MARKDOWN_METHOD_NAME_CLEAR$1);
23413
+ LOGGER_SERVICE$4.log(MARKDOWN_METHOD_NAME_CLEAR$1);
23390
23414
  this.getMarkdownStorage.clear();
23391
23415
  }
23392
23416
  /**
@@ -23394,7 +23418,7 @@ class MarkdownWriterAdapter {
23394
23418
  * All future markdown writes will be no-ops.
23395
23419
  */
23396
23420
  useDummy() {
23397
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
23421
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
23398
23422
  this.useMarkdownAdapter(MarkdownDummy);
23399
23423
  }
23400
23424
  }
@@ -23455,7 +23479,7 @@ class ReportBase {
23455
23479
  });
23456
23480
  }
23457
23481
  }, 15000));
23458
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_CTOR, {
23482
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_CTOR, {
23459
23483
  reportName: this.reportName,
23460
23484
  baseDir,
23461
23485
  });
@@ -23469,7 +23493,7 @@ class ReportBase {
23469
23493
  * @returns Promise that resolves when initialization is complete
23470
23494
  */
23471
23495
  async waitForInit(initial) {
23472
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
23496
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
23473
23497
  reportName: this.reportName,
23474
23498
  initial,
23475
23499
  });
@@ -23488,7 +23512,7 @@ class ReportBase {
23488
23512
  * @throws Error if stream not initialized or write timeout exceeded
23489
23513
  */
23490
23514
  async write(data, options) {
23491
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_WRITE, {
23515
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WRITE, {
23492
23516
  reportName: this.reportName,
23493
23517
  options,
23494
23518
  });
@@ -23585,7 +23609,7 @@ class ReportWriterAdapter {
23585
23609
  * @internal - Automatically called by report services, not for direct use
23586
23610
  */
23587
23611
  this.writeData = async (reportName, data, options) => {
23588
- LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
23612
+ LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
23589
23613
  reportName,
23590
23614
  options,
23591
23615
  });
@@ -23602,7 +23626,7 @@ class ReportWriterAdapter {
23602
23626
  * @param Ctor - Constructor for report storage adapter
23603
23627
  */
23604
23628
  useReportAdapter(Ctor) {
23605
- LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
23629
+ LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
23606
23630
  this.ReportFactory = Ctor;
23607
23631
  }
23608
23632
  /**
@@ -23611,7 +23635,7 @@ class ReportWriterAdapter {
23611
23635
  * so new storage instances are created with the updated base path.
23612
23636
  */
23613
23637
  clear() {
23614
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
23638
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
23615
23639
  this.getReportStorage.clear();
23616
23640
  }
23617
23641
  /**
@@ -23619,7 +23643,7 @@ class ReportWriterAdapter {
23619
23643
  * All future report writes will be no-ops.
23620
23644
  */
23621
23645
  useDummy() {
23622
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
23646
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
23623
23647
  this.useReportAdapter(ReportDummy);
23624
23648
  }
23625
23649
  /**
@@ -23627,7 +23651,7 @@ class ReportWriterAdapter {
23627
23651
  * All future report writes will use JSONL storage.
23628
23652
  */
23629
23653
  useJsonl() {
23630
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
23654
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
23631
23655
  this.useReportAdapter(ReportBase);
23632
23656
  }
23633
23657
  }
@@ -23682,7 +23706,7 @@ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, ti
23682
23706
  * @param value - Value to check
23683
23707
  * @returns true if value is unsafe, false otherwise
23684
23708
  */
23685
- function isUnsafe$3(value) {
23709
+ function isUnsafe$4(value) {
23686
23710
  if (typeof value !== "number") {
23687
23711
  return true;
23688
23712
  }
@@ -23694,6 +23718,25 @@ function isUnsafe$3(value) {
23694
23718
  }
23695
23719
  return false;
23696
23720
  }
23721
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
23722
+ const MIN_SIGNALS_FOR_ANNUALIZATION$2 = 10;
23723
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
23724
+ * sample size is too small to estimate variance meaningfully. */
23725
+ const MIN_SIGNALS_FOR_RATIOS$2 = 10;
23726
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
23727
+ const MIN_CALENDAR_SPAN_DAYS$2 = 14;
23728
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
23729
+ const MAX_TRADES_PER_YEAR$2 = 365;
23730
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
23731
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
23732
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
23733
+ const MAX_EXPECTED_YEARLY_RETURNS$2 = 100;
23734
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
23735
+ const MAX_CALMAR_RATIO$2 = 1000;
23736
+ /** Minimum stdDev required for Sharpe/Sortino computation. Identical-returns series produce
23737
+ * float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously inflates
23738
+ * sharpe to astronomical values. Treat any stdDev below this threshold as zero. */
23739
+ const STDDEV_EPSILON$2 = 1e-9;
23697
23740
  /**
23698
23741
  * Storage class for accumulating closed signals per strategy.
23699
23742
  * Maintains a list of all closed signals and provides methods to generate reports.
@@ -23747,65 +23790,190 @@ let ReportStorage$a = class ReportStorage {
23747
23790
  recoveryFactor: null,
23748
23791
  };
23749
23792
  }
23750
- const totalSignals = this._signalList.length;
23751
- const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
23752
- const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
23753
- // Calculate basic statistics
23754
- const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
23755
- const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
23756
- const winRate = (winCount / totalSignals) * 100;
23757
- // Calculate Sharpe Ratio (risk-free rate = 0)
23758
- const returns = this._signalList.map((s) => s.pnl.pnlPercentage);
23759
- const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalSignals;
23760
- const stdDev = Math.sqrt(variance);
23761
- const sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
23762
- const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
23763
- // Calculate Certainty Ratio
23764
- const wins = this._signalList.filter((s) => s.pnl.pnlPercentage > 0);
23765
- const losses = this._signalList.filter((s) => s.pnl.pnlPercentage < 0);
23793
+ // Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
23794
+ // of truth for EVERY metric in this method (counts, sums, span, equity curve,
23795
+ // ratios, annualization). If we used different subsets for different metrics, the
23796
+ // numerator of one ratio could be drawn from a different population than the
23797
+ // denominator of another and the report would silently lie. On clean data
23798
+ // validSignals === this._signalList; the filter only matters for corrupted runtime
23799
+ // data.
23800
+ const validSignals = this._signalList.filter((s) => typeof s.signal.pendingAt === "number" && s.signal.pendingAt > 0 &&
23801
+ typeof s.closeTimestamp === "number" && s.closeTimestamp > 0);
23802
+ const totalSignals = validSignals.length;
23803
+ const winCount = validSignals.filter((s) => s.pnl.pnlPercentage > 0).length;
23804
+ const lossCount = validSignals.filter((s) => s.pnl.pnlPercentage < 0).length;
23805
+ // Basic statistics guard against an empty validSignals (e.g. every signal had
23806
+ // corrupted timestamps) so we don't divide by zero.
23807
+ const avgPnl = totalSignals > 0
23808
+ ? validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals
23809
+ : 0;
23810
+ const totalPnl = validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
23811
+ // Win rate excludes break-even trades from both numerator and denominator.
23812
+ const decisiveTrades = winCount + lossCount;
23813
+ const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
23814
+ // Calendar span over the same validSignals set used for ratios.
23815
+ let firstPendingAt = Infinity;
23816
+ let lastCloseAt = -Infinity;
23817
+ for (const s of validSignals) {
23818
+ if (s.signal.pendingAt < firstPendingAt)
23819
+ firstPendingAt = s.signal.pendingAt;
23820
+ if (s.closeTimestamp > lastCloseAt)
23821
+ lastCloseAt = s.closeTimestamp;
23822
+ }
23823
+ const calendarSpanDays = isFinite(firstPendingAt) && isFinite(lastCloseAt)
23824
+ ? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
23825
+ : 0;
23826
+ // tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
23827
+ // silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
23828
+ // raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
23829
+ // for reliable annualization and surface every annualized metric as null.
23830
+ const rawTradesPerYear = totalSignals >= MIN_SIGNALS_FOR_ANNUALIZATION$2 &&
23831
+ calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$2
23832
+ ? (totalSignals / calendarSpanDays) * 365
23833
+ : 0;
23834
+ const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$2;
23835
+ const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
23836
+ // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
23837
+ // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
23838
+ // are too noisy to publish (high chance of spurious ±Sharpe).
23839
+ const returns = validSignals.map((s) => s.pnl.pnlPercentage);
23840
+ const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
23841
+ const stdDev = canComputeRatios
23842
+ ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
23843
+ : 0;
23844
+ // Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
23845
+ // float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
23846
+ // inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
23847
+ const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
23848
+ ? avgPnl / stdDev
23849
+ : null;
23850
+ // Annualize only when gate passes; otherwise null.
23851
+ const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
23852
+ ? sharpeRatio * Math.sqrt(tradesPerYear)
23853
+ : null;
23854
+ // Equity-curve max drawdown via compounded equity (multiplicative, not additive).
23855
+ // Returns are per-trade on cost basis — compounding assumes equal capital allocation
23856
+ // per trade ("as-if 100% allocation"). Walks validSignals in chronological order
23857
+ // (storage is newest-first, so iterate in reverse). Using validSignals (same set as
23858
+ // tradesPerYear) keeps equityFinal consistent with the annualization exponent.
23859
+ // If equity goes ≤ 0 (e.g. leveraged short with r < -100%) — account blown,
23860
+ // fix DD at 100% and stop walking the curve.
23861
+ let equity = 1;
23862
+ let peak = 1;
23863
+ let equityMaxDrawdown = 0;
23864
+ let blown = false;
23865
+ for (let i = validSignals.length - 1; i >= 0; i--) {
23866
+ equity *= 1 + validSignals[i].pnl.pnlPercentage / 100;
23867
+ if (equity <= 0) {
23868
+ equityMaxDrawdown = 100;
23869
+ blown = true;
23870
+ break;
23871
+ }
23872
+ if (equity > peak)
23873
+ peak = equity;
23874
+ const dd = (peak - equity) / peak * 100;
23875
+ if (dd > equityMaxDrawdown)
23876
+ equityMaxDrawdown = dd;
23877
+ }
23878
+ const equityFinal = blown ? 0 : equity;
23879
+ // Compounded yearly return via geometric mean of equity curve.
23880
+ // equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag that
23881
+ // arithmetic-mean compounding ((1+avgPnl)^N) misses. If account is blown, full loss.
23882
+ // If the raw value would exceed MAX_EXPECTED_YEARLY_RETURNS, return null rather than
23883
+ // showing the cap as a real figure — capped numbers mislead users into trusting them.
23884
+ const expectedYearlyReturns = canAnnualize
23885
+ ? blown
23886
+ ? -100
23887
+ : (() => {
23888
+ // Geometric annualization uses validSignals.length (same set that defined
23889
+ // tradesPerYear); using totalSignals here would mismatch numerator/denominator.
23890
+ const raw = (Math.pow(equityFinal, tradesPerYear / validSignals.length) - 1) * 100;
23891
+ return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$2 ? null : raw;
23892
+ })()
23893
+ : null;
23894
+ // Certainty Ratio — over validSignals so wins/losses come from the same set as
23895
+ // winCount/lossCount/avgPnl above.
23896
+ const wins = validSignals.filter((s) => s.pnl.pnlPercentage > 0);
23897
+ const losses = validSignals.filter((s) => s.pnl.pnlPercentage < 0);
23766
23898
  const avgWin = wins.length > 0
23767
23899
  ? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
23768
23900
  : 0;
23769
23901
  const avgLoss = losses.length > 0
23770
23902
  ? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
23771
23903
  : 0;
23772
- const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
23773
- // Calculate Expected Yearly Returns
23774
- const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / totalSignals;
23775
- const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
23776
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
23777
- const expectedYearlyReturns = avgPnl * tradesPerYear;
23778
- // Calculate average peak and fall PNL across all signals
23779
- const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / totalSignals;
23780
- const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / totalSignals;
23781
- // Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
23782
- const fallReturns = this._signalList.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
23783
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
23784
- const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalSignals;
23785
- const fallDeviation = Math.sqrt(fallVariance);
23786
- const sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
23787
- // Max absolute drawdown across all signals — used as denominator for Calmar and Recovery
23788
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
23789
- const calmarRatio = maxAbsFall > 0 ? expectedYearlyReturns / maxAbsFall : 0;
23790
- const recoveryFactor = maxAbsFall > 0 ? totalPnl / maxAbsFall : 0;
23904
+ // Null below MIN_SIGNALS_FOR_RATIOS on a handful of trades the win/loss
23905
+ // means are too noisy to publish a ratio (same sample-size gate as Sharpe/
23906
+ // Sortino, so the report doesn't surface certainty while withholding the rest).
23907
+ // Also null when no losing trades OR when |avgLoss| is below STDDEV_EPSILON
23908
+ // (float-artifact losses (-1e-15) would otherwise produce a spurious
23909
+ // astronomical certaintyRatio ≈1e14).
23910
+ const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
23911
+ ? avgWin / Math.abs(avgLoss)
23912
+ : null;
23913
+ // Average peak/fall PNL over validSignals; only signals that actually have the
23914
+ // value contribute (no zero dilution from missing peakProfit/maxDrawdown).
23915
+ const peakValues = validSignals
23916
+ .map((s) => s.signal.peakProfit?.pnlPercentage)
23917
+ .filter((v) => typeof v === "number");
23918
+ const fallValues = validSignals
23919
+ .map((s) => s.signal.maxDrawdown?.pnlPercentage)
23920
+ .filter((v) => typeof v === "number");
23921
+ const avgPeakPnl = peakValues.length > 0
23922
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
23923
+ : null;
23924
+ const avgFallPnl = fallValues.length > 0
23925
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
23926
+ : null;
23927
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
23928
+ // downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
23929
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
23930
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
23931
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
23932
+ // strategies.
23933
+ const negativeReturns = returns.filter((r) => r < 0);
23934
+ const sortinoRatio = (() => {
23935
+ if (!canComputeRatios)
23936
+ return null;
23937
+ if (negativeReturns.length === 0)
23938
+ return null;
23939
+ const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
23940
+ const downsideDeviation = Math.sqrt(downsideVariance);
23941
+ // Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
23942
+ return downsideDeviation > STDDEV_EPSILON$2 ? avgPnl / downsideDeviation : null;
23943
+ })();
23944
+ // Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
23945
+ const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
23946
+ ? Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, expectedYearlyReturns / equityMaxDrawdown))
23947
+ : null;
23948
+ // Recovery Factor: numerator must be the compounded total return (equityFinal − 1) × 100,
23949
+ // not the arithmetic totalPnl — denominator (equityMaxDrawdown) is from the compounded
23950
+ // curve, so mixing units would inflate Recovery on long winning streaks.
23951
+ // Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
23952
+ // so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
23953
+ // Null when account is blown — ratio is meaningless after total loss.
23954
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
23955
+ // and explode the same way when DD is near zero.
23956
+ const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
23957
+ ? null
23958
+ : Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
23791
23959
  return {
23792
23960
  signalList: this._signalList,
23793
23961
  totalSignals,
23794
23962
  winCount,
23795
23963
  lossCount,
23796
- winRate: isUnsafe$3(winRate) ? null : winRate,
23797
- avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
23798
- totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
23799
- stdDev: isUnsafe$3(stdDev) ? null : stdDev,
23800
- sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
23801
- annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
23802
- certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
23803
- expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
23804
- avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
23805
- avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
23806
- sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
23807
- calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
23808
- recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
23964
+ winRate: isUnsafe$4(winRate) ? null : winRate,
23965
+ avgPnl: isUnsafe$4(avgPnl) ? null : avgPnl,
23966
+ totalPnl: isUnsafe$4(totalPnl) ? null : totalPnl,
23967
+ stdDev: isUnsafe$4(stdDev) ? null : stdDev,
23968
+ sharpeRatio: isUnsafe$4(sharpeRatio) ? null : sharpeRatio,
23969
+ annualizedSharpeRatio: isUnsafe$4(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
23970
+ certaintyRatio: isUnsafe$4(certaintyRatio) ? null : certaintyRatio,
23971
+ expectedYearlyReturns: isUnsafe$4(expectedYearlyReturns) ? null : expectedYearlyReturns,
23972
+ avgPeakPnl: isUnsafe$4(avgPeakPnl) ? null : avgPeakPnl,
23973
+ avgFallPnl: isUnsafe$4(avgFallPnl) ? null : avgFallPnl,
23974
+ sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
23975
+ calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
23976
+ recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
23809
23977
  };
23810
23978
  }
23811
23979
  /**
@@ -23847,24 +24015,26 @@ let ReportStorage$a = class ReportStorage {
23847
24015
  `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
23848
24016
  `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
23849
24017
  `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
23850
- `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better, theoretical)`}`,
24018
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
23851
24019
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
23852
- `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better, theoretical)`}`,
24020
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
23853
24021
  `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
23854
24022
  `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
23855
24023
  `**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
23856
- `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better, theoretical)`}`,
24024
+ `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
23857
24025
  `**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
23858
24026
  "",
23859
24027
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
23860
24028
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
23861
- `*Annualized Sharpe Ratio: theoretical maximum assuming continuous trading. Real-world value is lower due to idle periods.*`,
23862
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24029
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$2} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$2} days. Assumes returns are iid — autocorrelated strategies are overstated.*`,
24030
+ `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
23863
24031
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
23864
- `*Expected Yearly Returns: theoretical maximum assuming all capital is deployed continuously with no idle time.*`,
23865
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
23866
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
23867
- `*All metrics require 100+ signals to be statistically reliable. Time period matters only for Annualized Sharpe Ratio and Expected Yearly Returns — they assume current market conditions hold year-round, which may not reflect reality.*`,
24032
+ `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$2}% — values above the cap return N/A.*`,
24033
+ `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at ±${MAX_CALMAR_RATIO$2}.*`,
24034
+ `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
24035
+ `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24036
+ `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
24037
+ `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
23868
24038
  ].join("\n");
23869
24039
  }
23870
24040
  /**
@@ -24176,7 +24346,7 @@ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, ti
24176
24346
  * @param value - Value to check
24177
24347
  * @returns true if value is unsafe, false otherwise
24178
24348
  */
24179
- function isUnsafe$2(value) {
24349
+ function isUnsafe$3(value) {
24180
24350
  if (typeof value !== "number") {
24181
24351
  return true;
24182
24352
  }
@@ -24188,6 +24358,25 @@ function isUnsafe$2(value) {
24188
24358
  }
24189
24359
  return false;
24190
24360
  }
24361
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
24362
+ const MIN_SIGNALS_FOR_ANNUALIZATION$1 = 10;
24363
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
24364
+ * sample size is too small to estimate variance meaningfully. */
24365
+ const MIN_SIGNALS_FOR_RATIOS$1 = 10;
24366
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
24367
+ const MIN_CALENDAR_SPAN_DAYS$1 = 14;
24368
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
24369
+ const MAX_TRADES_PER_YEAR$1 = 365;
24370
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
24371
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
24372
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
24373
+ const MAX_EXPECTED_YEARLY_RETURNS$1 = 100;
24374
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
24375
+ const MAX_CALMAR_RATIO$1 = 1000;
24376
+ /** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
24377
+ * float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
24378
+ * astronomical magnitudes (avgPnl / epsilon). */
24379
+ const STDDEV_EPSILON$1 = 1e-9;
24191
24380
  /**
24192
24381
  * Storage class for accumulating all tick events per strategy.
24193
24382
  * Maintains a chronological list of all events (idle, opened, active, closed).
@@ -24471,84 +24660,190 @@ let ReportStorage$9 = class ReportStorage {
24471
24660
  };
24472
24661
  }
24473
24662
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
24474
- const totalClosed = closedEvents.length;
24475
- const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
24476
- const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
24477
- // Calculate basic statistics
24478
- const avgPnl = totalClosed > 0
24479
- ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
24663
+ // Valid closed set — single source of truth. Events must have numeric pnl AND valid
24664
+ // timestamps. Win/loss counts, returns, calendar span, equity curve — all derived
24665
+ // from this set so they cannot disagree.
24666
+ const validClosed = closedEvents.filter((e) => typeof e.pnl === "number" &&
24667
+ typeof e.timestamp === "number" &&
24668
+ e.timestamp > 0 &&
24669
+ typeof (e.pendingAt ?? e.timestamp) === "number");
24670
+ const totalClosed = validClosed.length;
24671
+ const winCount = validClosed.filter((e) => e.pnl > 0).length;
24672
+ const lossCount = validClosed.filter((e) => e.pnl < 0).length;
24673
+ const returns = validClosed.map((e) => e.pnl);
24674
+ const avgPnl = returns.length > 0
24675
+ ? returns.reduce((sum, r) => sum + r, 0) / returns.length
24480
24676
  : 0;
24481
- const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
24482
- const winRate = (winCount / totalClosed) * 100;
24483
- // Calculate Sharpe Ratio (risk-free rate = 0)
24484
- let sharpeRatio = 0;
24485
- let stdDev = 0;
24486
- if (totalClosed > 0) {
24487
- const returns = closedEvents.map((e) => e.pnl || 0);
24488
- const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
24489
- stdDev = Math.sqrt(variance);
24490
- sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
24491
- }
24492
- const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
24493
- // Calculate Certainty Ratio
24494
- let certaintyRatio = 0;
24495
- if (totalClosed > 0) {
24496
- const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
24497
- const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
24677
+ const totalPnl = returns.reduce((sum, r) => sum + r, 0);
24678
+ // Win rate excludes break-even trades from both numerator and denominator.
24679
+ const decisiveTrades = winCount + lossCount;
24680
+ const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
24681
+ // Trade frequency from calendar span — gated by minimum span and sample size to
24682
+ // suppress absurd annualization on short / sparse runs. Span built from validClosed
24683
+ // so denominator (calendarSpanDays) and numerator (returns.length) come from the
24684
+ // same event set.
24685
+ let firstPendingAt = Infinity;
24686
+ let lastCloseAt = -Infinity;
24687
+ for (const e of validClosed) {
24688
+ const startAt = e.pendingAt ?? e.timestamp;
24689
+ if (startAt < firstPendingAt)
24690
+ firstPendingAt = startAt;
24691
+ if (e.timestamp > lastCloseAt)
24692
+ lastCloseAt = e.timestamp;
24693
+ }
24694
+ const calendarSpanDays = validClosed.length > 0
24695
+ ? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
24696
+ : 0;
24697
+ // tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
24698
+ // silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
24699
+ // raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
24700
+ // for reliable annualization and surface every annualized metric as null.
24701
+ const rawTradesPerYear = returns.length >= MIN_SIGNALS_FOR_ANNUALIZATION$1 &&
24702
+ calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$1
24703
+ ? (returns.length / calendarSpanDays) * 365
24704
+ : 0;
24705
+ const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$1;
24706
+ const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
24707
+ // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
24708
+ // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
24709
+ // are too noisy to publish (high chance of spurious ±Sharpe).
24710
+ const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
24711
+ const stdDev = canComputeRatios
24712
+ ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
24713
+ : 0;
24714
+ // STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
24715
+ // returns producing spuriously astronomical sharpe.
24716
+ const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
24717
+ ? avgPnl / stdDev
24718
+ : null;
24719
+ // Annualize only when gate passes; otherwise null.
24720
+ const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
24721
+ ? sharpeRatio * Math.sqrt(tradesPerYear)
24722
+ : null;
24723
+ // Certainty Ratio: null (not zero) when there are no losing trades — a flawless
24724
+ // strategy has undefined Certainty Ratio, not "worst case zero". Computed on
24725
+ // validClosed for consistency with other ratios.
24726
+ // Gated below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as Sharpe/Sortino,
24727
+ // so the report doesn't surface certainty on a handful of trades while
24728
+ // withholding the rest.
24729
+ let certaintyRatio = null;
24730
+ if (canComputeRatios && totalClosed > 0) {
24731
+ const wins = validClosed.filter((e) => e.pnl > 0);
24732
+ const losses = validClosed.filter((e) => e.pnl < 0);
24498
24733
  const avgWin = wins.length > 0
24499
- ? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
24734
+ ? wins.reduce((sum, e) => sum + e.pnl, 0) / wins.length
24500
24735
  : 0;
24501
24736
  const avgLoss = losses.length > 0
24502
- ? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
24737
+ ? losses.reduce((sum, e) => sum + e.pnl, 0) / losses.length
24503
24738
  : 0;
24504
- certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
24505
- }
24506
- // Calculate Expected Yearly Returns
24507
- let expectedYearlyReturns = 0;
24508
- if (totalClosed > 0) {
24509
- const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
24510
- const avgDurationDays = avgDurationMin / (60 * 24);
24511
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
24512
- expectedYearlyReturns = avgPnl * tradesPerYear;
24513
- }
24514
- const avgPeakPnl = totalClosed > 0
24515
- ? closedEvents.reduce((sum, e) => sum + (e.peakPnl || 0), 0) / totalClosed
24516
- : 0;
24517
- const avgFallPnl = totalClosed > 0
24518
- ? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
24519
- : 0;
24520
- // Downside per signal: fallPnl captures the worst intra-trade dip (maxDrawdown.pnlPercentage)
24521
- const fallReturns = closedEvents.map((e) => e.fallPnl || 0);
24522
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
24523
- let sortinoRatio = 0;
24524
- if (totalClosed > 0) {
24525
- const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalClosed;
24526
- const fallDeviation = Math.sqrt(fallVariance);
24527
- sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
24528
- }
24529
- // Max absolute drawdown across all signals — denominator for Calmar and Recovery
24530
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
24531
- const calmarRatio = maxAbsFall > 0 ? expectedYearlyReturns / maxAbsFall : 0;
24532
- const recoveryFactor = maxAbsFall > 0 ? totalPnl / maxAbsFall : 0;
24739
+ // STDDEV_EPSILON guard on |avgLoss| protects against float-artifact
24740
+ // losses producing spurious astronomical certaintyRatio.
24741
+ certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
24742
+ ? avgWin / Math.abs(avgLoss)
24743
+ : null;
24744
+ }
24745
+ // Average only over signals that have the value — do not dilute the mean with zeros.
24746
+ // Use validClosed to keep all metric denominators consistent.
24747
+ const peakValues = validClosed
24748
+ .map((e) => e.peakPnl)
24749
+ .filter((v) => typeof v === "number");
24750
+ const fallValues = validClosed
24751
+ .map((e) => e.fallPnl)
24752
+ .filter((v) => typeof v === "number");
24753
+ const avgPeakPnl = peakValues.length > 0
24754
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
24755
+ : null;
24756
+ const avgFallPnl = fallValues.length > 0
24757
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
24758
+ : null;
24759
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
24760
+ // downsideDev = ( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
24761
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
24762
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
24763
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
24764
+ // strategies.
24765
+ const sortinoRatio = (() => {
24766
+ if (!canComputeRatios)
24767
+ return null;
24768
+ const negativeReturns = returns.filter((r) => r < 0);
24769
+ if (negativeReturns.length === 0)
24770
+ return null;
24771
+ const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
24772
+ const downsideDeviation = Math.sqrt(downsideVariance);
24773
+ // Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
24774
+ return downsideDeviation > STDDEV_EPSILON$1 ? avgPnl / downsideDeviation : null;
24775
+ })();
24776
+ // Equity-curve max drawdown via compounded equity (multiplicative). Returns are per-trade
24777
+ // on cost basis — compounding assumes equal capital allocation per trade ("as-if 100%").
24778
+ // If equity ≤ 0 (leveraged short with r < -100%) — account blown, fix DD at 100%.
24779
+ // Built from validClosed (newest-first), iterated reverse for chronological order.
24780
+ const chronologicalReturns = [];
24781
+ for (let i = validClosed.length - 1; i >= 0; i--) {
24782
+ chronologicalReturns.push(validClosed[i].pnl);
24783
+ }
24784
+ let equity = 1;
24785
+ let peak = 1;
24786
+ let equityMaxDrawdown = 0;
24787
+ let blown = false;
24788
+ for (const r of chronologicalReturns) {
24789
+ equity *= 1 + r / 100;
24790
+ if (equity <= 0) {
24791
+ equityMaxDrawdown = 100;
24792
+ blown = true;
24793
+ break;
24794
+ }
24795
+ if (equity > peak)
24796
+ peak = equity;
24797
+ const dd = (peak - equity) / peak * 100;
24798
+ if (dd > equityMaxDrawdown)
24799
+ equityMaxDrawdown = dd;
24800
+ }
24801
+ const equityFinal = blown ? 0 : equity;
24802
+ // Compounded yearly return via geometric mean of equity curve:
24803
+ // equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
24804
+ // If account is blown, full loss. If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS,
24805
+ // return null rather than showing the cap — capped numbers mislead users.
24806
+ const expectedYearlyReturns = canAnnualize
24807
+ ? blown
24808
+ ? -100
24809
+ : (() => {
24810
+ const raw = (Math.pow(equityFinal, tradesPerYear / returns.length) - 1) * 100;
24811
+ return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$1 ? null : raw;
24812
+ })()
24813
+ : null;
24814
+ // Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
24815
+ const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
24816
+ ? Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, expectedYearlyReturns / equityMaxDrawdown))
24817
+ : null;
24818
+ // Recovery Factor: numerator must be the compounded total return, not arithmetic totalPnl —
24819
+ // denominator is from the compounded equity curve, so mixing units inflates Recovery.
24820
+ // Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
24821
+ // so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
24822
+ // Null when account is blown.
24823
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
24824
+ // and explode the same way when DD is near zero.
24825
+ const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
24826
+ ? null
24827
+ : Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
24533
24828
  return {
24534
24829
  eventList: this._eventList,
24535
24830
  totalEvents: this._eventList.length,
24536
24831
  totalClosed,
24537
24832
  winCount,
24538
24833
  lossCount,
24539
- winRate: isUnsafe$2(winRate) ? null : winRate,
24540
- avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
24541
- totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
24542
- stdDev: isUnsafe$2(stdDev) ? null : stdDev,
24543
- sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
24544
- annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
24545
- certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
24546
- expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
24547
- avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
24548
- avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
24549
- sortinoRatio: isUnsafe$2(sortinoRatio) ? null : sortinoRatio,
24550
- calmarRatio: isUnsafe$2(calmarRatio) ? null : calmarRatio,
24551
- recoveryFactor: isUnsafe$2(recoveryFactor) ? null : recoveryFactor,
24834
+ winRate: isUnsafe$3(winRate) ? null : winRate,
24835
+ avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
24836
+ totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
24837
+ stdDev: isUnsafe$3(stdDev) ? null : stdDev,
24838
+ sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
24839
+ annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
24840
+ certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
24841
+ expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
24842
+ avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
24843
+ avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
24844
+ sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
24845
+ calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
24846
+ recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
24552
24847
  };
24553
24848
  }
24554
24849
  /**
@@ -24596,18 +24891,20 @@ let ReportStorage$9 = class ReportStorage {
24596
24891
  `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
24597
24892
  `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
24598
24893
  `**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
24599
- `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better, theoretical)`}`,
24894
+ `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
24600
24895
  `**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
24601
24896
  "",
24602
24897
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24603
24898
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24604
- `*Annualized Sharpe Ratio: theoretical maximum assuming continuous trading. Real-world value is lower due to idle periods.*`,
24605
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24899
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$1} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$1} days. Assumes returns are iid — autocorrelated strategies are overstated.*`,
24900
+ `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
24606
24901
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
24607
- `*Expected Yearly Returns: theoretical maximum assuming all capital is deployed continuously with no idle time.*`,
24608
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
24609
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
24610
- `*All metrics require 100+ signals to be statistically reliable. Time period matters only for Annualized Sharpe Ratio and Expected Yearly Returns — they assume current market conditions hold year-round, which may not reflect reality.*`,
24902
+ `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS$1}% — values above the cap return N/A.*`,
24903
+ `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at ±${MAX_CALMAR_RATIO$1}.*`,
24904
+ `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
24905
+ `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24906
+ `*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
24907
+ `*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
24611
24908
  ].join("\n");
24612
24909
  }
24613
24910
  /**
@@ -24986,7 +25283,9 @@ let ReportStorage$8 = class ReportStorage {
24986
25283
  */
24987
25284
  addOpenedEvent(data) {
24988
25285
  const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
24989
- const durationMin = Math.round(durationMs / 60000);
25286
+ // Keep fractional minutes rounding to whole minutes zeroed out sub-30s durations,
25287
+ // which dragged high-frequency averages towards zero.
25288
+ const durationMin = durationMs / 60000;
24990
25289
  const newEvent = {
24991
25290
  timestamp: data.signal.pendingAt,
24992
25291
  action: "opened",
@@ -25022,7 +25321,8 @@ let ReportStorage$8 = class ReportStorage {
25022
25321
  */
25023
25322
  addCancelledEvent(data) {
25024
25323
  const durationMs = data.closeTimestamp - data.signal.scheduledAt;
25025
- const durationMin = Math.round(durationMs / 60000);
25324
+ // Keep fractional minutes rounding to whole minutes zeroed out sub-30s durations.
25325
+ const durationMin = durationMs / 60000;
25026
25326
  const newEvent = {
25027
25327
  timestamp: data.closeTimestamp,
25028
25328
  action: "cancelled",
@@ -25078,19 +25378,33 @@ let ReportStorage$8 = class ReportStorage {
25078
25378
  const totalScheduled = scheduledEvents.length;
25079
25379
  const totalOpened = openedEvents.length;
25080
25380
  const totalCancelled = cancelledEvents.length;
25081
- // Calculate cancellation rate
25082
- const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
25083
- // Calculate activation rate
25084
- const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
25085
- // Calculate average wait time for cancelled signals
25086
- const avgWaitTime = totalCancelled > 0
25087
- ? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
25088
- totalCancelled
25381
+ // Rate denominators must include only scheduled events whose outcome (opened/cancelled)
25382
+ // is also in the buffer. Otherwise a sliding window of 250 entries can drop the
25383
+ // "scheduled" record before its outcome arrives, inflating rates above 100% or
25384
+ // causing one rate to fire without the other. Match by signalId.
25385
+ const scheduledIds = new Set(scheduledEvents.map((e) => e.signalId).filter((id) => typeof id === "string"));
25386
+ const openedFromScheduled = openedEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
25387
+ const cancelledFromScheduled = cancelledEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
25388
+ const resolvedScheduled = openedFromScheduled.length + cancelledFromScheduled.length;
25389
+ const cancellationRate = resolvedScheduled > 0
25390
+ ? (cancelledFromScheduled.length / resolvedScheduled) * 100
25391
+ : null;
25392
+ const activationRate = resolvedScheduled > 0
25393
+ ? (openedFromScheduled.length / resolvedScheduled) * 100
25089
25394
  : null;
25090
- // Calculate average activation time for opened signals
25091
- const avgActivationTime = totalOpened > 0
25092
- ? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
25093
- totalOpened
25395
+ // Average durations include only events with a numeric duration, do not dilute
25396
+ // the mean with zeros for missing values.
25397
+ const cancelledDurations = cancelledEvents
25398
+ .map((e) => e.duration)
25399
+ .filter((d) => typeof d === "number");
25400
+ const openedDurations = openedEvents
25401
+ .map((e) => e.duration)
25402
+ .filter((d) => typeof d === "number");
25403
+ const avgWaitTime = cancelledDurations.length > 0
25404
+ ? cancelledDurations.reduce((sum, d) => sum + d, 0) / cancelledDurations.length
25405
+ : null;
25406
+ const avgActivationTime = openedDurations.length > 0
25407
+ ? openedDurations.reduce((sum, d) => sum + d, 0) / openedDurations.length
25094
25408
  : null;
25095
25409
  return {
25096
25410
  eventList: this._eventList,
@@ -25137,13 +25451,15 @@ let ReportStorage$8 = class ReportStorage {
25137
25451
  table,
25138
25452
  "",
25139
25453
  `**Total events:** ${stats.totalEvents}`,
25140
- `**Scheduled signals:** ${stats.totalScheduled}`,
25454
+ `**Scheduled signals (raw):** ${stats.totalScheduled}`,
25141
25455
  `**Opened signals:** ${stats.totalOpened}`,
25142
25456
  `**Cancelled signals:** ${stats.totalCancelled}`,
25143
25457
  `**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
25144
25458
  `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
25145
25459
  `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
25146
- `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
25460
+ `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
25461
+ "",
25462
+ `*Activation / Cancellation rates are computed over scheduled signals whose outcome (opened or cancelled) is also in the buffer — matched by signalId. "Scheduled signals (raw)" above is the unmatched count and may include records whose outcome has not yet arrived or was evicted from the buffer.*`
25147
25463
  ].join("\n");
25148
25464
  }
25149
25465
  /**
@@ -25448,13 +25764,37 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
25448
25764
  return `${parts.join("_")}-${timestamp}.md`;
25449
25765
  };
25450
25766
  /**
25451
- * Calculates percentile value from sorted array.
25767
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
25768
+ */
25769
+ function isUnsafe$2(value) {
25770
+ if (typeof value !== "number") {
25771
+ return true;
25772
+ }
25773
+ if (isNaN(value)) {
25774
+ return true;
25775
+ }
25776
+ if (!isFinite(value)) {
25777
+ return true;
25778
+ }
25779
+ return false;
25780
+ }
25781
+ /**
25782
+ * Calculates percentile value from sorted array using linear interpolation
25783
+ * between adjacent ranks (equivalent to numpy.percentile with default linear method).
25784
+ * Falls back to nearest-rank for length 0/1.
25452
25785
  */
25453
25786
  function percentile(sortedArray, p) {
25454
25787
  if (sortedArray.length === 0)
25455
25788
  return 0;
25456
- const index = Math.ceil((sortedArray.length * p) / 100) - 1;
25457
- return sortedArray[Math.max(0, index)];
25789
+ if (sortedArray.length === 1)
25790
+ return sortedArray[0];
25791
+ const rank = (p / 100) * (sortedArray.length - 1);
25792
+ const lower = Math.floor(rank);
25793
+ const upper = Math.ceil(rank);
25794
+ if (lower === upper)
25795
+ return sortedArray[lower];
25796
+ const fraction = rank - lower;
25797
+ return sortedArray[lower] * (1 - fraction) + sortedArray[upper] * fraction;
25458
25798
  }
25459
25799
  /**
25460
25800
  * Storage class for accumulating performance metrics per strategy.
@@ -25510,10 +25850,12 @@ class PerformanceStorage {
25510
25850
  const durations = events.map((e) => e.duration).sort((a, b) => a - b);
25511
25851
  const totalDuration = durations.reduce((sum, d) => sum + d, 0);
25512
25852
  const avgDuration = totalDuration / durations.length;
25513
- // Calculate standard deviation
25514
- const variance = durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
25515
- durations.length;
25516
- const stdDev = Math.sqrt(variance);
25853
+ // Sample standard deviation (Bessel correction: divide by N-1, not N) — consistent
25854
+ // with Sharpe/Sortino calculations in Backtest/Live/Heat services.
25855
+ const stdDev = durations.length > 1
25856
+ ? Math.sqrt(durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
25857
+ (durations.length - 1))
25858
+ : 0;
25517
25859
  // Calculate wait times between events
25518
25860
  const waitTimes = [];
25519
25861
  for (let i = 0; i < events.length; i++) {
@@ -25586,9 +25928,13 @@ class PerformanceStorage {
25586
25928
  const rows = await Promise.all(sortedMetrics.map(async (metric, index) => Promise.all(visibleColumns.map((col) => col.format(metric, index)))));
25587
25929
  const tableData = [header, separator, ...rows];
25588
25930
  const summaryTable = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
25589
- // Calculate percentage of total time for each metric
25931
+ // Calculate percentage of total time for each metric. Guard against zero total
25932
+ // duration (all-instant operations) to avoid NaN% in the rendered report.
25590
25933
  const percentages = sortedMetrics.map((metric) => {
25591
- const pct = (metric.totalDuration / stats.totalDuration) * 100;
25934
+ const pctRaw = stats.totalDuration > 0
25935
+ ? (metric.totalDuration / stats.totalDuration) * 100
25936
+ : 0;
25937
+ const pct = isUnsafe$2(pctRaw) ? 0 : pctRaw;
25592
25938
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
25593
25939
  });
25594
25940
  return [
@@ -26357,6 +26703,25 @@ function isUnsafe(value) {
26357
26703
  }
26358
26704
  return false;
26359
26705
  }
26706
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
26707
+ const MIN_SIGNALS_FOR_ANNUALIZATION = 10;
26708
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
26709
+ * sample size is too small to estimate variance meaningfully. */
26710
+ const MIN_SIGNALS_FOR_RATIOS = 10;
26711
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
26712
+ const MIN_CALENDAR_SPAN_DAYS = 14;
26713
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
26714
+ const MAX_TRADES_PER_YEAR = 365;
26715
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
26716
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
26717
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
26718
+ const MAX_EXPECTED_YEARLY_RETURNS = 100;
26719
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
26720
+ const MAX_CALMAR_RATIO = 1000;
26721
+ /** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
26722
+ * float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
26723
+ * astronomical magnitudes (avgPnl / epsilon). */
26724
+ const STDDEV_EPSILON = 1e-9;
26360
26725
  /**
26361
26726
  * Storage class for accumulating closed signals per strategy and generating heatmap.
26362
26727
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -26398,7 +26763,7 @@ class HeatmapStorage {
26398
26763
  * - **totalPnl** — sum of `pnlPercentage` across all signals
26399
26764
  * - **avgPnl** — arithmetic mean of `pnlPercentage`
26400
26765
  * - **stdDev** — population standard deviation of `pnlPercentage`
26401
- * - **sharpeRatio** — `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
26766
+ * - **sharpeRatio** — per-trade Sharpe: `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
26402
26767
  * - **maxDrawdown** — largest cumulative loss streak (absolute value of peak negative equity)
26403
26768
  * - **profitFactor** — `sumWins / |sumLosses|`; requires at least one win and one loss
26404
26769
  * - **avgWin / avgLoss** — mean of positive / negative trades respectively
@@ -26414,10 +26779,12 @@ class HeatmapStorage {
26414
26779
  const totalTrades = signals.length;
26415
26780
  const winCount = signals.filter((s) => s.pnl.pnlPercentage > 0).length;
26416
26781
  const lossCount = signals.filter((s) => s.pnl.pnlPercentage < 0).length;
26417
- // Calculate win rate
26782
+ // Win rate excludes break-even trades from both numerator and denominator —
26783
+ // they are neither wins nor losses.
26418
26784
  let winRate = null;
26419
- if (totalTrades > 0) {
26420
- winRate = (winCount / totalTrades) * 100;
26785
+ const decisiveTrades = winCount + lossCount;
26786
+ if (decisiveTrades > 0) {
26787
+ winRate = (winCount / decisiveTrades) * 100;
26421
26788
  }
26422
26789
  // Calculate total PNL
26423
26790
  let totalPnl = null;
@@ -26429,36 +26796,47 @@ class HeatmapStorage {
26429
26796
  if (signals.length > 0) {
26430
26797
  avgPnl = totalPnl / signals.length;
26431
26798
  }
26432
- // Calculate standard deviation
26799
+ // Sample standard deviation (Bessel correction: divide by N-1, not N).
26800
+ // Per-symbol ratios are gated by MIN_SIGNALS_FOR_RATIOS — variance estimates from
26801
+ // tiny samples are too noisy to publish.
26802
+ const canComputeRatios = signals.length >= MIN_SIGNALS_FOR_RATIOS;
26433
26803
  let stdDev = null;
26434
- if (signals.length > 1 && avgPnl !== null) {
26435
- const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / signals.length;
26804
+ if (canComputeRatios && avgPnl !== null) {
26805
+ const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / (signals.length - 1);
26436
26806
  stdDev = Math.sqrt(variance);
26437
26807
  }
26438
- // Calculate Sharpe Ratio
26808
+ // Per-trade Sharpe Ratio
26439
26809
  let sharpeRatio = null;
26440
- if (avgPnl !== null && stdDev !== null && stdDev !== 0) {
26810
+ // STDDEV_EPSILON guard protects against float-artifact stdDev producing
26811
+ // spuriously astronomical sharpe on identical-returns symbols.
26812
+ if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
26441
26813
  sharpeRatio = avgPnl / stdDev;
26442
26814
  }
26443
- // Calculate Maximum Drawdown
26815
+ // Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
26816
+ // Signals are stored newest-first (unshift in addSignal), so iterate in reverse.
26817
+ // If equity ≤ 0 — account blown, fix DD at 100%. equityFinal feeds expectedYearlyReturns.
26444
26818
  let maxDrawdown = null;
26819
+ let equityFinal = 1;
26820
+ let blown = false;
26445
26821
  if (signals.length > 0) {
26446
- let peak = 0;
26447
- let currentDrawdown = 0;
26822
+ let equity = 1;
26823
+ let peak = 1;
26448
26824
  let maxDD = 0;
26449
- for (const signal of signals) {
26450
- peak += signal.pnl.pnlPercentage;
26451
- if (peak > 0) {
26452
- currentDrawdown = 0;
26453
- }
26454
- else {
26455
- currentDrawdown = Math.abs(peak);
26456
- if (currentDrawdown > maxDD) {
26457
- maxDD = currentDrawdown;
26458
- }
26825
+ for (let i = signals.length - 1; i >= 0; i--) {
26826
+ equity *= 1 + signals[i].pnl.pnlPercentage / 100;
26827
+ if (equity <= 0) {
26828
+ maxDD = 100;
26829
+ blown = true;
26830
+ break;
26459
26831
  }
26832
+ if (equity > peak)
26833
+ peak = equity;
26834
+ const dd = (peak - equity) / peak * 100;
26835
+ if (dd > maxDD)
26836
+ maxDD = dd;
26460
26837
  }
26461
26838
  maxDrawdown = maxDD;
26839
+ equityFinal = blown ? 0 : equity;
26462
26840
  }
26463
26841
  // Calculate Profit Factor
26464
26842
  let profitFactor = null;
@@ -26469,7 +26847,9 @@ class HeatmapStorage {
26469
26847
  const sumLosses = Math.abs(signals
26470
26848
  .filter((s) => s.pnl.pnlPercentage < 0)
26471
26849
  .reduce((acc, s) => acc + s.pnl.pnlPercentage, 0));
26472
- if (sumLosses > 0) {
26850
+ // STDDEV_EPSILON guard — float-artifact losses (≈1e-15) would otherwise
26851
+ // produce spurious astronomical profitFactor (≈1e14).
26852
+ if (sumLosses > STDDEV_EPSILON) {
26473
26853
  profitFactor = sumWins / sumLosses;
26474
26854
  }
26475
26855
  }
@@ -26509,45 +26889,110 @@ class HeatmapStorage {
26509
26889
  }
26510
26890
  }
26511
26891
  }
26512
- // Calculate Expectancy
26892
+ // Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
26513
26893
  let expectancy = null;
26514
- if (winRate !== null && avgWin !== null && avgLoss !== null) {
26515
- const lossRate = 100 - winRate;
26516
- expectancy = (winRate / 100) * avgWin + (lossRate / 100) * avgLoss;
26894
+ if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
26895
+ const winProb = winCount / totalTrades;
26896
+ const lossProb = lossCount / totalTrades;
26897
+ expectancy = winProb * avgWin + lossProb * avgLoss;
26898
+ }
26899
+ else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
26900
+ // No losing trades — expectancy is just average win frequency × avgWin
26901
+ expectancy = (winCount / totalTrades) * avgWin;
26517
26902
  }
26518
- // Calculate average peak and fall PNL
26903
+ else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
26904
+ expectancy = (lossCount / totalTrades) * avgLoss;
26905
+ }
26906
+ // Average only over signals that have the value — do not dilute the mean with zeros.
26519
26907
  let avgPeakPnl = null;
26520
26908
  let avgFallPnl = null;
26521
26909
  if (signals.length > 0) {
26522
- avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / signals.length;
26523
- avgFallPnl = signals.reduce((acc, s) => acc + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / signals.length;
26910
+ const peakValues = signals
26911
+ .map((s) => s.signal.peakProfit?.pnlPercentage)
26912
+ .filter((v) => typeof v === "number");
26913
+ const fallValues = signals
26914
+ .map((s) => s.signal.maxDrawdown?.pnlPercentage)
26915
+ .filter((v) => typeof v === "number");
26916
+ avgPeakPnl = peakValues.length > 0
26917
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
26918
+ : null;
26919
+ avgFallPnl = fallValues.length > 0
26920
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
26921
+ : null;
26524
26922
  }
26525
- // Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
26526
- const fallReturns = signals.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
26527
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
26923
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
26924
+ // downsideDev = ( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
26925
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
26926
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
26927
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
26928
+ // strategies.
26528
26929
  let sortinoRatio = null;
26529
- if (signals.length > 0 && avgPnl !== null) {
26530
- const fallVariance = fallReturns.reduce((acc, r) => acc + Math.pow(r, 2), 0) / signals.length;
26531
- const fallDeviation = Math.sqrt(fallVariance);
26532
- if (fallDeviation > 0) {
26533
- sortinoRatio = avgPnl / fallDeviation;
26534
- }
26535
- }
26536
- // Max absolute drawdown across all signals denominator for Calmar and Recovery
26537
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
26538
- // Expected yearly returns — needed for Calmar
26539
- let expectedYearlyReturns = 0;
26540
- if (signals.length > 0 && avgPnl !== null) {
26541
- const avgDurationMs = signals.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / signals.length;
26542
- const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
26543
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
26544
- expectedYearlyReturns = avgPnl * tradesPerYear;
26930
+ if (canComputeRatios && avgPnl !== null) {
26931
+ const negativeReturns = signals
26932
+ .map((s) => s.pnl.pnlPercentage)
26933
+ .filter((r) => r < 0);
26934
+ if (negativeReturns.length > 0) {
26935
+ const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / signals.length;
26936
+ const downsideDeviation = Math.sqrt(downsideVariance);
26937
+ // Same epsilon guard as Sharpeprotects against float-artifact downsideDev.
26938
+ if (downsideDeviation > STDDEV_EPSILON) {
26939
+ sortinoRatio = avgPnl / downsideDeviation;
26940
+ }
26941
+ }
26942
+ }
26943
+ // Expected yearly returns via geometric mean of equity curve.
26944
+ // equityFinal^(tradesPerYear / N) - 1 accounts for volatility drag.
26945
+ // Gated by sample size and calendar span; if account blown → full loss.
26946
+ let expectedYearlyReturns = null;
26947
+ let tradesPerYear = null;
26948
+ if (signals.length >= MIN_SIGNALS_FOR_ANNUALIZATION) {
26949
+ let firstPendingAt = Infinity;
26950
+ let lastCloseAt = -Infinity;
26951
+ for (const s of signals) {
26952
+ if (s.signal.pendingAt < firstPendingAt)
26953
+ firstPendingAt = s.signal.pendingAt;
26954
+ if (s.closeTimestamp > lastCloseAt)
26955
+ lastCloseAt = s.closeTimestamp;
26956
+ }
26957
+ const calendarSpanDays = (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24);
26958
+ if (calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS) {
26959
+ // tradesPerYear uses RAW observed frequency — no clipping. If the raw value
26960
+ // exceeds MAX_TRADES_PER_YEAR the sample is too clustered for reliable
26961
+ // annualization, and we leave the annualized metric null instead of silently
26962
+ // understating it with a clipped frequency.
26963
+ const rawTradesPerYear = (signals.length / calendarSpanDays) * 365;
26964
+ if (rawTradesPerYear <= MAX_TRADES_PER_YEAR) {
26965
+ tradesPerYear = rawTradesPerYear;
26966
+ if (blown) {
26967
+ expectedYearlyReturns = -100;
26968
+ }
26969
+ else {
26970
+ // If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS, leave null rather than
26971
+ // show the cap — capped numbers mislead users into trusting them.
26972
+ const raw = (Math.pow(equityFinal, tradesPerYear / signals.length) - 1) * 100;
26973
+ expectedYearlyReturns = Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS ? null : raw;
26974
+ }
26975
+ }
26976
+ }
26545
26977
  }
26978
+ // Calmar = annualized return / equity-curve max drawdown, capped at ±MAX_CALMAR_RATIO.
26979
+ // Recovery Factor uses the compounded total return (equityFinal-1)*100, not arithmetic
26980
+ // totalPnl — denominator is compounded so numerator must match. Null when account blown.
26546
26981
  let calmarRatio = null;
26547
26982
  let recoveryFactor = null;
26548
- if (maxAbsFall > 0 && totalPnl !== null) {
26549
- calmarRatio = expectedYearlyReturns / maxAbsFall;
26550
- recoveryFactor = totalPnl / maxAbsFall;
26983
+ if (maxDrawdown !== null && maxDrawdown > 0) {
26984
+ if (expectedYearlyReturns !== null) {
26985
+ const raw = expectedYearlyReturns / maxDrawdown;
26986
+ calmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, raw));
26987
+ }
26988
+ if (!blown && canComputeRatios) {
26989
+ // Gated below MIN_SIGNALS_FOR_RATIOS like Sharpe — a Recovery Factor on
26990
+ // a handful of trades is statistically meaningless, so don't surface it
26991
+ // per-symbol while Sharpe is N/A.
26992
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both compounded-profit/DD ratios.
26993
+ const rawRec = ((equityFinal - 1) * 100) / maxDrawdown;
26994
+ recoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
26995
+ }
26551
26996
  }
26552
26997
  // Apply safe math checks
26553
26998
  if (isUnsafe(winRate))
@@ -26612,12 +27057,18 @@ class HeatmapStorage {
26612
27057
  * 2. Sorts symbols by `sharpeRatio` descending — best performers first,
26613
27058
  * symbols with `null` sharpeRatio placed at the end.
26614
27059
  * 3. Computes portfolio-wide aggregates:
26615
- * - `portfolioTotalPnl` — sum of all per-symbol `totalPnl` values (treats `null` as 0)
26616
- * - `portfolioTotalTrades` sum of all per-symbol `totalTrades`
26617
- * - `portfolioSharpeRatio` trade-count-weighted average of per-symbol sharpe ratios
26618
- *
26619
- * @returns Promise resolving to `HeatmapStatisticsModel` with per-symbol rows and
26620
- * portfolio-wide `portfolioTotalPnl`, `portfolioSharpeRatio`, `portfolioTotalTrades`
27060
+ * - `portfolioTotalPnl` — sum of per-symbol `totalPnl` values, skipping `null` entries
27061
+ * (so a symbol with no data does not silently contribute 0). If every symbol's
27062
+ * `totalPnl` is null, the portfolio value is null.
27063
+ * - `portfolioTotalTrades` — sum of per-symbol `totalTrades`
27064
+ * - `portfolioSharpeRatio` POOLED Sharpe over all trades across symbols (sample
27065
+ * stddev, N-1). NOT a Markowitz portfolio Sharpe — ignores cross-symbol
27066
+ * correlations and capital allocation. Rendered as "Pooled Sharpe" in the report.
27067
+ * Gated by `MIN_SIGNALS_FOR_RATIOS` on the pooled count.
27068
+ * - `portfolioAvgPeakPnl` / `portfolioAvgFallPnl` — trade-count-weighted means
27069
+ * over symbols that have non-null values.
27070
+ *
27071
+ * @returns Promise resolving to `HeatmapStatisticsModel`
26621
27072
  */
26622
27073
  async getData() {
26623
27074
  const symbols = [];
@@ -26636,31 +27087,53 @@ class HeatmapStorage {
26636
27087
  return -1;
26637
27088
  return b.sharpeRatio - a.sharpeRatio;
26638
27089
  });
26639
- // Calculate portfolio-wide metrics
27090
+ // Portfolio totals — sum only over symbols with non-null totalPnl. `s.totalPnl || 0`
27091
+ // would silently treat a missing value as zero and hide that some symbols had no data.
26640
27092
  const totalSymbols = symbols.length;
26641
27093
  let portfolioTotalPnl = null;
26642
27094
  let portfolioTotalTrades = 0;
26643
27095
  if (symbols.length > 0) {
26644
- portfolioTotalPnl = symbols.reduce((acc, s) => acc + (s.totalPnl || 0), 0);
27096
+ const validTotalPnls = symbols.filter((s) => s.totalPnl !== null);
27097
+ portfolioTotalPnl = validTotalPnls.length > 0
27098
+ ? validTotalPnls.reduce((acc, s) => acc + s.totalPnl, 0)
27099
+ : null;
26645
27100
  portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
26646
27101
  }
26647
- // Calculate portfolio Sharpe Ratio (weighted by number of trades)
27102
+ // Pooled Sharpe over all returns across symbols. NOTE: this is NOT a Markowitz
27103
+ // portfolio Sharpe — it ignores cross-symbol correlations and treats trades as a
27104
+ // single pooled sample. Gated by MIN_SIGNALS_FOR_RATIOS so a 2-trade pool cannot
27105
+ // produce a noisy ±Sharpe.
26648
27106
  let portfolioSharpeRatio = null;
26649
- const validSharpes = symbols.filter((s) => s.sharpeRatio !== null);
26650
- if (validSharpes.length > 0 && portfolioTotalTrades > 0) {
26651
- const weightedSum = validSharpes.reduce((acc, s) => acc + s.sharpeRatio * s.totalTrades, 0);
26652
- portfolioSharpeRatio = weightedSum / portfolioTotalTrades;
27107
+ const allReturns = [];
27108
+ for (const signals of this.symbolData.values()) {
27109
+ for (const s of signals) {
27110
+ allReturns.push(s.pnl.pnlPercentage);
27111
+ }
27112
+ }
27113
+ if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
27114
+ const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
27115
+ const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
27116
+ (allReturns.length - 1);
27117
+ const portfolioStdDev = Math.sqrt(portfolioVariance);
27118
+ // STDDEV_EPSILON guard — same protection as per-symbol Sharpe.
27119
+ if (portfolioStdDev > STDDEV_EPSILON) {
27120
+ portfolioSharpeRatio = portfolioAvg / portfolioStdDev;
27121
+ }
26653
27122
  }
26654
- // Calculate portfolio-wide weighted average peak/fall PNL
27123
+ // Portfolio-wide weighted average peak/fall PNL. Denominator must include only
27124
+ // symbols that contributed a value — otherwise trade-count-weighted mean is diluted
27125
+ // by symbols without the metric.
26655
27126
  let portfolioAvgPeakPnl = null;
26656
27127
  let portfolioAvgFallPnl = null;
26657
27128
  const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
26658
27129
  const validFall = symbols.filter((s) => s.avgFallPnl !== null);
26659
- if (validPeak.length > 0 && portfolioTotalTrades > 0) {
26660
- portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / portfolioTotalTrades;
27130
+ const peakTradesTotal = validPeak.reduce((acc, s) => acc + s.totalTrades, 0);
27131
+ const fallTradesTotal = validFall.reduce((acc, s) => acc + s.totalTrades, 0);
27132
+ if (validPeak.length > 0 && peakTradesTotal > 0) {
27133
+ portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / peakTradesTotal;
26661
27134
  }
26662
- if (validFall.length > 0 && portfolioTotalTrades > 0) {
26663
- portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / portfolioTotalTrades;
27135
+ if (validFall.length > 0 && fallTradesTotal > 0) {
27136
+ portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / fallTradesTotal;
26664
27137
  }
26665
27138
  // Apply safe math
26666
27139
  if (isUnsafe(portfolioTotalPnl))
@@ -26688,7 +27161,7 @@ class HeatmapStorage {
26688
27161
  * ```
26689
27162
  * # Portfolio Heatmap: {strategyName}
26690
27163
  *
26691
- * **Total Symbols:** N | **Portfolio PNL:** X% | **Portfolio Sharpe:** Y | **Total Trades:** Z
27164
+ * **Total Symbols:** N | **Portfolio PNL:** X% | **Pooled Sharpe:** Y | **Total Trades:** Z
26692
27165
  *
26693
27166
  * | col1 | col2 | ... |
26694
27167
  * | --- | --- | ... |
@@ -26727,18 +27200,21 @@ class HeatmapStorage {
26727
27200
  return [
26728
27201
  `# Portfolio Heatmap: ${strategyName}`,
26729
27202
  "",
26730
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
27203
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
26731
27204
  "",
26732
27205
  table,
26733
27206
  "",
26734
27207
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
27208
+ `*Pooled Sharpe: Sharpe computed over all trades across symbols treated as one sample. NOT a Markowitz portfolio Sharpe — ignores cross-symbol correlations and capital allocation. N/A unless ≥${MIN_SIGNALS_FOR_RATIOS} pooled trades.*`,
26735
27209
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
26736
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
27210
+ `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
26737
27211
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
26738
27212
  `*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
26739
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
26740
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
26741
- `*All metrics require 100+ signals per symbol to be statistically reliable. Time period matters only for Calmar Ratio it assumes current market conditions hold year-round, which may not reflect reality.*`,
27213
+ `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals per symbol and span ≥${MIN_CALENDAR_SPAN_DAYS} days. Capped at ±${MAX_CALMAR_RATIO}.*`,
27214
+ `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
27215
+ `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
27216
+ `*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
27217
+ `*Negative values for Sharpe / Sortino / Calmar / Recovery indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
26742
27218
  ].join("\n");
26743
27219
  }
26744
27220
  /**
@@ -26933,7 +27409,7 @@ class HeatMarkdownService {
26933
27409
  * console.log(markdown);
26934
27410
  * // # Portfolio Heatmap: my-strategy
26935
27411
  * //
26936
- * // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
27412
+ * // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Pooled Sharpe:** 1.85 | **Total Trades:** 120
26937
27413
  * //
26938
27414
  * // | Symbol | Total PNL | Sharpe | Max DD | Trades |
26939
27415
  * // | --- | --- | --- | --- | --- |
@@ -53928,7 +54404,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
53928
54404
  const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
53929
54405
  const REPORT_UTILS_METHOD_NAME_CLEAR = "ReportUtils.clear";
53930
54406
  /** Logger service injected as DI singleton */
53931
- const LOGGER_SERVICE$2 = new LoggerService();
54407
+ const LOGGER_SERVICE$3 = new LoggerService();
53932
54408
  /**
53933
54409
  * Default configuration that enables all report services.
53934
54410
  * Used when no specific configuration is provided to enable().
@@ -53985,7 +54461,7 @@ class ReportUtils {
53985
54461
  * @returns Cleanup function that unsubscribes from all enabled services
53986
54462
  */
53987
54463
  this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
53988
- LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
54464
+ LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
53989
54465
  backtest: bt,
53990
54466
  breakeven,
53991
54467
  heat,
@@ -54080,7 +54556,7 @@ class ReportUtils {
54080
54556
  * ```
54081
54557
  */
54082
54558
  this.disable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
54083
- LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
54559
+ LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
54084
54560
  backtest: bt,
54085
54561
  breakeven,
54086
54562
  heat,
@@ -54160,7 +54636,7 @@ class ReportAdapter extends ReportUtils {
54160
54636
  * @param Ctor - Constructor for report storage adapter
54161
54637
  */
54162
54638
  useReportAdapter(Ctor) {
54163
- LOGGER_SERVICE$2.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
54639
+ LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
54164
54640
  ReportWriter.useReportAdapter(Ctor);
54165
54641
  }
54166
54642
  /**
@@ -54169,7 +54645,7 @@ class ReportAdapter extends ReportUtils {
54169
54645
  * so new storage instances are created with the updated base path.
54170
54646
  */
54171
54647
  clear() {
54172
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_CLEAR);
54648
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR);
54173
54649
  ReportWriter.clear();
54174
54650
  }
54175
54651
  /**
@@ -54177,7 +54653,7 @@ class ReportAdapter extends ReportUtils {
54177
54653
  * All future report writes will be no-ops.
54178
54654
  */
54179
54655
  useDummy() {
54180
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
54656
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
54181
54657
  ReportWriter.useDummy();
54182
54658
  }
54183
54659
  /**
@@ -54185,7 +54661,7 @@ class ReportAdapter extends ReportUtils {
54185
54661
  * All future report writes will use JSONL storage.
54186
54662
  */
54187
54663
  useJsonl() {
54188
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
54664
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
54189
54665
  ReportWriter.useJsonl();
54190
54666
  }
54191
54667
  }
@@ -54203,7 +54679,7 @@ const MARKDOWN_METHOD_NAME_USE_JSONL = "MarkdownAdapter.useJsonl";
54203
54679
  const MARKDOWN_METHOD_NAME_USE_DUMMY = "MarkdownAdapter.useDummy";
54204
54680
  const MARKDOWN_METHOD_NAME_CLEAR = "MarkdownAdapter.clear";
54205
54681
  /** Logger service injected as DI singleton */
54206
- const LOGGER_SERVICE$1 = new LoggerService();
54682
+ const LOGGER_SERVICE$2 = new LoggerService();
54207
54683
  /**
54208
54684
  * Default configuration that enables all markdown services.
54209
54685
  * Used when no specific configuration is provided to `enable()`.
@@ -54260,7 +54736,7 @@ class MarkdownUtils {
54260
54736
  * @returns Cleanup function that unsubscribes from all enabled services
54261
54737
  */
54262
54738
  this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
54263
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_ENABLE, {
54739
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_ENABLE, {
54264
54740
  backtest: bt,
54265
54741
  breakeven,
54266
54742
  heat,
@@ -54357,7 +54833,7 @@ class MarkdownUtils {
54357
54833
  * ```
54358
54834
  */
54359
54835
  this.disable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
54360
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_DISABLE, {
54836
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_DISABLE, {
54361
54837
  backtest: bt,
54362
54838
  breakeven,
54363
54839
  heat,
@@ -54443,7 +54919,7 @@ class MarkdownUtils {
54443
54919
  * @param config.max_drawdown - Clear max drawdown report data
54444
54920
  */
54445
54921
  this.clear = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
54446
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_CLEAR, {
54922
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_CLEAR, {
54447
54923
  backtest: bt,
54448
54924
  breakeven,
54449
54925
  heat,
@@ -54518,7 +54994,7 @@ class MarkdownAdapter extends MarkdownUtils {
54518
54994
  * @param Ctor - Constructor for markdown storage adapter
54519
54995
  */
54520
54996
  useMarkdownAdapter(Ctor) {
54521
- LOGGER_SERVICE$1.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
54997
+ LOGGER_SERVICE$2.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
54522
54998
  return MarkdownWriter.useMarkdownAdapter(Ctor);
54523
54999
  }
54524
55000
  /**
@@ -54527,7 +55003,7 @@ class MarkdownAdapter extends MarkdownUtils {
54527
55003
  * Each dump creates a separate .md file.
54528
55004
  */
54529
55005
  useMd() {
54530
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_MD);
55006
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_MD);
54531
55007
  MarkdownWriter.useMd();
54532
55008
  }
54533
55009
  /**
@@ -54536,7 +55012,7 @@ class MarkdownAdapter extends MarkdownUtils {
54536
55012
  * All dumps append to a single .jsonl file per markdown type.
54537
55013
  */
54538
55014
  useJsonl() {
54539
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
55015
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
54540
55016
  MarkdownWriter.useJsonl();
54541
55017
  }
54542
55018
  /**
@@ -54544,7 +55020,7 @@ class MarkdownAdapter extends MarkdownUtils {
54544
55020
  * All future markdown writes will be no-ops.
54545
55021
  */
54546
55022
  useDummy() {
54547
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
55023
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
54548
55024
  MarkdownWriter.useDummy();
54549
55025
  }
54550
55026
  }
@@ -63233,6 +63709,503 @@ class IntervalUtils {
63233
63709
  */
63234
63710
  const Interval = new IntervalUtils();
63235
63711
 
63712
+ const CRON_METHOD_NAME_REGISTER = "CronUtils.register";
63713
+ const CRON_METHOD_NAME_UNREGISTER = "CronUtils.unregister";
63714
+ const CRON_METHOD_NAME_CLEAR = "CronUtils.clear";
63715
+ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
63716
+ const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
63717
+ const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
63718
+ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
63719
+ /**
63720
+ * Local logger instance.
63721
+ *
63722
+ * Created directly rather than resolved from the DI container so that
63723
+ * `CronUtils` has no compile-time dependency on the rest of the framework
63724
+ * being bootstrapped — `Cron` can be imported and used in isolation.
63725
+ */
63726
+ const LOGGER_SERVICE$1 = new LoggerService();
63727
+ /**
63728
+ * Utility class for registering periodic tasks that fire on candle-interval
63729
+ * boundaries of the virtual time produced by parallel backtests.
63730
+ *
63731
+ * Exported as singleton instance `Cron` for convenient usage.
63732
+ *
63733
+ * Key property — **singleshot coordination across parallel backtests**:
63734
+ * when several `Backtest.background(symbol, ...)` runs hit the same aligned
63735
+ * boundary concurrently, the handler is invoked exactly once. Every parallel
63736
+ * `tick` for that boundary awaits the same in-flight promise and is released
63737
+ * together when the promise settles. After settlement the slot is cleared and
63738
+ * the next boundary produces a fresh promise.
63739
+ *
63740
+ * Typical wiring:
63741
+ *
63742
+ * @example
63743
+ * ```typescript
63744
+ * import { Cron, Backtest } from "backtest-kit";
63745
+ *
63746
+ * Cron.register({
63747
+ * name: "tg-signal-parser",
63748
+ * interval: "1h",
63749
+ * handler: async (symbol, when, backtest) => {
63750
+ * await parseTelegramSignalsToMongo(when);
63751
+ * },
63752
+ * });
63753
+ *
63754
+ * // Subscribe Cron to the engine's lifecycle subjects (beforeStart,
63755
+ * // idlePing, activePing, schedulePing) once at startup. After this every
63756
+ * // strategy tick is forwarded into Cron automatically.
63757
+ * Cron.enable();
63758
+ *
63759
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "TRXUSDT"]) {
63760
+ * Backtest.background(symbol, { strategyName, exchangeName, frameName });
63761
+ * }
63762
+ *
63763
+ * // On shutdown:
63764
+ * // Cron.disable();
63765
+ * ```
63766
+ */
63767
+ class CronUtils {
63768
+ constructor() {
63769
+ /**
63770
+ * Registered entries by `name`.
63771
+ *
63772
+ * Each record carries a monotonically increasing `generation` counter that
63773
+ * is bumped on every `register(entry)` call for the same name. The
63774
+ * generation participates in `firedKey` so writes from a still-in-flight
63775
+ * handler of a previous incarnation cannot poison `_firedOnce` for the
63776
+ * current incarnation — their key has a different generation suffix and
63777
+ * is simply ignored on lookup.
63778
+ */
63779
+ this._entries = new Map();
63780
+ /** Monotonic counter used to mint new entry generations on `register`. */
63781
+ this._generationCounter = 0;
63782
+ /**
63783
+ * In-flight handler slots.
63784
+ *
63785
+ * Slot key shape (always includes the generation suffix `:g${generation}`;
63786
+ * the `:${symbol}` scope is present only in fan-out mode):
63787
+ * - Periodic global: `${name}:${alignedMs}:g${generation}`.
63788
+ * - Periodic fan-out: `${name}:${alignedMs}:${symbol}:g${generation}`.
63789
+ * - Fire-once global: `${name}:once:g${generation}`.
63790
+ * - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
63791
+ *
63792
+ * Value is the shared in-flight handler promise. Every parallel `tick` for
63793
+ * the same slot key awaits this exact promise (mutex semantics) and is
63794
+ * released together when it settles. `_inFlight` is owned exclusively by
63795
+ * `_runEntry` — `clear()` does **not** touch it, so the singleshot promise
63796
+ * survives concurrent `clear` calls and continues to coordinate parallel
63797
+ * ticks until it settles.
63798
+ */
63799
+ this._inFlight = new Map();
63800
+ /**
63801
+ * Keys of fire-once entries whose handler has already settled successfully.
63802
+ *
63803
+ * Key shape (always includes the entry generation suffix `:g${generation}`):
63804
+ * - Global fire-once: `${name}:g${generation}`.
63805
+ * - Fan-out fire-once: `${name}:${symbol}:g${generation}` — one entry per
63806
+ * whitelisted symbol.
63807
+ *
63808
+ * The generation suffix isolates incarnations of the same `name`: writes
63809
+ * landing from a still-in-flight handler of a previous `register()` carry
63810
+ * the old generation and are never matched by the new entry's lookup.
63811
+ * Stale entries are pruned by `_clearFiredOnceFor` on `register`/`unregister`
63812
+ * and wiped by `clear()`.
63813
+ *
63814
+ * Looked up by `_tick` to decide whether to skip; written by `_runEntry`
63815
+ * on successful settle.
63816
+ */
63817
+ this._firedOnce = new Set();
63818
+ /**
63819
+ * Register a periodic cron entry.
63820
+ *
63821
+ * Idempotent on `name`: re-registering the same name replaces the previous
63822
+ * entry (interval/symbols/handler can all change). Re-registration does
63823
+ * **not** clear in-flight promises — entries still resolving complete with
63824
+ * the previous handler.
63825
+ *
63826
+ * @param entry - Entry configuration; see {@link CronEntry}.
63827
+ * @returns Disposer function — call it to unregister the entry.
63828
+ *
63829
+ * @example
63830
+ * ```typescript
63831
+ * const dispose = Cron.register({
63832
+ * name: "fetch-funding",
63833
+ * interval: "8h",
63834
+ * symbols: ["BTCUSDT", "ETHUSDT"],
63835
+ * handler: async (symbol, when, backtest) => { ... },
63836
+ * });
63837
+ * // Later:
63838
+ * dispose();
63839
+ * ```
63840
+ */
63841
+ this.register = (entry) => {
63842
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_REGISTER, {
63843
+ name: entry.name,
63844
+ interval: entry.interval,
63845
+ symbols: entry.symbols,
63846
+ });
63847
+ if (!entry.name) {
63848
+ throw new Error("CronUtils.register requires a non-empty name");
63849
+ }
63850
+ if (entry.name.includes(":")) {
63851
+ throw new Error(`CronUtils.register: name must not contain ':' (got "${entry.name}"). ` +
63852
+ `':' is reserved as the segment separator in slot keys.`);
63853
+ }
63854
+ if (entry.symbols) {
63855
+ for (const symbol of entry.symbols) {
63856
+ if (symbol.includes(":")) {
63857
+ throw new Error(`CronUtils.register: symbols[] entry must not contain ':' (got "${symbol}"). ` +
63858
+ `':' is reserved as the segment separator in slot keys.`);
63859
+ }
63860
+ }
63861
+ }
63862
+ this._clearFiredOnceFor(entry.name);
63863
+ const generation = ++this._generationCounter;
63864
+ this._entries.set(entry.name, { entry, generation });
63865
+ return () => this.unregister(entry.name);
63866
+ };
63867
+ /**
63868
+ * Remove a registered entry by name.
63869
+ *
63870
+ * Does not cancel handlers already in flight — those resolve on their own
63871
+ * and clear their slot via `.finally()`.
63872
+ *
63873
+ * @param name - Name passed to `register`.
63874
+ */
63875
+ this.unregister = (name) => {
63876
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
63877
+ this._entries.delete(name);
63878
+ this._clearFiredOnceFor(name);
63879
+ };
63880
+ /**
63881
+ * Clear fire-once marks so that fire-once entries can fire again.
63882
+ *
63883
+ * Does **not** touch `_inFlight` — that map holds shared in-flight handler
63884
+ * promises through which parallel `tick`s coordinate. Wiping it mid-flight
63885
+ * would let a new `tick` start a second handler for a boundary that's
63886
+ * already running, breaking the singleshot contract.
63887
+ *
63888
+ * Two modes:
63889
+ * - **Per-symbol** (`symbol` provided): clears only fan-out fire-once
63890
+ * marks for that symbol — keys of the shape `${name}:${symbol}:g${gen}`.
63891
+ * Global fire-once marks (`${name}:g${gen}`, no symbol component) are
63892
+ * left intact, since they are not attributable to a single symbol.
63893
+ * Useful for re-arming fan-out fire-once entries when a particular
63894
+ * symbol's run finishes and you want a future re-run to fire again.
63895
+ * - **All** (no argument): wipes every fire-once mark across all entries
63896
+ * and symbols. Registered entries are not removed — use `unregister`
63897
+ * (or the disposer returned by `register`) for that.
63898
+ *
63899
+ * **Race with in-flight handlers.** `_firedOnce` is written in
63900
+ * `_runEntry`'s `.finally()`, which can run *after* a concurrent
63901
+ * `clear()` call. In that case the fire-once mark reappears immediately
63902
+ * after being wiped, and the next tick will treat the entry as already
63903
+ * fired. This is consistent with the singleshot promise itself surviving
63904
+ * `clear()` — the handler is allowed to finish — and the entry's
63905
+ * generation suffix in `firedKey` guarantees the stale mark cannot
63906
+ * outlive a subsequent `register()` of the same name. If you need a hard
63907
+ * re-arm, `unregister` + `register` bumps the generation and makes any
63908
+ * late write a no-op.
63909
+ *
63910
+ * @param symbol - Optional symbol filter; if omitted, clears all fire-once
63911
+ * marks.
63912
+ */
63913
+ this.clear = (symbol) => {
63914
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_CLEAR, { symbol });
63915
+ if (!symbol) {
63916
+ this._firedOnce.clear();
63917
+ return;
63918
+ }
63919
+ const symbolSegment = `:${symbol}:`;
63920
+ for (const key of this._firedOnce) {
63921
+ if (key.includes(symbolSegment)) {
63922
+ this._firedOnce.delete(key);
63923
+ }
63924
+ }
63925
+ };
63926
+ /**
63927
+ * Process a virtual-time tick for `symbol` and fire any due cron entries.
63928
+ *
63929
+ * **Private.** Invoked exclusively by the lifecycle bridge installed in
63930
+ * {@link enable} — `beforeStart` / `idlePing` / `activePing` / `schedulePing`
63931
+ * are funneled here through a shared `singlerun` queue, so calls to
63932
+ * `_tick` are serialised end-to-end. Do not call directly.
63933
+ *
63934
+ * Algorithm (per registered entry):
63935
+ * 0. Base-align the incoming `when` down to the 1-minute boundary (`ts`).
63936
+ * Lifecycle subjects may emit with sub-second jitter; rounding here
63937
+ * guarantees that `beforeStart` / `idlePing` / `activePing` /
63938
+ * `schedulePing` for the same virtual minute all hash to the same
63939
+ * slot key.
63940
+ * 1. If `entry.symbols` is non-empty and does not include `symbol`, skip.
63941
+ * 2. Decide scope from `entry.symbols`:
63942
+ * - Empty/undefined → **global** (slot key has no symbol component).
63943
+ * - Non-empty → **fan-out**, slot key carries `:${symbol}` so each
63944
+ * whitelisted symbol gets its own slot and handler invocation.
63945
+ * 3. Append the current entry generation suffix `:g${generation}` to both
63946
+ * slot key and fired-once key. This isolates incarnations of the same
63947
+ * `name`: a `register()` after an in-flight handler bumps the
63948
+ * generation, so the late `_firedOnce` write from the old handler can
63949
+ * never block the new entry.
63950
+ * 4. **Fire-once** (`entry.interval === undefined`):
63951
+ * - If the entry's fired-once key is already in `_firedOnce`, skip.
63952
+ * - Slot key: `${name}:once` (+ scope) (+ gen).
63953
+ * - `aligned` = the 1-minute-aligned `when` from step 0.
63954
+ * 5. **Periodic** (`entry.interval` set):
63955
+ * - Align `when` further to the entry's interval via {@link alignToInterval}.
63956
+ * - If `ts !== alignedMs`, the tick is mid-interval — skip.
63957
+ * (This is the "remainder === 0" boundary check from the spec;
63958
+ * since `ts` is already on the 1-minute boundary, the check is exact
63959
+ * for `1m` and consistent for higher intervals.)
63960
+ * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
63961
+ * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
63962
+ * already exists, `await` the same promise. Otherwise invoke
63963
+ * `entry.handler`, store the promise, and `await` it. The slot is
63964
+ * removed in `.finally()` so the next boundary creates a fresh promise;
63965
+ * for fire-once entries the fired-once key is also added to
63966
+ * `_firedOnce` on success so subsequent ticks skip it.
63967
+ *
63968
+ * Errors thrown by `handler` are caught, logged via `console.error`, and
63969
+ * **not** rethrown — a failing handler must not break the per-symbol
63970
+ * tick loop or unblock other parallel backtests with an unhandled
63971
+ * rejection. A failed fire-once handler is **not** marked as fired and
63972
+ * will retry on the next tick.
63973
+ *
63974
+ * Requires active method context and execution context.
63975
+ *
63976
+ * @param symbol - Trading symbol from the current tick.
63977
+ * @param when - Virtual time of the current tick.
63978
+ * @param backtest - `true` for backtest ticks, `false` for live ticks.
63979
+ * Forwarded as the third argument to `entry.handler`. Only the value
63980
+ * from the tick that **opens** a given slot is observed by all parallel
63981
+ * awaiters of that slot.
63982
+ * @throws Error if method or execution context is missing.
63983
+ */
63984
+ this._tick = async (symbol, when, backtest) => {
63985
+ LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
63986
+ symbol,
63987
+ when,
63988
+ });
63989
+ if (!MethodContextService.hasContext()) {
63990
+ throw new Error("CronUtils _tick requires method context");
63991
+ }
63992
+ if (!ExecutionContextService.hasContext()) {
63993
+ throw new Error("CronUtils _tick requires execution context");
63994
+ }
63995
+ const ts = alignToInterval(when, "1m").getTime();
63996
+ const taskList = [];
63997
+ for (const { entry, generation } of this._entries.values()) {
63998
+ if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
63999
+ continue;
64000
+ }
64001
+ const perSymbol = !!entry.symbols?.length;
64002
+ const scope = perSymbol ? `:${symbol}` : "";
64003
+ const genSuffix = `:g${generation}`;
64004
+ let aligned;
64005
+ let alignedMs;
64006
+ let slotKey;
64007
+ let firedKey;
64008
+ if (entry.interval === undefined) {
64009
+ const onceKey = `${entry.name}${scope}${genSuffix}`;
64010
+ if (this._firedOnce.has(onceKey)) {
64011
+ continue;
64012
+ }
64013
+ aligned = alignToInterval(when, "1m");
64014
+ alignedMs = ts;
64015
+ slotKey = `${entry.name}:once${scope}${genSuffix}`;
64016
+ firedKey = onceKey;
64017
+ }
64018
+ else {
64019
+ aligned = alignToInterval(when, entry.interval);
64020
+ alignedMs = aligned.getTime();
64021
+ if (ts !== alignedMs) {
64022
+ continue;
64023
+ }
64024
+ slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
64025
+ firedKey = null;
64026
+ }
64027
+ let pending = this._inFlight.get(slotKey);
64028
+ if (!pending) {
64029
+ pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64030
+ this._inFlight.set(slotKey, pending);
64031
+ }
64032
+ taskList.push(pending);
64033
+ }
64034
+ await Promise.all(taskList);
64035
+ };
64036
+ /**
64037
+ * Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
64038
+ * entries fire automatically — no manual wiring of `listenTickBacktest` /
64039
+ * `listenSchedulePing` etc. needed.
64040
+ *
64041
+ * Subjects funneled into {@link _tick}:
64042
+ * - `beforeStartSubject` — first event of every run.
64043
+ * - `idlePingSubject` — every tick when no signal is pending or scheduled.
64044
+ * - `activePingSubject` — every tick while a pending signal is being monitored.
64045
+ * - `schedulePingSubject` — every tick while a scheduled signal is being monitored.
64046
+ *
64047
+ * All four subjects are subscribed to a single `singlerun`-wrapped
64048
+ * handler that builds `_tick(event.symbol, new Date(event.timestamp),
64049
+ * event.backtest)`. `singlerun` merges the four streams into one serial
64050
+ * queue: at most one `_tick` runs at a time, the next waits. This matters
64051
+ * because the engine can emit `beforeStart` and an immediate `idlePing`
64052
+ * on the very same minute, and concurrent `_tick`s on the same
64053
+ * `(symbol, minute)` would otherwise race to open the same `_inFlight`
64054
+ * slot before either commit. Together these four sources cover every
64055
+ * tick the engine processes for every `(symbol, virtual-minute)` pair
64056
+ * regardless of whether the strategy is idle, active, or scheduled.
64057
+ *
64058
+ * `enable` itself is wrapped in `singleshot`, so calling it repeatedly is
64059
+ * a no-op — subsequent calls return the same disposer. The disposer
64060
+ * unsubscribes from every subject and resets the singleshot so a future
64061
+ * `enable()` can re-subscribe cleanly. Equivalent to the
64062
+ * `RecentAdapter.enable` pattern.
64063
+ *
64064
+ * The `.subscribe` callbacks are synchronous wrappers around the
64065
+ * `singlerun`-async handler; `_tick`'s returned promise is awaited inside
64066
+ * `singlerun` to enforce ordering but not bubbled back to the subject.
64067
+ * Errors are caught and logged inside `_runEntry`.
64068
+ *
64069
+ * @returns Cleanup function that unsubscribes from all four subjects and
64070
+ * resets the singleshot. Idempotent.
64071
+ *
64072
+ * @example
64073
+ * ```typescript
64074
+ * import { Cron } from "backtest-kit";
64075
+ *
64076
+ * Cron.register({ name: "tg-parser", interval: "1h", handler });
64077
+ * Cron.enable(); // wire once at startup
64078
+ * // ... run backtests / live as usual
64079
+ * Cron.disable(); // on shutdown
64080
+ * ```
64081
+ */
64082
+ this.enable = singleshot(() => {
64083
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
64084
+ const handleTick = singlerun(async (event) => {
64085
+ return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
64086
+ });
64087
+ const unBeforeStart = beforeStartSubject.subscribe(handleTick);
64088
+ const unIdlePing = idlePingSubject.subscribe(handleTick);
64089
+ const unActivePing = activePingSubject.subscribe(handleTick);
64090
+ const unSchedulePing = schedulePingSubject.subscribe(handleTick);
64091
+ return compose(() => unBeforeStart(), () => unIdlePing(), () => unActivePing(), () => unSchedulePing(), () => this.enable.clear());
64092
+ });
64093
+ /**
64094
+ * Tear down the lifecycle subscriptions installed by {@link enable}.
64095
+ *
64096
+ * Safe to call multiple times and safe to call before `enable()` — both
64097
+ * are no-ops. Does **not** unregister entries, does **not** touch
64098
+ * `_inFlight`, and does **not** wipe `_firedOnce` (use `unregister` or
64099
+ * `clear()` for those).
64100
+ */
64101
+ this.disable = () => {
64102
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISABLE);
64103
+ if (this.enable.hasValue()) {
64104
+ const lastSubscription = this.enable();
64105
+ lastSubscription();
64106
+ }
64107
+ };
64108
+ /**
64109
+ * Hard-reset the entire `Cron` state.
64110
+ *
64111
+ * Performs in order:
64112
+ * 1. {@link disable} — tears down lifecycle subscriptions and resets the
64113
+ * `enable` singleshot so a future `enable()` re-subscribes cleanly.
64114
+ * 2. Wipes `_entries` — every {@link register}'ed entry is forgotten.
64115
+ * Disposers returned by previous `register()` calls become no-ops
64116
+ * (their `unregister(name)` will not find anything to remove).
64117
+ * 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
64118
+ * re-registration of the same `name` fires again on the next matching
64119
+ * tick.
64120
+ * 4. Does **not** touch `_inFlight` — in-flight handlers continue to
64121
+ * settle in the background and clear their own slots via `.finally()`.
64122
+ * Their final `_firedOnce.add(firedKey)` writes carry old-generation
64123
+ * keys and are harmless (lookup uses the post-dispose generation).
64124
+ *
64125
+ * Use from a CLI/session teardown when you want to throw away every
64126
+ * registration along with the lifecycle wiring — e.g. between two
64127
+ * independent runner scopes. For "just snap the subscriptions but keep
64128
+ * registrations" use {@link disable} instead; for "just re-arm fire-once
64129
+ * marks" use {@link clear}.
64130
+ *
64131
+ * Idempotent. Safe to call multiple times and safe to call before
64132
+ * `enable()` / without any registrations.
64133
+ */
64134
+ this.dispose = () => {
64135
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISPOSE);
64136
+ this.disable();
64137
+ this._entries.clear();
64138
+ this._firedOnce.clear();
64139
+ };
64140
+ }
64141
+ /**
64142
+ * Garbage-collect every `_firedOnce` key that belongs to the entry `name`
64143
+ * (any generation, global or fan-out).
64144
+ *
64145
+ * Called from `register`/`unregister` to free memory; **not** required
64146
+ * for correctness — the generation suffix already isolates re-registrations,
64147
+ * so leftover keys from old generations can never block a new entry.
64148
+ * They just sit unused until they are GC'd here or wiped by `clear()`.
64149
+ */
64150
+ _clearFiredOnceFor(name) {
64151
+ if (!name) {
64152
+ return;
64153
+ }
64154
+ const prefix = `${name}:`;
64155
+ for (const key of this._firedOnce) {
64156
+ if (key === name || key.startsWith(prefix)) {
64157
+ this._firedOnce.delete(key);
64158
+ }
64159
+ }
64160
+ }
64161
+ /**
64162
+ * Build the singleshot promise for a single in-flight slot.
64163
+ *
64164
+ * Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
64165
+ * any error via `console.error`, and clears the `_inFlight` slot
64166
+ * in `.finally()` so the next boundary produces a fresh promise. For
64167
+ * fire-once entries `firedKey` is added to `_firedOnce` on success so
64168
+ * subsequent ticks skip it.
64169
+ *
64170
+ * @param firedKey - Key to add to `_firedOnce` on success, or `null` for
64171
+ * periodic entries (which never populate `_firedOnce`).
64172
+ * @param backtest - Value forwarded as the third handler argument; the
64173
+ * "winner" tick's flag is what all parallel awaiters of this slot see.
64174
+ */
64175
+ async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
64176
+ let failed = false;
64177
+ try {
64178
+ await entry.handler(symbol, aligned, backtest);
64179
+ }
64180
+ catch (err) {
64181
+ failed = true;
64182
+ console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
64183
+ }
64184
+ finally {
64185
+ this._inFlight.delete(slotKey);
64186
+ if (!failed && firedKey !== null) {
64187
+ this._firedOnce.add(firedKey);
64188
+ }
64189
+ }
64190
+ }
64191
+ }
64192
+ /**
64193
+ * Singleton instance of {@link CronUtils} for registering periodic tasks
64194
+ * coordinated across parallel `Backtest.background` runs.
64195
+ *
64196
+ * @example
64197
+ * ```typescript
64198
+ * import { Cron } from "backtest-kit";
64199
+ *
64200
+ * Cron.register({
64201
+ * name: "tg-parser",
64202
+ * interval: "1h",
64203
+ * handler: async (symbol, when, backtest) => { ... },
64204
+ * });
64205
+ * ```
64206
+ */
64207
+ const Cron = new CronUtils();
64208
+
63236
64209
  const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
63237
64210
  const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
63238
64211
  const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
@@ -64315,7 +65288,7 @@ const validateSignal = (signal, currentPrice) => {
64315
65288
  }
64316
65289
  }
64317
65290
  if (errors.length > 0) {
64318
- console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
65291
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
64319
65292
  return false;
64320
65293
  }
64321
65294
  try {
@@ -64388,9 +65361,9 @@ const validateSignal = (signal, currentPrice) => {
64388
65361
  }
64389
65362
  }
64390
65363
  if (errors.length > 0) {
64391
- console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
65364
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
64392
65365
  }
64393
65366
  return !errors.length;
64394
65367
  };
64395
65368
 
64396
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
65369
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };