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.cjs CHANGED
@@ -962,7 +962,7 @@ async function writeFileAtomic(file, data, options = {}) {
962
962
 
963
963
  var _a$3;
964
964
  /** Logger service injected as DI singleton */
965
- const LOGGER_SERVICE$8 = new LoggerService();
965
+ const LOGGER_SERVICE$9 = new LoggerService();
966
966
  /** Symbol key for the singleshot waitForInit function on PersistBase instances. */
967
967
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
968
968
  // Calculate step in milliseconds for candle close time validation
@@ -1085,7 +1085,7 @@ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
1085
1085
  const BASE_UNLINK_RETRY_COUNT = 5;
1086
1086
  const BASE_UNLINK_RETRY_DELAY = 1000;
1087
1087
  const BASE_WAIT_FOR_INIT_FN = async (self) => {
1088
- LOGGER_SERVICE$8.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
1088
+ LOGGER_SERVICE$9.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
1089
1089
  entityName: self.entityName,
1090
1090
  directory: self._directory,
1091
1091
  });
@@ -1143,7 +1143,7 @@ class PersistBase {
1143
1143
  this.entityName = entityName;
1144
1144
  this.baseDir = baseDir;
1145
1145
  this[_a$3] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1146
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1146
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1147
1147
  entityName: this.entityName,
1148
1148
  baseDir,
1149
1149
  });
@@ -1159,14 +1159,14 @@ class PersistBase {
1159
1159
  return path.join(this.baseDir, this.entityName, `${entityId}.json`);
1160
1160
  }
1161
1161
  async waitForInit(initial) {
1162
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
1162
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
1163
1163
  entityName: this.entityName,
1164
1164
  initial,
1165
1165
  });
1166
1166
  await this[BASE_WAIT_FOR_INIT_SYMBOL]();
1167
1167
  }
1168
1168
  async readValue(entityId) {
1169
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
1169
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
1170
1170
  entityName: this.entityName,
1171
1171
  entityId,
1172
1172
  });
@@ -1183,7 +1183,7 @@ class PersistBase {
1183
1183
  }
1184
1184
  }
1185
1185
  async hasValue(entityId) {
1186
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1186
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1187
1187
  entityName: this.entityName,
1188
1188
  entityId,
1189
1189
  });
@@ -1200,7 +1200,7 @@ class PersistBase {
1200
1200
  }
1201
1201
  }
1202
1202
  async writeValue(entityId, entity) {
1203
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
1203
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
1204
1204
  entityName: this.entityName,
1205
1205
  entityId,
1206
1206
  });
@@ -1222,7 +1222,7 @@ class PersistBase {
1222
1222
  * @throws Error if reading fails
1223
1223
  */
1224
1224
  async *keys() {
1225
- LOGGER_SERVICE$8.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
1225
+ LOGGER_SERVICE$9.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
1226
1226
  entityName: this.entityName,
1227
1227
  });
1228
1228
  try {
@@ -1367,7 +1367,7 @@ class PersistSignalUtils {
1367
1367
  * @returns Promise resolving to signal or null if none persisted
1368
1368
  */
1369
1369
  this.readSignalData = async (symbol, strategyName, exchangeName) => {
1370
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1370
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1371
1371
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1372
1372
  const isInitial = !this.getStorage.has(key);
1373
1373
  const instance = this.getStorage(symbol, strategyName, exchangeName);
@@ -1385,7 +1385,7 @@ class PersistSignalUtils {
1385
1385
  * @returns Promise that resolves when write is complete
1386
1386
  */
1387
1387
  this.writeSignalData = async (signalRow, symbol, strategyName, exchangeName) => {
1388
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1388
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1389
1389
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1390
1390
  const isInitial = !this.getStorage.has(key);
1391
1391
  const instance = this.getStorage(symbol, strategyName, exchangeName);
@@ -1400,7 +1400,7 @@ class PersistSignalUtils {
1400
1400
  * @param Ctor - Custom IPersistSignalInstance constructor
1401
1401
  */
1402
1402
  usePersistSignalAdapter(Ctor) {
1403
- LOGGER_SERVICE$8.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1403
+ LOGGER_SERVICE$9.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1404
1404
  this.PersistSignalInstanceCtor = Ctor;
1405
1405
  this.getStorage.clear();
1406
1406
  }
@@ -1409,21 +1409,21 @@ class PersistSignalUtils {
1409
1409
  * Call when process.cwd() changes between strategy iterations.
1410
1410
  */
1411
1411
  clear() {
1412
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
1412
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
1413
1413
  this.getStorage.clear();
1414
1414
  }
1415
1415
  /**
1416
1416
  * Switches to the default file-based PersistSignalInstance.
1417
1417
  */
1418
1418
  useJson() {
1419
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1419
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1420
1420
  this.usePersistSignalAdapter(PersistSignalInstance);
1421
1421
  }
1422
1422
  /**
1423
1423
  * Switches to PersistSignalDummyInstance (all operations are no-ops).
1424
1424
  */
1425
1425
  useDummy() {
1426
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1426
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1427
1427
  this.usePersistSignalAdapter(PersistSignalDummyInstance);
1428
1428
  }
1429
1429
  }
@@ -1563,7 +1563,7 @@ class PersistRiskUtils {
1563
1563
  * @returns Promise resolving to position entries (empty array if none)
1564
1564
  */
1565
1565
  this.readPositionData = async (riskName, exchangeName, when) => {
1566
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1566
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1567
1567
  const key = `${riskName}:${exchangeName}`;
1568
1568
  const isInitial = !this.getRiskStorage.has(key);
1569
1569
  const instance = this.getRiskStorage(riskName, exchangeName);
@@ -1581,7 +1581,7 @@ class PersistRiskUtils {
1581
1581
  * @returns Promise that resolves when write is complete
1582
1582
  */
1583
1583
  this.writePositionData = async (riskRow, riskName, exchangeName, when) => {
1584
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1584
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1585
1585
  const key = `${riskName}:${exchangeName}`;
1586
1586
  const isInitial = !this.getRiskStorage.has(key);
1587
1587
  const instance = this.getRiskStorage(riskName, exchangeName);
@@ -1596,7 +1596,7 @@ class PersistRiskUtils {
1596
1596
  * @param Ctor - Custom IPersistRiskInstance constructor
1597
1597
  */
1598
1598
  usePersistRiskAdapter(Ctor) {
1599
- LOGGER_SERVICE$8.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1599
+ LOGGER_SERVICE$9.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1600
1600
  this.PersistRiskInstanceCtor = Ctor;
1601
1601
  this.getRiskStorage.clear();
1602
1602
  }
@@ -1605,21 +1605,21 @@ class PersistRiskUtils {
1605
1605
  * Call when process.cwd() changes between strategy iterations.
1606
1606
  */
1607
1607
  clear() {
1608
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
1608
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
1609
1609
  this.getRiskStorage.clear();
1610
1610
  }
1611
1611
  /**
1612
1612
  * Switches to the default file-based PersistRiskInstance.
1613
1613
  */
1614
1614
  useJson() {
1615
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1615
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1616
1616
  this.usePersistRiskAdapter(PersistRiskInstance);
1617
1617
  }
1618
1618
  /**
1619
1619
  * Switches to PersistRiskDummyInstance (all operations are no-ops).
1620
1620
  */
1621
1621
  useDummy() {
1622
- LOGGER_SERVICE$8.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1622
+ LOGGER_SERVICE$9.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1623
1623
  this.usePersistRiskAdapter(PersistRiskDummyInstance);
1624
1624
  }
1625
1625
  }
@@ -1758,7 +1758,7 @@ class PersistScheduleUtils {
1758
1758
  * @returns Promise resolving to scheduled signal or null if none persisted
1759
1759
  */
1760
1760
  this.readScheduleData = async (symbol, strategyName, exchangeName) => {
1761
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1761
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1762
1762
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1763
1763
  const isInitial = !this.getScheduleStorage.has(key);
1764
1764
  const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
@@ -1776,7 +1776,7 @@ class PersistScheduleUtils {
1776
1776
  * @returns Promise that resolves when write is complete
1777
1777
  */
1778
1778
  this.writeScheduleData = async (scheduledSignalRow, symbol, strategyName, exchangeName) => {
1779
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1779
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1780
1780
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1781
1781
  const isInitial = !this.getScheduleStorage.has(key);
1782
1782
  const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
@@ -1791,7 +1791,7 @@ class PersistScheduleUtils {
1791
1791
  * @param Ctor - Custom IPersistScheduleInstance constructor
1792
1792
  */
1793
1793
  usePersistScheduleAdapter(Ctor) {
1794
- LOGGER_SERVICE$8.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1794
+ LOGGER_SERVICE$9.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1795
1795
  this.PersistScheduleInstanceCtor = Ctor;
1796
1796
  this.getScheduleStorage.clear();
1797
1797
  }
@@ -1800,21 +1800,21 @@ class PersistScheduleUtils {
1800
1800
  * Call when process.cwd() changes between strategy iterations.
1801
1801
  */
1802
1802
  clear() {
1803
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
1803
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
1804
1804
  this.getScheduleStorage.clear();
1805
1805
  }
1806
1806
  /**
1807
1807
  * Switches to the default file-based PersistScheduleInstance.
1808
1808
  */
1809
1809
  useJson() {
1810
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1810
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1811
1811
  this.usePersistScheduleAdapter(PersistScheduleInstance);
1812
1812
  }
1813
1813
  /**
1814
1814
  * Switches to PersistScheduleDummyInstance (all operations are no-ops).
1815
1815
  */
1816
1816
  useDummy() {
1817
- LOGGER_SERVICE$8.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1817
+ LOGGER_SERVICE$9.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1818
1818
  this.usePersistScheduleAdapter(PersistScheduleDummyInstance);
1819
1819
  }
1820
1820
  }
@@ -1959,7 +1959,7 @@ class PersistPartialUtils {
1959
1959
  * @returns Promise resolving to partial data record (empty object if none)
1960
1960
  */
1961
1961
  this.readPartialData = async (symbol, strategyName, signalId, exchangeName, when) => {
1962
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1962
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_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);
@@ -1979,7 +1979,7 @@ class PersistPartialUtils {
1979
1979
  * @returns Promise that resolves when write is complete
1980
1980
  */
1981
1981
  this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName, when) => {
1982
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1982
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1983
1983
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1984
1984
  const isInitial = !this.getPartialStorage.has(key);
1985
1985
  const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
@@ -1994,7 +1994,7 @@ class PersistPartialUtils {
1994
1994
  * @param Ctor - Custom IPersistPartialInstance constructor
1995
1995
  */
1996
1996
  usePersistPartialAdapter(Ctor) {
1997
- LOGGER_SERVICE$8.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1997
+ LOGGER_SERVICE$9.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1998
1998
  this.PersistPartialInstanceCtor = Ctor;
1999
1999
  this.getPartialStorage.clear();
2000
2000
  }
@@ -2003,21 +2003,21 @@ class PersistPartialUtils {
2003
2003
  * Call when process.cwd() changes between strategy iterations.
2004
2004
  */
2005
2005
  clear() {
2006
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
2006
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
2007
2007
  this.getPartialStorage.clear();
2008
2008
  }
2009
2009
  /**
2010
2010
  * Switches to the default file-based PersistPartialInstance.
2011
2011
  */
2012
2012
  useJson() {
2013
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
2013
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
2014
2014
  this.usePersistPartialAdapter(PersistPartialInstance);
2015
2015
  }
2016
2016
  /**
2017
2017
  * Switches to PersistPartialDummyInstance (all operations are no-ops).
2018
2018
  */
2019
2019
  useDummy() {
2020
- LOGGER_SERVICE$8.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
2020
+ LOGGER_SERVICE$9.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
2021
2021
  this.usePersistPartialAdapter(PersistPartialDummyInstance);
2022
2022
  }
2023
2023
  }
@@ -2182,7 +2182,7 @@ class PersistBreakevenUtils {
2182
2182
  * @returns Promise resolving to breakeven data record (empty object if none)
2183
2183
  */
2184
2184
  this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName, when) => {
2185
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
2185
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_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);
@@ -2202,7 +2202,7 @@ class PersistBreakevenUtils {
2202
2202
  * @returns Promise that resolves when write is complete
2203
2203
  */
2204
2204
  this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName, when) => {
2205
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
2205
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
2206
2206
  const key = `${symbol}:${strategyName}:${exchangeName}`;
2207
2207
  const isInitial = !this.getBreakevenStorage.has(key);
2208
2208
  const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
@@ -2217,7 +2217,7 @@ class PersistBreakevenUtils {
2217
2217
  * @param Ctor - Custom IPersistBreakevenInstance constructor
2218
2218
  */
2219
2219
  usePersistBreakevenAdapter(Ctor) {
2220
- LOGGER_SERVICE$8.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
2220
+ LOGGER_SERVICE$9.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
2221
2221
  this.PersistBreakevenInstanceCtor = Ctor;
2222
2222
  this.getBreakevenStorage.clear();
2223
2223
  }
@@ -2226,21 +2226,21 @@ class PersistBreakevenUtils {
2226
2226
  * Call when process.cwd() changes between strategy iterations.
2227
2227
  */
2228
2228
  clear() {
2229
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
2229
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
2230
2230
  this.getBreakevenStorage.clear();
2231
2231
  }
2232
2232
  /**
2233
2233
  * Switches to the default file-based PersistBreakevenInstance.
2234
2234
  */
2235
2235
  useJson() {
2236
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
2236
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
2237
2237
  this.usePersistBreakevenAdapter(PersistBreakevenInstance);
2238
2238
  }
2239
2239
  /**
2240
2240
  * Switches to PersistBreakevenDummyInstance (all operations are no-ops).
2241
2241
  */
2242
2242
  useDummy() {
2243
- LOGGER_SERVICE$8.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
2243
+ LOGGER_SERVICE$9.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
2244
2244
  this.usePersistBreakevenAdapter(PersistBreakevenDummyInstance);
2245
2245
  }
2246
2246
  }
@@ -2331,7 +2331,7 @@ class PersistCandleInstance {
2331
2331
  error: functoolsKit.errorData(error),
2332
2332
  message: functoolsKit.getErrorMessage(error),
2333
2333
  };
2334
- LOGGER_SERVICE$8.warn(message, payload);
2334
+ LOGGER_SERVICE$9.warn(message, payload);
2335
2335
  console.warn(message, payload);
2336
2336
  errorEmitter.next(error);
2337
2337
  return null;
@@ -2353,7 +2353,7 @@ class PersistCandleInstance {
2353
2353
  for (const candle of candles) {
2354
2354
  const candleCloseTime = candle.timestamp + stepMs;
2355
2355
  if (candleCloseTime > now) {
2356
- LOGGER_SERVICE$8.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
2356
+ LOGGER_SERVICE$9.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
2357
2357
  symbol: this.symbol,
2358
2358
  interval: this.interval,
2359
2359
  exchangeName: this.exchangeName,
@@ -2430,7 +2430,7 @@ class PersistCandleUtils {
2430
2430
  * @returns Promise resolving to candles in order, or null on cache miss
2431
2431
  */
2432
2432
  this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
2433
- LOGGER_SERVICE$8.info("PersistCandleUtils.readCandlesData", {
2433
+ LOGGER_SERVICE$9.info("PersistCandleUtils.readCandlesData", {
2434
2434
  symbol,
2435
2435
  interval,
2436
2436
  exchangeName,
@@ -2454,7 +2454,7 @@ class PersistCandleUtils {
2454
2454
  * @returns Promise that resolves when all writes are complete
2455
2455
  */
2456
2456
  this.writeCandlesData = async (candles, symbol, interval, exchangeName) => {
2457
- LOGGER_SERVICE$8.info("PersistCandleUtils.writeCandlesData", {
2457
+ LOGGER_SERVICE$9.info("PersistCandleUtils.writeCandlesData", {
2458
2458
  symbol,
2459
2459
  interval,
2460
2460
  exchangeName,
@@ -2474,7 +2474,7 @@ class PersistCandleUtils {
2474
2474
  * @param Ctor - Custom IPersistCandleInstance constructor
2475
2475
  */
2476
2476
  usePersistCandleAdapter(Ctor) {
2477
- LOGGER_SERVICE$8.info("PersistCandleUtils.usePersistCandleAdapter");
2477
+ LOGGER_SERVICE$9.info("PersistCandleUtils.usePersistCandleAdapter");
2478
2478
  this.PersistCandleInstanceCtor = Ctor;
2479
2479
  this.getCandlesStorage.clear();
2480
2480
  }
@@ -2483,21 +2483,21 @@ class PersistCandleUtils {
2483
2483
  * Call when process.cwd() changes between strategy iterations.
2484
2484
  */
2485
2485
  clear() {
2486
- LOGGER_SERVICE$8.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
2486
+ LOGGER_SERVICE$9.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
2487
2487
  this.getCandlesStorage.clear();
2488
2488
  }
2489
2489
  /**
2490
2490
  * Switches to the default file-based PersistCandleInstance.
2491
2491
  */
2492
2492
  useJson() {
2493
- LOGGER_SERVICE$8.log("PersistCandleUtils.useJson");
2493
+ LOGGER_SERVICE$9.log("PersistCandleUtils.useJson");
2494
2494
  this.usePersistCandleAdapter(PersistCandleInstance);
2495
2495
  }
2496
2496
  /**
2497
2497
  * Switches to PersistCandleDummyInstance (always returns null on read, discards writes).
2498
2498
  */
2499
2499
  useDummy() {
2500
- LOGGER_SERVICE$8.log("PersistCandleUtils.useDummy");
2500
+ LOGGER_SERVICE$9.log("PersistCandleUtils.useDummy");
2501
2501
  this.usePersistCandleAdapter(PersistCandleDummyInstance);
2502
2502
  }
2503
2503
  }
@@ -2635,7 +2635,7 @@ class PersistStorageUtils {
2635
2635
  * @returns Promise resolving to array of signal entries
2636
2636
  */
2637
2637
  this.readStorageData = async (backtest) => {
2638
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
2638
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
2639
2639
  const key = backtest ? `backtest` : `live`;
2640
2640
  const isInitial = !this.getStorage.has(key);
2641
2641
  const instance = this.getStorage(backtest);
@@ -2651,7 +2651,7 @@ class PersistStorageUtils {
2651
2651
  * @returns Promise that resolves when write is complete
2652
2652
  */
2653
2653
  this.writeStorageData = async (signalData, backtest) => {
2654
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
2654
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
2655
2655
  const key = backtest ? `backtest` : `live`;
2656
2656
  const isInitial = !this.getStorage.has(key);
2657
2657
  const instance = this.getStorage(backtest);
@@ -2666,7 +2666,7 @@ class PersistStorageUtils {
2666
2666
  * @param Ctor - Custom IPersistStorageInstance constructor
2667
2667
  */
2668
2668
  usePersistStorageAdapter(Ctor) {
2669
- LOGGER_SERVICE$8.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
2669
+ LOGGER_SERVICE$9.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
2670
2670
  this.PersistStorageInstanceCtor = Ctor;
2671
2671
  this.getStorage.clear();
2672
2672
  }
@@ -2675,21 +2675,21 @@ class PersistStorageUtils {
2675
2675
  * Call when process.cwd() changes between strategy iterations.
2676
2676
  */
2677
2677
  clear() {
2678
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
2678
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
2679
2679
  this.getStorage.clear();
2680
2680
  }
2681
2681
  /**
2682
2682
  * Switches to the default file-based PersistStorageInstance.
2683
2683
  */
2684
2684
  useJson() {
2685
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
2685
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
2686
2686
  this.usePersistStorageAdapter(PersistStorageInstance);
2687
2687
  }
2688
2688
  /**
2689
2689
  * Switches to PersistStorageDummyInstance (all operations are no-ops).
2690
2690
  */
2691
2691
  useDummy() {
2692
- LOGGER_SERVICE$8.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
2692
+ LOGGER_SERVICE$9.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
2693
2693
  this.usePersistStorageAdapter(PersistStorageDummyInstance);
2694
2694
  }
2695
2695
  }
@@ -2816,7 +2816,7 @@ class PersistNotificationUtils {
2816
2816
  * @returns Promise resolving to array of notification entries
2817
2817
  */
2818
2818
  this.readNotificationData = async (backtest) => {
2819
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
2819
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
2820
2820
  const key = backtest ? `backtest` : `live`;
2821
2821
  const isInitial = !this.getNotificationStorage.has(key);
2822
2822
  const instance = this.getNotificationStorage(backtest);
@@ -2832,7 +2832,7 @@ class PersistNotificationUtils {
2832
2832
  * @returns Promise that resolves when write is complete
2833
2833
  */
2834
2834
  this.writeNotificationData = async (notificationData, backtest) => {
2835
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
2835
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
2836
2836
  const key = backtest ? `backtest` : `live`;
2837
2837
  const isInitial = !this.getNotificationStorage.has(key);
2838
2838
  const instance = this.getNotificationStorage(backtest);
@@ -2847,7 +2847,7 @@ class PersistNotificationUtils {
2847
2847
  * @param Ctor - Custom IPersistNotificationInstance constructor
2848
2848
  */
2849
2849
  usePersistNotificationAdapter(Ctor) {
2850
- LOGGER_SERVICE$8.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
2850
+ LOGGER_SERVICE$9.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
2851
2851
  this.PersistNotificationInstanceCtor = Ctor;
2852
2852
  this.getNotificationStorage.clear();
2853
2853
  }
@@ -2857,21 +2857,21 @@ class PersistNotificationUtils {
2857
2857
  * instances are created with the updated base path.
2858
2858
  */
2859
2859
  clear() {
2860
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
2860
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
2861
2861
  this.getNotificationStorage.clear();
2862
2862
  }
2863
2863
  /**
2864
2864
  * Switches to the default file-based PersistNotificationInstance.
2865
2865
  */
2866
2866
  useJson() {
2867
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
2867
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
2868
2868
  this.usePersistNotificationAdapter(PersistNotificationInstance);
2869
2869
  }
2870
2870
  /**
2871
2871
  * Switches to PersistNotificationDummyInstance (all operations are no-ops).
2872
2872
  */
2873
2873
  useDummy() {
2874
- LOGGER_SERVICE$8.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
2874
+ LOGGER_SERVICE$9.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
2875
2875
  this.usePersistNotificationAdapter(PersistNotificationDummyInstance);
2876
2876
  }
2877
2877
  }
@@ -2995,7 +2995,7 @@ class PersistLogUtils {
2995
2995
  * @returns Promise resolving to array of log entries
2996
2996
  */
2997
2997
  this.readLogData = async () => {
2998
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
2998
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
2999
2999
  const isInitial = !this._logInstance;
3000
3000
  const instance = this.getLogInstance();
3001
3001
  await instance.waitForInit(isInitial);
@@ -3009,7 +3009,7 @@ class PersistLogUtils {
3009
3009
  * @returns Promise that resolves when write is complete
3010
3010
  */
3011
3011
  this.writeLogData = async (logData) => {
3012
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
3012
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
3013
3013
  const isInitial = !this._logInstance;
3014
3014
  const instance = this.getLogInstance();
3015
3015
  await instance.waitForInit(isInitial);
@@ -3034,7 +3034,7 @@ class PersistLogUtils {
3034
3034
  * @param Ctor - Custom IPersistLogInstance constructor
3035
3035
  */
3036
3036
  usePersistLogAdapter(Ctor) {
3037
- LOGGER_SERVICE$8.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
3037
+ LOGGER_SERVICE$9.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
3038
3038
  this.PersistLogInstanceCtor = Ctor;
3039
3039
  this._logInstance = null;
3040
3040
  }
@@ -3043,21 +3043,21 @@ class PersistLogUtils {
3043
3043
  * Call when process.cwd() changes between strategy iterations.
3044
3044
  */
3045
3045
  clear() {
3046
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
3046
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
3047
3047
  this._logInstance = null;
3048
3048
  }
3049
3049
  /**
3050
3050
  * Switches to the default file-based PersistLogInstance.
3051
3051
  */
3052
3052
  useJson() {
3053
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
3053
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
3054
3054
  this.usePersistLogAdapter(PersistLogInstance);
3055
3055
  }
3056
3056
  /**
3057
3057
  * Switches to PersistLogDummyInstance (all operations are no-ops).
3058
3058
  */
3059
3059
  useDummy() {
3060
- LOGGER_SERVICE$8.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
3060
+ LOGGER_SERVICE$9.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
3061
3061
  this.usePersistLogAdapter(PersistLogDummyInstance);
3062
3062
  }
3063
3063
  }
@@ -3219,7 +3219,7 @@ class PersistMeasureUtils {
3219
3219
  * @returns Promise resolving to cached value, or null if not found / soft-deleted
3220
3220
  */
3221
3221
  this.readMeasureData = async (bucket, key) => {
3222
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3222
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3223
3223
  const isInitial = !this.getMeasureStorage.has(bucket);
3224
3224
  const instance = this.getMeasureStorage(bucket);
3225
3225
  await instance.waitForInit(isInitial);
@@ -3235,7 +3235,7 @@ class PersistMeasureUtils {
3235
3235
  * @returns Promise that resolves when write is complete
3236
3236
  */
3237
3237
  this.writeMeasureData = async (data, bucket, key, when) => {
3238
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3238
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3239
3239
  const isInitial = !this.getMeasureStorage.has(bucket);
3240
3240
  const instance = this.getMeasureStorage(bucket);
3241
3241
  await instance.waitForInit(isInitial);
@@ -3250,7 +3250,7 @@ class PersistMeasureUtils {
3250
3250
  * @returns Promise that resolves when removal is complete
3251
3251
  */
3252
3252
  this.removeMeasureData = async (bucket, key) => {
3253
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3253
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3254
3254
  const isInitial = !this.getMeasureStorage.has(bucket);
3255
3255
  const instance = this.getMeasureStorage(bucket);
3256
3256
  await instance.waitForInit(isInitial);
@@ -3264,7 +3264,7 @@ class PersistMeasureUtils {
3264
3264
  * @param Ctor - Custom IPersistMeasureInstance constructor
3265
3265
  */
3266
3266
  usePersistMeasureAdapter(Ctor) {
3267
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
3267
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
3268
3268
  this.PersistMeasureInstanceCtor = Ctor;
3269
3269
  this.getMeasureStorage.clear();
3270
3270
  }
@@ -3276,7 +3276,7 @@ class PersistMeasureUtils {
3276
3276
  * @returns AsyncGenerator yielding entry keys
3277
3277
  */
3278
3278
  async *listMeasureData(bucket) {
3279
- LOGGER_SERVICE$8.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3279
+ LOGGER_SERVICE$9.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3280
3280
  const isInitial = !this.getMeasureStorage.has(bucket);
3281
3281
  const instance = this.getMeasureStorage(bucket);
3282
3282
  await instance.waitForInit(isInitial);
@@ -3287,21 +3287,21 @@ class PersistMeasureUtils {
3287
3287
  * Call when process.cwd() changes between strategy iterations.
3288
3288
  */
3289
3289
  clear() {
3290
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
3290
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
3291
3291
  this.getMeasureStorage.clear();
3292
3292
  }
3293
3293
  /**
3294
3294
  * Switches to the default file-based PersistMeasureInstance.
3295
3295
  */
3296
3296
  useJson() {
3297
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
3297
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
3298
3298
  this.usePersistMeasureAdapter(PersistMeasureInstance);
3299
3299
  }
3300
3300
  /**
3301
3301
  * Switches to PersistMeasureDummyInstance (all operations are no-ops).
3302
3302
  */
3303
3303
  useDummy() {
3304
- LOGGER_SERVICE$8.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
3304
+ LOGGER_SERVICE$9.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
3305
3305
  this.usePersistMeasureAdapter(PersistMeasureDummyInstance);
3306
3306
  }
3307
3307
  }
@@ -3460,7 +3460,7 @@ class PersistIntervalUtils {
3460
3460
  * @returns Promise resolving to marker data, or null if not found / soft-deleted
3461
3461
  */
3462
3462
  this.readIntervalData = async (bucket, key) => {
3463
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3463
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
3464
3464
  const isInitial = !this.getIntervalStorage.has(bucket);
3465
3465
  const instance = this.getIntervalStorage(bucket);
3466
3466
  await instance.waitForInit(isInitial);
@@ -3476,7 +3476,7 @@ class PersistIntervalUtils {
3476
3476
  * @returns Promise that resolves when write is complete
3477
3477
  */
3478
3478
  this.writeIntervalData = async (data, bucket, key, when) => {
3479
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3479
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3480
3480
  const isInitial = !this.getIntervalStorage.has(bucket);
3481
3481
  const instance = this.getIntervalStorage(bucket);
3482
3482
  await instance.waitForInit(isInitial);
@@ -3491,7 +3491,7 @@ class PersistIntervalUtils {
3491
3491
  * @returns Promise that resolves when removal is complete
3492
3492
  */
3493
3493
  this.removeIntervalData = async (bucket, key) => {
3494
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3494
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
3495
3495
  const isInitial = !this.getIntervalStorage.has(bucket);
3496
3496
  const instance = this.getIntervalStorage(bucket);
3497
3497
  await instance.waitForInit(isInitial);
@@ -3505,7 +3505,7 @@ class PersistIntervalUtils {
3505
3505
  * @param Ctor - Custom IPersistIntervalInstance constructor
3506
3506
  */
3507
3507
  usePersistIntervalAdapter(Ctor) {
3508
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
3508
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
3509
3509
  this.PersistIntervalInstanceCtor = Ctor;
3510
3510
  this.getIntervalStorage.clear();
3511
3511
  }
@@ -3517,7 +3517,7 @@ class PersistIntervalUtils {
3517
3517
  * @returns AsyncGenerator yielding marker keys
3518
3518
  */
3519
3519
  async *listIntervalData(bucket) {
3520
- LOGGER_SERVICE$8.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3520
+ LOGGER_SERVICE$9.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
3521
3521
  const isInitial = !this.getIntervalStorage.has(bucket);
3522
3522
  const instance = this.getIntervalStorage(bucket);
3523
3523
  await instance.waitForInit(isInitial);
@@ -3528,21 +3528,21 @@ class PersistIntervalUtils {
3528
3528
  * Call when process.cwd() changes between strategy iterations.
3529
3529
  */
3530
3530
  clear() {
3531
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
3531
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
3532
3532
  this.getIntervalStorage.clear();
3533
3533
  }
3534
3534
  /**
3535
3535
  * Switches to the default file-based PersistIntervalInstance.
3536
3536
  */
3537
3537
  useJson() {
3538
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
3538
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
3539
3539
  this.usePersistIntervalAdapter(PersistIntervalInstance);
3540
3540
  }
3541
3541
  /**
3542
3542
  * Switches to PersistIntervalDummyInstance (all operations are no-ops).
3543
3543
  */
3544
3544
  useDummy() {
3545
- LOGGER_SERVICE$8.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
3545
+ LOGGER_SERVICE$9.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
3546
3546
  this.usePersistIntervalAdapter(PersistIntervalDummyInstance);
3547
3547
  }
3548
3548
  }
@@ -3748,7 +3748,7 @@ class PersistMemoryUtils {
3748
3748
  * @returns Promise resolving to entry data, or null if not found / soft-deleted
3749
3749
  */
3750
3750
  this.readMemoryData = async (signalId, bucketName, memoryId) => {
3751
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
3751
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
3752
3752
  const key = `${signalId}:${bucketName}`;
3753
3753
  const isInitial = !this.getMemoryStorage.has(key);
3754
3754
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3765,7 +3765,7 @@ class PersistMemoryUtils {
3765
3765
  * @returns Promise resolving to true if entry exists
3766
3766
  */
3767
3767
  this.hasMemoryData = async (signalId, bucketName, memoryId) => {
3768
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
3768
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
3769
3769
  const key = `${signalId}:${bucketName}`;
3770
3770
  const isInitial = !this.getMemoryStorage.has(key);
3771
3771
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3784,7 +3784,7 @@ class PersistMemoryUtils {
3784
3784
  * @returns Promise that resolves when write is complete
3785
3785
  */
3786
3786
  this.writeMemoryData = async (data, signalId, bucketName, memoryId, when) => {
3787
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
3787
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
3788
3788
  const key = `${signalId}:${bucketName}`;
3789
3789
  const isInitial = !this.getMemoryStorage.has(key);
3790
3790
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3801,7 +3801,7 @@ class PersistMemoryUtils {
3801
3801
  * @returns Promise that resolves when removal is complete
3802
3802
  */
3803
3803
  this.removeMemoryData = async (signalId, bucketName, memoryId) => {
3804
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
3804
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
3805
3805
  const key = `${signalId}:${bucketName}`;
3806
3806
  const isInitial = !this.getMemoryStorage.has(key);
3807
3807
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3813,7 +3813,7 @@ class PersistMemoryUtils {
3813
3813
  * Call when process.cwd() changes between strategy iterations.
3814
3814
  */
3815
3815
  this.clear = () => {
3816
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
3816
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
3817
3817
  this.getMemoryStorage.clear();
3818
3818
  };
3819
3819
  /**
@@ -3824,7 +3824,7 @@ class PersistMemoryUtils {
3824
3824
  * @param bucketName - Bucket name
3825
3825
  */
3826
3826
  this.dispose = (signalId, bucketName) => {
3827
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
3827
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
3828
3828
  const key = `${signalId}:${bucketName}`;
3829
3829
  this.getMemoryStorage.clear(key);
3830
3830
  };
@@ -3836,7 +3836,7 @@ class PersistMemoryUtils {
3836
3836
  * @param Ctor - Custom IPersistMemoryInstance constructor
3837
3837
  */
3838
3838
  usePersistMemoryAdapter(Ctor) {
3839
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
3839
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
3840
3840
  this.PersistMemoryInstanceCtor = Ctor;
3841
3841
  this.getMemoryStorage.clear();
3842
3842
  }
@@ -3850,7 +3850,7 @@ class PersistMemoryUtils {
3850
3850
  * @returns AsyncGenerator yielding `{ memoryId, data }` tuples
3851
3851
  */
3852
3852
  async *listMemoryData(signalId, bucketName) {
3853
- LOGGER_SERVICE$8.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
3853
+ LOGGER_SERVICE$9.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
3854
3854
  const key = `${signalId}:${bucketName}`;
3855
3855
  const isInitial = !this.getMemoryStorage.has(key);
3856
3856
  const instance = this.getMemoryStorage(signalId, bucketName);
@@ -3861,14 +3861,14 @@ class PersistMemoryUtils {
3861
3861
  * Switches to the default file-based PersistMemoryInstance.
3862
3862
  */
3863
3863
  useJson() {
3864
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
3864
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
3865
3865
  this.usePersistMemoryAdapter(PersistMemoryInstance);
3866
3866
  }
3867
3867
  /**
3868
3868
  * Switches to PersistMemoryDummyInstance (all operations are no-ops).
3869
3869
  */
3870
3870
  useDummy() {
3871
- LOGGER_SERVICE$8.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
3871
+ LOGGER_SERVICE$9.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
3872
3872
  this.usePersistMemoryAdapter(PersistMemoryDummyInstance);
3873
3873
  }
3874
3874
  }
@@ -4017,7 +4017,7 @@ class PersistRecentUtils {
4017
4017
  * @returns Promise resolving to recent signal or null if none persisted
4018
4018
  */
4019
4019
  this.readRecentData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
4020
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
4020
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
4021
4021
  const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
4022
4022
  const isInitial = !this.getStorage.has(key);
4023
4023
  const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
@@ -4038,7 +4038,7 @@ class PersistRecentUtils {
4038
4038
  * @returns Promise that resolves when write is complete
4039
4039
  */
4040
4040
  this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest, when) => {
4041
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
4041
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
4042
4042
  const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
4043
4043
  const isInitial = !this.getStorage.has(key);
4044
4044
  const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
@@ -4071,7 +4071,7 @@ class PersistRecentUtils {
4071
4071
  * @param Ctor - Custom IPersistRecentInstance constructor
4072
4072
  */
4073
4073
  usePersistRecentAdapter(Ctor) {
4074
- LOGGER_SERVICE$8.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
4074
+ LOGGER_SERVICE$9.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
4075
4075
  this.PersistRecentInstanceCtor = Ctor;
4076
4076
  this.getStorage.clear();
4077
4077
  }
@@ -4080,21 +4080,21 @@ class PersistRecentUtils {
4080
4080
  * Call when process.cwd() changes between strategy iterations.
4081
4081
  */
4082
4082
  clear() {
4083
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
4083
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
4084
4084
  this.getStorage.clear();
4085
4085
  }
4086
4086
  /**
4087
4087
  * Switches to the default file-based PersistRecentInstance.
4088
4088
  */
4089
4089
  useJson() {
4090
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
4090
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
4091
4091
  this.usePersistRecentAdapter(PersistRecentInstance);
4092
4092
  }
4093
4093
  /**
4094
4094
  * Switches to PersistRecentDummyInstance (all operations are no-ops).
4095
4095
  */
4096
4096
  useDummy() {
4097
- LOGGER_SERVICE$8.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
4097
+ LOGGER_SERVICE$9.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
4098
4098
  this.usePersistRecentAdapter(PersistRecentDummyInstance);
4099
4099
  }
4100
4100
  }
@@ -4229,7 +4229,7 @@ class PersistStateUtils {
4229
4229
  * @returns Promise that resolves when initialization is complete
4230
4230
  */
4231
4231
  this.waitForInit = async (signalId, bucketName, initial) => {
4232
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
4232
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
4233
4233
  const key = `${signalId}:${bucketName}`;
4234
4234
  const isInitial = initial && !this.getStateStorage.has(key);
4235
4235
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4244,7 +4244,7 @@ class PersistStateUtils {
4244
4244
  * @returns Promise resolving to state data or null if none persisted
4245
4245
  */
4246
4246
  this.readStateData = async (signalId, bucketName) => {
4247
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
4247
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
4248
4248
  const key = `${signalId}:${bucketName}`;
4249
4249
  const isInitial = !this.getStateStorage.has(key);
4250
4250
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4262,7 +4262,7 @@ class PersistStateUtils {
4262
4262
  * @returns Promise that resolves when write is complete
4263
4263
  */
4264
4264
  this.writeStateData = async (data, signalId, bucketName, when) => {
4265
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
4265
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
4266
4266
  const key = `${signalId}:${bucketName}`;
4267
4267
  const isInitial = !this.getStateStorage.has(key);
4268
4268
  const instance = this.getStateStorage(signalId, bucketName);
@@ -4273,14 +4273,14 @@ class PersistStateUtils {
4273
4273
  * Switches to PersistStateDummyInstance (all operations are no-ops).
4274
4274
  */
4275
4275
  this.useDummy = () => {
4276
- LOGGER_SERVICE$8.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
4276
+ LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
4277
4277
  this.usePersistStateAdapter(PersistStateDummyInstance);
4278
4278
  };
4279
4279
  /**
4280
4280
  * Switches to the default file-based PersistStateInstance.
4281
4281
  */
4282
4282
  this.useJson = () => {
4283
- LOGGER_SERVICE$8.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
4283
+ LOGGER_SERVICE$9.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
4284
4284
  this.usePersistStateAdapter(PersistStateInstance);
4285
4285
  };
4286
4286
  /**
@@ -4288,7 +4288,7 @@ class PersistStateUtils {
4288
4288
  * Call when process.cwd() changes between strategy iterations.
4289
4289
  */
4290
4290
  this.clear = () => {
4291
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
4291
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
4292
4292
  this.getStateStorage.clear();
4293
4293
  };
4294
4294
  /**
@@ -4299,7 +4299,7 @@ class PersistStateUtils {
4299
4299
  * @param bucketName - Bucket name
4300
4300
  */
4301
4301
  this.dispose = (signalId, bucketName) => {
4302
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
4302
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_DISPOSE);
4303
4303
  const key = `${signalId}:${bucketName}`;
4304
4304
  this.getStateStorage.clear(key);
4305
4305
  };
@@ -4311,7 +4311,7 @@ class PersistStateUtils {
4311
4311
  * @param Ctor - Custom IPersistStateInstance constructor
4312
4312
  */
4313
4313
  usePersistStateAdapter(Ctor) {
4314
- LOGGER_SERVICE$8.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
4314
+ LOGGER_SERVICE$9.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
4315
4315
  this.PersistStateInstanceCtor = Ctor;
4316
4316
  this.getStateStorage.clear();
4317
4317
  }
@@ -4451,7 +4451,7 @@ class PersistSessionUtils {
4451
4451
  * @returns Promise that resolves when initialization is complete
4452
4452
  */
4453
4453
  this.waitForInit = async (strategyName, exchangeName, frameName, initial) => {
4454
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
4454
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
4455
4455
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4456
4456
  const isInitial = initial && !this.getSessionStorage.has(key);
4457
4457
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4467,7 +4467,7 @@ class PersistSessionUtils {
4467
4467
  * @returns Promise resolving to session data or null if none persisted
4468
4468
  */
4469
4469
  this.readSessionData = async (strategyName, exchangeName, frameName) => {
4470
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
4470
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
4471
4471
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4472
4472
  const isInitial = !this.getSessionStorage.has(key);
4473
4473
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4486,7 +4486,7 @@ class PersistSessionUtils {
4486
4486
  * @returns Promise that resolves when write is complete
4487
4487
  */
4488
4488
  this.writeSessionData = async (data, strategyName, exchangeName, frameName, when) => {
4489
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
4489
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
4490
4490
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4491
4491
  const isInitial = !this.getSessionStorage.has(key);
4492
4492
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
@@ -4497,14 +4497,14 @@ class PersistSessionUtils {
4497
4497
  * Switches to PersistSessionDummyInstance (all operations are no-ops).
4498
4498
  */
4499
4499
  this.useDummy = () => {
4500
- LOGGER_SERVICE$8.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
4500
+ LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
4501
4501
  this.usePersistSessionAdapter(PersistSessionDummyInstance);
4502
4502
  };
4503
4503
  /**
4504
4504
  * Switches to the default file-based PersistSessionInstance.
4505
4505
  */
4506
4506
  this.useJson = () => {
4507
- LOGGER_SERVICE$8.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
4507
+ LOGGER_SERVICE$9.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
4508
4508
  this.usePersistSessionAdapter(PersistSessionInstance);
4509
4509
  };
4510
4510
  /**
@@ -4512,7 +4512,7 @@ class PersistSessionUtils {
4512
4512
  * Call when process.cwd() changes between strategy iterations.
4513
4513
  */
4514
4514
  this.clear = () => {
4515
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
4515
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
4516
4516
  this.getSessionStorage.clear();
4517
4517
  };
4518
4518
  /**
@@ -4524,7 +4524,7 @@ class PersistSessionUtils {
4524
4524
  * @param frameName - Frame identifier
4525
4525
  */
4526
4526
  this.dispose = (strategyName, exchangeName, frameName) => {
4527
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
4527
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_DISPOSE);
4528
4528
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4529
4529
  this.getSessionStorage.clear(key);
4530
4530
  };
@@ -4536,7 +4536,7 @@ class PersistSessionUtils {
4536
4536
  * @param Ctor - Custom IPersistSessionInstance constructor
4537
4537
  */
4538
4538
  usePersistSessionAdapter(Ctor) {
4539
- LOGGER_SERVICE$8.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
4539
+ LOGGER_SERVICE$9.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
4540
4540
  this.PersistSessionInstanceCtor = Ctor;
4541
4541
  this.getSessionStorage.clear();
4542
4542
  }
@@ -4658,7 +4658,7 @@ const METHOD_NAME_ADD_ACTIVITY = "LookupUtils.addActivity";
4658
4658
  const METHOD_NAME_REMOVE_ACTIVITY = "LookupUtils.removeActivity";
4659
4659
  const METHOD_NAME_LIST_ACTIVITY = "LookupUtils.listActivity";
4660
4660
  /** Logger service injected as DI singleton */
4661
- const LOGGER_SERVICE$7 = new LoggerService();
4661
+ const LOGGER_SERVICE$8 = new LoggerService();
4662
4662
  /**
4663
4663
  * Builds the composite {@link Key} used to register an activity in `_lookupMap`.
4664
4664
  *
@@ -4712,7 +4712,7 @@ class LookupUtils {
4712
4712
  * @param activity - Activity descriptor identifying the running workload.
4713
4713
  */
4714
4714
  this.addActivity = (activity) => {
4715
- LOGGER_SERVICE$7.info(METHOD_NAME_ADD_ACTIVITY, {
4715
+ LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
4716
4716
  activity,
4717
4717
  });
4718
4718
  const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
@@ -4726,7 +4726,7 @@ class LookupUtils {
4726
4726
  * @param activity - Activity descriptor matching the one passed to {@link addActivity}.
4727
4727
  */
4728
4728
  this.removeActivity = (activity) => {
4729
- LOGGER_SERVICE$7.info(METHOD_NAME_REMOVE_ACTIVITY, {
4729
+ LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
4730
4730
  activity,
4731
4731
  });
4732
4732
  const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
@@ -4738,7 +4738,7 @@ class LookupUtils {
4738
4738
  * @returns Array of all activities present in the lookup map at call time.
4739
4739
  */
4740
4740
  this.listActivity = () => {
4741
- LOGGER_SERVICE$7.info(METHOD_NAME_LIST_ACTIVITY);
4741
+ LOGGER_SERVICE$8.info(METHOD_NAME_LIST_ACTIVITY);
4742
4742
  return Array.from(this._lookupMap.values());
4743
4743
  };
4744
4744
  }
@@ -4767,7 +4767,7 @@ const METHOD_NAME_SPIN_LOCK = "CandleUtils.spinLock";
4767
4767
  */
4768
4768
  const ROTATE_DELAY = 50;
4769
4769
  /** Logger service injected as DI singleton */
4770
- const LOGGER_SERVICE$6 = new LoggerService();
4770
+ const LOGGER_SERVICE$7 = new LoggerService();
4771
4771
  /**
4772
4772
  * Process-wide coordinator for candle-fetch serialization and cooperative
4773
4773
  * yielding between parallel backtests.
@@ -4816,7 +4816,7 @@ class CandleUtils {
4816
4816
  * @param source - Caller identifier for logging.
4817
4817
  */
4818
4818
  this.acquireLock = async (source) => {
4819
- LOGGER_SERVICE$6.info(METHOD_NAME_ACQUIRE_LOCK, {
4819
+ LOGGER_SERVICE$7.info(METHOD_NAME_ACQUIRE_LOCK, {
4820
4820
  source,
4821
4821
  });
4822
4822
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -4832,7 +4832,7 @@ class CandleUtils {
4832
4832
  * @param source - Caller identifier for logging.
4833
4833
  */
4834
4834
  this.releaseLock = async (source) => {
4835
- LOGGER_SERVICE$6.info(METHOD_NAME_RELEASE_LOCK, {
4835
+ LOGGER_SERVICE$7.info(METHOD_NAME_RELEASE_LOCK, {
4836
4836
  source,
4837
4837
  });
4838
4838
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -4857,7 +4857,7 @@ class CandleUtils {
4857
4857
  * @param source - Caller identifier for logging.
4858
4858
  */
4859
4859
  this.spinLock = async (source) => {
4860
- LOGGER_SERVICE$6.info(METHOD_NAME_SPIN_LOCK, {
4860
+ LOGGER_SERVICE$7.info(METHOD_NAME_SPIN_LOCK, {
4861
4861
  source,
4862
4862
  });
4863
4863
  if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
@@ -6367,7 +6367,7 @@ const validateCommonSignal = (signal) => {
6367
6367
  }
6368
6368
  // Кидаем ошибку если есть проблемы
6369
6369
  if (errors.length > 0) {
6370
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6370
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6371
6371
  }
6372
6372
  };
6373
6373
 
@@ -6420,7 +6420,7 @@ const validatePendingSignal = (signal, currentPrice) => {
6420
6420
  }
6421
6421
  }
6422
6422
  if (errors.length > 0) {
6423
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6423
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6424
6424
  }
6425
6425
  validateCommonSignal(signal);
6426
6426
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
@@ -6468,7 +6468,7 @@ const validatePendingSignal = (signal, currentPrice) => {
6468
6468
  }
6469
6469
  }
6470
6470
  if (errors.length > 0) {
6471
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6471
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6472
6472
  }
6473
6473
  };
6474
6474
 
@@ -6521,7 +6521,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
6521
6521
  }
6522
6522
  }
6523
6523
  if (errors.length > 0) {
6524
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6524
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6525
6525
  }
6526
6526
  validateCommonSignal(signal);
6527
6527
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
@@ -6567,7 +6567,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
6567
6567
  // pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
6568
6568
  }
6569
6569
  if (errors.length > 0) {
6570
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
6570
+ throw new Error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6571
6571
  }
6572
6572
  };
6573
6573
 
@@ -7014,6 +7014,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
7014
7014
  if (!signal) {
7015
7015
  return null;
7016
7016
  }
7017
+ if (signal?.symbol && signal?.symbol !== self.params.execution.context.symbol) {
7018
+ throw new Error(`Symbol mismatch: expected ${self.params.execution.context.symbol}, got ${signal.symbol}`);
7019
+ }
7020
+ // Whipsaw protection: skip signal if its id matches the last accepted pending id
7021
+ if (signal.id && signal.id === self._lastPendingId) {
7022
+ return null;
7023
+ }
7017
7024
  if (self._isStopped) {
7018
7025
  return null;
7019
7026
  }
@@ -7057,6 +7064,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
7057
7064
  }
7058
7065
  // Валидируем сигнал перед возвратом
7059
7066
  validatePendingSignal(signalRow, currentPrice);
7067
+ if (signal.id) {
7068
+ self._lastPendingId = signal.id;
7069
+ }
7060
7070
  return signalRow;
7061
7071
  }
7062
7072
  // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
@@ -7083,6 +7093,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
7083
7093
  };
7084
7094
  // Валидируем сигнал перед возвратом
7085
7095
  validateScheduledSignal(scheduledSignalRow, currentPrice);
7096
+ if (signal.id) {
7097
+ self._lastPendingId = signal.id;
7098
+ }
7086
7099
  return scheduledSignalRow;
7087
7100
  }
7088
7101
  const signalRow = {
@@ -7110,6 +7123,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
7110
7123
  }
7111
7124
  // Валидируем сигнал перед возвратом
7112
7125
  validatePendingSignal(signalRow, currentPrice);
7126
+ if (signal.id) {
7127
+ self._lastPendingId = signal.id;
7128
+ }
7113
7129
  return signalRow;
7114
7130
  }, {
7115
7131
  defaultValue: null,
@@ -7139,6 +7155,13 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7139
7155
  if (self.params.execution.context.backtest) {
7140
7156
  return;
7141
7157
  }
7158
+ // Restore last pending signal id for whipsaw protection in GET_SIGNAL_FN
7159
+ {
7160
+ const recentSignal = await PersistRecentAdapter.readRecentData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, false);
7161
+ if (recentSignal?.id) {
7162
+ self._lastPendingId = recentSignal.id;
7163
+ }
7164
+ }
7142
7165
  // Restore pending signal
7143
7166
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7144
7167
  if (pendingSignal) {
@@ -9279,6 +9302,7 @@ class ClientStrategy {
9279
9302
  this._isStopped = false;
9280
9303
  this._pendingSignal = null;
9281
9304
  this._lastSignalTimestamp = null;
9305
+ this._lastPendingId = null;
9282
9306
  this._scheduledSignal = null;
9283
9307
  this._cancelledSignal = null;
9284
9308
  this._closedSignal = null;
@@ -12068,7 +12092,7 @@ const RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE = "MergeRisk.checkSignalAndReser
12068
12092
  const RISK_METHOD_NAME_ADD_SIGNAL = "MergeRisk.addSignal";
12069
12093
  const RISK_METHOD_NAME_REMOVE_SIGNAL = "MergeRisk.removeSignal";
12070
12094
  /** Logger service injected as DI singleton */
12071
- const LOGGER_SERVICE$5 = new LoggerService();
12095
+ const LOGGER_SERVICE$6 = new LoggerService();
12072
12096
  /**
12073
12097
  * Composite risk management class that combines multiple risk profiles.
12074
12098
  *
@@ -12124,7 +12148,7 @@ class MergeRisk {
12124
12148
  * @returns Promise resolving to true if all risks approve, false if any risk rejects
12125
12149
  */
12126
12150
  async checkSignal(params, options = {}) {
12127
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
12151
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
12128
12152
  params,
12129
12153
  });
12130
12154
  for (const [riskName, risk] of Object.entries(this._riskMap)) {
@@ -12154,7 +12178,7 @@ class MergeRisk {
12154
12178
  * @returns Promise resolving to true if all risks approve (and reserved), false if any risk rejects
12155
12179
  */
12156
12180
  async checkSignalAndReserve(params) {
12157
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
12181
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
12158
12182
  params,
12159
12183
  });
12160
12184
  for (const [riskName, risk] of Object.entries(this._riskMap)) {
@@ -12178,7 +12202,7 @@ class MergeRisk {
12178
12202
  * @returns Promise that resolves when all risks have registered the signal
12179
12203
  */
12180
12204
  async addSignal(symbol, context, positionData) {
12181
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_ADD_SIGNAL, {
12205
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_ADD_SIGNAL, {
12182
12206
  symbol,
12183
12207
  context,
12184
12208
  });
@@ -12195,7 +12219,7 @@ class MergeRisk {
12195
12219
  * @returns Promise that resolves when all risks have removed the signal
12196
12220
  */
12197
12221
  async removeSignal(symbol, context) {
12198
- LOGGER_SERVICE$5.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
12222
+ LOGGER_SERVICE$6.info(RISK_METHOD_NAME_REMOVE_SIGNAL, {
12199
12223
  symbol,
12200
12224
  context,
12201
12225
  });
@@ -14988,7 +15012,7 @@ class RiskConnectionService {
14988
15012
  }
14989
15013
 
14990
15014
  /** Logger service injected as DI singleton */
14991
- const LOGGER_SERVICE$4 = new LoggerService();
15015
+ const LOGGER_SERVICE$5 = new LoggerService();
14992
15016
  /**
14993
15017
  * Wrapper to call init method with error capture.
14994
15018
  */
@@ -15003,7 +15027,7 @@ const CALL_INIT_FN = functoolsKit.trycatch(async (self) => {
15003
15027
  error: functoolsKit.errorData(error),
15004
15028
  message: functoolsKit.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_FN = functoolsKit.trycatch(async (event, self) => {
15023
15047
  error: functoolsKit.errorData(error),
15024
15048
  message: functoolsKit.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_LIVE_FN = functoolsKit.trycatch(async (event, self) => {
15043
15067
  error: functoolsKit.errorData(error),
15044
15068
  message: functoolsKit.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
  },
@@ -15063,7 +15087,7 @@ const CALL_SIGNAL_BACKTEST_FN = functoolsKit.trycatch(async (event, self) => {
15063
15087
  error: functoolsKit.errorData(error),
15064
15088
  message: functoolsKit.getErrorMessage(error),
15065
15089
  };
15066
- LOGGER_SERVICE$4.warn(message, payload);
15090
+ LOGGER_SERVICE$5.warn(message, payload);
15067
15091
  console.warn(message, payload);
15068
15092
  errorEmitter.next(error);
15069
15093
  },
@@ -15091,7 +15115,7 @@ const CALL_BREAKEVEN_AVAILABLE_FN = functoolsKit.trycatch(async (event, self) =>
15091
15115
  error: functoolsKit.errorData(error),
15092
15116
  message: functoolsKit.getErrorMessage(error),
15093
15117
  };
15094
- LOGGER_SERVICE$4.warn(message, payload);
15118
+ LOGGER_SERVICE$5.warn(message, payload);
15095
15119
  console.warn(message, payload);
15096
15120
  errorEmitter.next(error);
15097
15121
  },
@@ -15119,7 +15143,7 @@ const CALL_PARTIAL_PROFIT_AVAILABLE_FN = functoolsKit.trycatch(async (event, sel
15119
15143
  error: functoolsKit.errorData(error),
15120
15144
  message: functoolsKit.getErrorMessage(error),
15121
15145
  };
15122
- LOGGER_SERVICE$4.warn(message, payload);
15146
+ LOGGER_SERVICE$5.warn(message, payload);
15123
15147
  console.warn(message, payload);
15124
15148
  errorEmitter.next(error);
15125
15149
  },
@@ -15147,7 +15171,7 @@ const CALL_PARTIAL_LOSS_AVAILABLE_FN = functoolsKit.trycatch(async (event, self)
15147
15171
  error: functoolsKit.errorData(error),
15148
15172
  message: functoolsKit.getErrorMessage(error),
15149
15173
  };
15150
- LOGGER_SERVICE$4.warn(message, payload);
15174
+ LOGGER_SERVICE$5.warn(message, payload);
15151
15175
  console.warn(message, payload);
15152
15176
  errorEmitter.next(error);
15153
15177
  },
@@ -15175,7 +15199,7 @@ const CALL_PING_SCHEDULED_FN = functoolsKit.trycatch(async (event, self) => {
15175
15199
  error: functoolsKit.errorData(error),
15176
15200
  message: functoolsKit.getErrorMessage(error),
15177
15201
  };
15178
- LOGGER_SERVICE$4.warn(message, payload);
15202
+ LOGGER_SERVICE$5.warn(message, payload);
15179
15203
  console.warn(message, payload);
15180
15204
  errorEmitter.next(error);
15181
15205
  },
@@ -15203,7 +15227,7 @@ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
15203
15227
  error: functoolsKit.errorData(error),
15204
15228
  message: functoolsKit.getErrorMessage(error),
15205
15229
  };
15206
- LOGGER_SERVICE$4.warn(message, payload);
15230
+ LOGGER_SERVICE$5.warn(message, payload);
15207
15231
  console.warn(message, payload);
15208
15232
  errorEmitter.next(error);
15209
15233
  },
@@ -15231,7 +15255,7 @@ const CALL_PING_ACTIVE_FN = functoolsKit.trycatch(async (event, self) => {
15231
15255
  error: functoolsKit.errorData(error),
15232
15256
  message: functoolsKit.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_RISK_REJECTION_FN = functoolsKit.trycatch(async (event, self) => {
15251
15275
  error: functoolsKit.errorData(error),
15252
15276
  message: functoolsKit.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
  },
@@ -15271,7 +15295,7 @@ const CALL_DISPOSE_FN = functoolsKit.trycatch(async (self) => {
15271
15295
  error: functoolsKit.errorData(error),
15272
15296
  message: functoolsKit.getErrorMessage(error),
15273
15297
  };
15274
- LOGGER_SERVICE$4.warn(message, payload);
15298
+ LOGGER_SERVICE$5.warn(message, payload);
15275
15299
  console.warn(message, payload);
15276
15300
  errorEmitter.next(error);
15277
15301
  },
@@ -23135,7 +23159,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY$1 = "ReportUtils.useDummy";
23135
23159
  const REPORT_UTILS_METHOD_NAME_USE_JSONL$1 = "ReportUtils.useJsonl";
23136
23160
  const REPORT_UTILS_METHOD_NAME_CLEAR$1 = "ReportUtils.clear";
23137
23161
  /** Logger service injected as DI singleton */
23138
- const LOGGER_SERVICE$3 = new LoggerService();
23162
+ const LOGGER_SERVICE$4 = new LoggerService();
23139
23163
  /** Symbol key for the singleshot waitForInit function on MarkdownFileBase instances. */
23140
23164
  const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
23141
23165
  /** Symbol key for the timeout-protected write function on MarkdownFileBase instances. */
@@ -23216,7 +23240,7 @@ class MarkdownFileBase {
23216
23240
  * @throws Error if stream not initialized or write timeout exceeded
23217
23241
  */
23218
23242
  async dump(data, options) {
23219
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
23243
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FILE_DUMP, {
23220
23244
  markdownName: this.markdownName,
23221
23245
  options,
23222
23246
  });
@@ -23296,7 +23320,7 @@ class MarkdownFolderBase {
23296
23320
  * @throws Error if directory creation or file write fails
23297
23321
  */
23298
23322
  async dump(content, options) {
23299
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
23323
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_FOLDER_DUMP, {
23300
23324
  markdownName: this.markdownName,
23301
23325
  options,
23302
23326
  });
@@ -23361,7 +23385,7 @@ class MarkdownWriterAdapter {
23361
23385
  * @param Ctor - Constructor for markdown storage adapter
23362
23386
  */
23363
23387
  useMarkdownAdapter(Ctor) {
23364
- LOGGER_SERVICE$3.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
23388
+ LOGGER_SERVICE$4.info(MARKDOWN_METHOD_NAME_USE_ADAPTER$1);
23365
23389
  this.MarkdownFactory = Ctor;
23366
23390
  }
23367
23391
  /**
@@ -23375,7 +23399,7 @@ class MarkdownWriterAdapter {
23375
23399
  * @throws Error if write fails or storage initialization fails
23376
23400
  */
23377
23401
  async writeData(markdownName, content, options) {
23378
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
23402
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_WRITE_DATA, {
23379
23403
  markdownName,
23380
23404
  options,
23381
23405
  });
@@ -23389,7 +23413,7 @@ class MarkdownWriterAdapter {
23389
23413
  * Each report is written as a separate .md file.
23390
23414
  */
23391
23415
  useMd() {
23392
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
23416
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_MD$1);
23393
23417
  this.useMarkdownAdapter(MarkdownFolderBase);
23394
23418
  }
23395
23419
  /**
@@ -23397,7 +23421,7 @@ class MarkdownWriterAdapter {
23397
23421
  * All reports are appended to a single .jsonl file per markdown type.
23398
23422
  */
23399
23423
  useJsonl() {
23400
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
23424
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_JSONL$1);
23401
23425
  this.useMarkdownAdapter(MarkdownFileBase);
23402
23426
  }
23403
23427
  /**
@@ -23406,7 +23430,7 @@ class MarkdownWriterAdapter {
23406
23430
  * so new storage instances are created with the updated base path.
23407
23431
  */
23408
23432
  clear() {
23409
- LOGGER_SERVICE$3.log(MARKDOWN_METHOD_NAME_CLEAR$1);
23433
+ LOGGER_SERVICE$4.log(MARKDOWN_METHOD_NAME_CLEAR$1);
23410
23434
  this.getMarkdownStorage.clear();
23411
23435
  }
23412
23436
  /**
@@ -23414,7 +23438,7 @@ class MarkdownWriterAdapter {
23414
23438
  * All future markdown writes will be no-ops.
23415
23439
  */
23416
23440
  useDummy() {
23417
- LOGGER_SERVICE$3.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
23441
+ LOGGER_SERVICE$4.debug(MARKDOWN_METHOD_NAME_USE_DUMMY$1);
23418
23442
  this.useMarkdownAdapter(MarkdownDummy);
23419
23443
  }
23420
23444
  }
@@ -23475,7 +23499,7 @@ class ReportBase {
23475
23499
  });
23476
23500
  }
23477
23501
  }, 15000));
23478
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_CTOR, {
23502
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_CTOR, {
23479
23503
  reportName: this.reportName,
23480
23504
  baseDir,
23481
23505
  });
@@ -23489,7 +23513,7 @@ class ReportBase {
23489
23513
  * @returns Promise that resolves when initialization is complete
23490
23514
  */
23491
23515
  async waitForInit(initial) {
23492
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
23516
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
23493
23517
  reportName: this.reportName,
23494
23518
  initial,
23495
23519
  });
@@ -23508,7 +23532,7 @@ class ReportBase {
23508
23532
  * @throws Error if stream not initialized or write timeout exceeded
23509
23533
  */
23510
23534
  async write(data, options) {
23511
- LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_WRITE, {
23535
+ LOGGER_SERVICE$4.debug(REPORT_BASE_METHOD_NAME_WRITE, {
23512
23536
  reportName: this.reportName,
23513
23537
  options,
23514
23538
  });
@@ -23605,7 +23629,7 @@ class ReportWriterAdapter {
23605
23629
  * @internal - Automatically called by report services, not for direct use
23606
23630
  */
23607
23631
  this.writeData = async (reportName, data, options) => {
23608
- LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
23632
+ LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
23609
23633
  reportName,
23610
23634
  options,
23611
23635
  });
@@ -23622,7 +23646,7 @@ class ReportWriterAdapter {
23622
23646
  * @param Ctor - Constructor for report storage adapter
23623
23647
  */
23624
23648
  useReportAdapter(Ctor) {
23625
- LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
23649
+ LOGGER_SERVICE$4.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER$1);
23626
23650
  this.ReportFactory = Ctor;
23627
23651
  }
23628
23652
  /**
@@ -23631,7 +23655,7 @@ class ReportWriterAdapter {
23631
23655
  * so new storage instances are created with the updated base path.
23632
23656
  */
23633
23657
  clear() {
23634
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
23658
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_CLEAR$1);
23635
23659
  this.getReportStorage.clear();
23636
23660
  }
23637
23661
  /**
@@ -23639,7 +23663,7 @@ class ReportWriterAdapter {
23639
23663
  * All future report writes will be no-ops.
23640
23664
  */
23641
23665
  useDummy() {
23642
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
23666
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY$1);
23643
23667
  this.useReportAdapter(ReportDummy);
23644
23668
  }
23645
23669
  /**
@@ -23647,7 +23671,7 @@ class ReportWriterAdapter {
23647
23671
  * All future report writes will use JSONL storage.
23648
23672
  */
23649
23673
  useJsonl() {
23650
- LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
23674
+ LOGGER_SERVICE$4.log(REPORT_UTILS_METHOD_NAME_USE_JSONL$1);
23651
23675
  this.useReportAdapter(ReportBase);
23652
23676
  }
23653
23677
  }
@@ -23702,7 +23726,7 @@ const CREATE_FILE_NAME_FN$c = (symbol, strategyName, exchangeName, frameName, ti
23702
23726
  * @param value - Value to check
23703
23727
  * @returns true if value is unsafe, false otherwise
23704
23728
  */
23705
- function isUnsafe$3(value) {
23729
+ function isUnsafe$4(value) {
23706
23730
  if (typeof value !== "number") {
23707
23731
  return true;
23708
23732
  }
@@ -23714,6 +23738,25 @@ function isUnsafe$3(value) {
23714
23738
  }
23715
23739
  return false;
23716
23740
  }
23741
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
23742
+ const MIN_SIGNALS_FOR_ANNUALIZATION$2 = 10;
23743
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
23744
+ * sample size is too small to estimate variance meaningfully. */
23745
+ const MIN_SIGNALS_FOR_RATIOS$2 = 10;
23746
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
23747
+ const MIN_CALENDAR_SPAN_DAYS$2 = 14;
23748
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
23749
+ const MAX_TRADES_PER_YEAR$2 = 365;
23750
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
23751
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
23752
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
23753
+ const MAX_EXPECTED_YEARLY_RETURNS$2 = 100;
23754
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
23755
+ const MAX_CALMAR_RATIO$2 = 1000;
23756
+ /** Minimum stdDev required for Sharpe/Sortino computation. Identical-returns series produce
23757
+ * float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously inflates
23758
+ * sharpe to astronomical values. Treat any stdDev below this threshold as zero. */
23759
+ const STDDEV_EPSILON$2 = 1e-9;
23717
23760
  /**
23718
23761
  * Storage class for accumulating closed signals per strategy.
23719
23762
  * Maintains a list of all closed signals and provides methods to generate reports.
@@ -23767,65 +23810,190 @@ let ReportStorage$a = class ReportStorage {
23767
23810
  recoveryFactor: null,
23768
23811
  };
23769
23812
  }
23770
- const totalSignals = this._signalList.length;
23771
- const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
23772
- const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
23773
- // Calculate basic statistics
23774
- const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
23775
- const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
23776
- const winRate = (winCount / totalSignals) * 100;
23777
- // Calculate Sharpe Ratio (risk-free rate = 0)
23778
- const returns = this._signalList.map((s) => s.pnl.pnlPercentage);
23779
- const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalSignals;
23780
- const stdDev = Math.sqrt(variance);
23781
- const sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
23782
- const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
23783
- // Calculate Certainty Ratio
23784
- const wins = this._signalList.filter((s) => s.pnl.pnlPercentage > 0);
23785
- const losses = this._signalList.filter((s) => s.pnl.pnlPercentage < 0);
23813
+ // Valid signal set — those with usable pendingAt AND closeTimestamp. Single source
23814
+ // of truth for EVERY metric in this method (counts, sums, span, equity curve,
23815
+ // ratios, annualization). If we used different subsets for different metrics, the
23816
+ // numerator of one ratio could be drawn from a different population than the
23817
+ // denominator of another and the report would silently lie. On clean data
23818
+ // validSignals === this._signalList; the filter only matters for corrupted runtime
23819
+ // data.
23820
+ const validSignals = this._signalList.filter((s) => typeof s.signal.pendingAt === "number" && s.signal.pendingAt > 0 &&
23821
+ typeof s.closeTimestamp === "number" && s.closeTimestamp > 0);
23822
+ const totalSignals = validSignals.length;
23823
+ const winCount = validSignals.filter((s) => s.pnl.pnlPercentage > 0).length;
23824
+ const lossCount = validSignals.filter((s) => s.pnl.pnlPercentage < 0).length;
23825
+ // Basic statistics guard against an empty validSignals (e.g. every signal had
23826
+ // corrupted timestamps) so we don't divide by zero.
23827
+ const avgPnl = totalSignals > 0
23828
+ ? validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals
23829
+ : 0;
23830
+ const totalPnl = validSignals.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
23831
+ // Win rate excludes break-even trades from both numerator and denominator.
23832
+ const decisiveTrades = winCount + lossCount;
23833
+ const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
23834
+ // Calendar span over the same validSignals set used for ratios.
23835
+ let firstPendingAt = Infinity;
23836
+ let lastCloseAt = -Infinity;
23837
+ for (const s of validSignals) {
23838
+ if (s.signal.pendingAt < firstPendingAt)
23839
+ firstPendingAt = s.signal.pendingAt;
23840
+ if (s.closeTimestamp > lastCloseAt)
23841
+ lastCloseAt = s.closeTimestamp;
23842
+ }
23843
+ const calendarSpanDays = isFinite(firstPendingAt) && isFinite(lastCloseAt)
23844
+ ? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
23845
+ : 0;
23846
+ // tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
23847
+ // silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
23848
+ // raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
23849
+ // for reliable annualization and surface every annualized metric as null.
23850
+ const rawTradesPerYear = totalSignals >= MIN_SIGNALS_FOR_ANNUALIZATION$2 &&
23851
+ calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$2
23852
+ ? (totalSignals / calendarSpanDays) * 365
23853
+ : 0;
23854
+ const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$2;
23855
+ const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
23856
+ // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
23857
+ // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
23858
+ // are too noisy to publish (high chance of spurious ±Sharpe).
23859
+ const returns = validSignals.map((s) => s.pnl.pnlPercentage);
23860
+ const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
23861
+ const stdDev = canComputeRatios
23862
+ ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
23863
+ : 0;
23864
+ // Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
23865
+ // float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
23866
+ // inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
23867
+ const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
23868
+ ? avgPnl / stdDev
23869
+ : null;
23870
+ // Annualize only when gate passes; otherwise null.
23871
+ const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
23872
+ ? sharpeRatio * Math.sqrt(tradesPerYear)
23873
+ : null;
23874
+ // Equity-curve max drawdown via compounded equity (multiplicative, not additive).
23875
+ // Returns are per-trade on cost basis — compounding assumes equal capital allocation
23876
+ // per trade ("as-if 100% allocation"). Walks validSignals in chronological order
23877
+ // (storage is newest-first, so iterate in reverse). Using validSignals (same set as
23878
+ // tradesPerYear) keeps equityFinal consistent with the annualization exponent.
23879
+ // If equity goes ≤ 0 (e.g. leveraged short with r < -100%) — account blown,
23880
+ // fix DD at 100% and stop walking the curve.
23881
+ let equity = 1;
23882
+ let peak = 1;
23883
+ let equityMaxDrawdown = 0;
23884
+ let blown = false;
23885
+ for (let i = validSignals.length - 1; i >= 0; i--) {
23886
+ equity *= 1 + validSignals[i].pnl.pnlPercentage / 100;
23887
+ if (equity <= 0) {
23888
+ equityMaxDrawdown = 100;
23889
+ blown = true;
23890
+ break;
23891
+ }
23892
+ if (equity > peak)
23893
+ peak = equity;
23894
+ const dd = (peak - equity) / peak * 100;
23895
+ if (dd > equityMaxDrawdown)
23896
+ equityMaxDrawdown = dd;
23897
+ }
23898
+ const equityFinal = blown ? 0 : equity;
23899
+ // Compounded yearly return via geometric mean of equity curve.
23900
+ // equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag that
23901
+ // arithmetic-mean compounding ((1+avgPnl)^N) misses. If account is blown, full loss.
23902
+ // If the raw value would exceed MAX_EXPECTED_YEARLY_RETURNS, return null rather than
23903
+ // showing the cap as a real figure — capped numbers mislead users into trusting them.
23904
+ const expectedYearlyReturns = canAnnualize
23905
+ ? blown
23906
+ ? -100
23907
+ : (() => {
23908
+ // Geometric annualization uses validSignals.length (same set that defined
23909
+ // tradesPerYear); using totalSignals here would mismatch numerator/denominator.
23910
+ const raw = (Math.pow(equityFinal, tradesPerYear / validSignals.length) - 1) * 100;
23911
+ return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$2 ? null : raw;
23912
+ })()
23913
+ : null;
23914
+ // Certainty Ratio — over validSignals so wins/losses come from the same set as
23915
+ // winCount/lossCount/avgPnl above.
23916
+ const wins = validSignals.filter((s) => s.pnl.pnlPercentage > 0);
23917
+ const losses = validSignals.filter((s) => s.pnl.pnlPercentage < 0);
23786
23918
  const avgWin = wins.length > 0
23787
23919
  ? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
23788
23920
  : 0;
23789
23921
  const avgLoss = losses.length > 0
23790
23922
  ? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
23791
23923
  : 0;
23792
- const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
23793
- // Calculate Expected Yearly Returns
23794
- const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / totalSignals;
23795
- const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
23796
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
23797
- const expectedYearlyReturns = avgPnl * tradesPerYear;
23798
- // Calculate average peak and fall PNL across all signals
23799
- const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / totalSignals;
23800
- const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / totalSignals;
23801
- // Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
23802
- const fallReturns = this._signalList.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
23803
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
23804
- const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalSignals;
23805
- const fallDeviation = Math.sqrt(fallVariance);
23806
- const sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
23807
- // Max absolute drawdown across all signals — used as denominator for Calmar and Recovery
23808
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
23809
- const calmarRatio = maxAbsFall > 0 ? expectedYearlyReturns / maxAbsFall : 0;
23810
- const recoveryFactor = maxAbsFall > 0 ? totalPnl / maxAbsFall : 0;
23924
+ // Null below MIN_SIGNALS_FOR_RATIOS on a handful of trades the win/loss
23925
+ // means are too noisy to publish a ratio (same sample-size gate as Sharpe/
23926
+ // Sortino, so the report doesn't surface certainty while withholding the rest).
23927
+ // Also null when no losing trades OR when |avgLoss| is below STDDEV_EPSILON
23928
+ // (float-artifact losses (-1e-15) would otherwise produce a spurious
23929
+ // astronomical certaintyRatio ≈1e14).
23930
+ const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
23931
+ ? avgWin / Math.abs(avgLoss)
23932
+ : null;
23933
+ // Average peak/fall PNL over validSignals; only signals that actually have the
23934
+ // value contribute (no zero dilution from missing peakProfit/maxDrawdown).
23935
+ const peakValues = validSignals
23936
+ .map((s) => s.signal.peakProfit?.pnlPercentage)
23937
+ .filter((v) => typeof v === "number");
23938
+ const fallValues = validSignals
23939
+ .map((s) => s.signal.maxDrawdown?.pnlPercentage)
23940
+ .filter((v) => typeof v === "number");
23941
+ const avgPeakPnl = peakValues.length > 0
23942
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
23943
+ : null;
23944
+ const avgFallPnl = fallValues.length > 0
23945
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
23946
+ : null;
23947
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
23948
+ // downsideDev = √( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
23949
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
23950
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
23951
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
23952
+ // strategies.
23953
+ const negativeReturns = returns.filter((r) => r < 0);
23954
+ const sortinoRatio = (() => {
23955
+ if (!canComputeRatios)
23956
+ return null;
23957
+ if (negativeReturns.length === 0)
23958
+ return null;
23959
+ const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
23960
+ const downsideDeviation = Math.sqrt(downsideVariance);
23961
+ // Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
23962
+ return downsideDeviation > STDDEV_EPSILON$2 ? avgPnl / downsideDeviation : null;
23963
+ })();
23964
+ // Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
23965
+ const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
23966
+ ? Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, expectedYearlyReturns / equityMaxDrawdown))
23967
+ : null;
23968
+ // Recovery Factor: numerator must be the compounded total return (equityFinal − 1) × 100,
23969
+ // not the arithmetic totalPnl — denominator (equityMaxDrawdown) is from the compounded
23970
+ // curve, so mixing units would inflate Recovery on long winning streaks.
23971
+ // Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
23972
+ // so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
23973
+ // Null when account is blown — ratio is meaningless after total loss.
23974
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
23975
+ // and explode the same way when DD is near zero.
23976
+ const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
23977
+ ? null
23978
+ : Math.max(-MAX_CALMAR_RATIO$2, Math.min(MAX_CALMAR_RATIO$2, ((equityFinal - 1) * 100) / equityMaxDrawdown));
23811
23979
  return {
23812
23980
  signalList: this._signalList,
23813
23981
  totalSignals,
23814
23982
  winCount,
23815
23983
  lossCount,
23816
- winRate: isUnsafe$3(winRate) ? null : winRate,
23817
- avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
23818
- totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
23819
- stdDev: isUnsafe$3(stdDev) ? null : stdDev,
23820
- sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
23821
- annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
23822
- certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
23823
- expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
23824
- avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
23825
- avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
23826
- sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
23827
- calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
23828
- recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
23984
+ winRate: isUnsafe$4(winRate) ? null : winRate,
23985
+ avgPnl: isUnsafe$4(avgPnl) ? null : avgPnl,
23986
+ totalPnl: isUnsafe$4(totalPnl) ? null : totalPnl,
23987
+ stdDev: isUnsafe$4(stdDev) ? null : stdDev,
23988
+ sharpeRatio: isUnsafe$4(sharpeRatio) ? null : sharpeRatio,
23989
+ annualizedSharpeRatio: isUnsafe$4(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
23990
+ certaintyRatio: isUnsafe$4(certaintyRatio) ? null : certaintyRatio,
23991
+ expectedYearlyReturns: isUnsafe$4(expectedYearlyReturns) ? null : expectedYearlyReturns,
23992
+ avgPeakPnl: isUnsafe$4(avgPeakPnl) ? null : avgPeakPnl,
23993
+ avgFallPnl: isUnsafe$4(avgFallPnl) ? null : avgFallPnl,
23994
+ sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
23995
+ calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
23996
+ recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
23829
23997
  };
23830
23998
  }
23831
23999
  /**
@@ -23867,24 +24035,26 @@ let ReportStorage$a = class ReportStorage {
23867
24035
  `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
23868
24036
  `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
23869
24037
  `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
23870
- `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better, theoretical)`}`,
24038
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
23871
24039
  `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
23872
- `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better, theoretical)`}`,
24040
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
23873
24041
  `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
23874
24042
  `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
23875
24043
  `**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
23876
- `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better, theoretical)`}`,
24044
+ `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
23877
24045
  `**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
23878
24046
  "",
23879
24047
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
23880
24048
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
23881
- `*Annualized Sharpe Ratio: theoretical maximum assuming continuous trading. Real-world value is lower due to idle periods.*`,
23882
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24049
+ `*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.*`,
24050
+ `*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".*`,
23883
24051
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
23884
- `*Expected Yearly Returns: theoretical maximum assuming all capital is deployed continuously with no idle time.*`,
23885
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
23886
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
23887
- `*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.*`,
24052
+ `*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.*`,
24053
+ `*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}.*`,
24054
+ `*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.*`,
24055
+ `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24056
+ `*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.*`,
24057
+ `*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.*`,
23888
24058
  ].join("\n");
23889
24059
  }
23890
24060
  /**
@@ -24196,7 +24366,7 @@ const CREATE_FILE_NAME_FN$b = (symbol, strategyName, exchangeName, frameName, ti
24196
24366
  * @param value - Value to check
24197
24367
  * @returns true if value is unsafe, false otherwise
24198
24368
  */
24199
- function isUnsafe$2(value) {
24369
+ function isUnsafe$3(value) {
24200
24370
  if (typeof value !== "number") {
24201
24371
  return true;
24202
24372
  }
@@ -24208,6 +24378,25 @@ function isUnsafe$2(value) {
24208
24378
  }
24209
24379
  return false;
24210
24380
  }
24381
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
24382
+ const MIN_SIGNALS_FOR_ANNUALIZATION$1 = 10;
24383
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
24384
+ * sample size is too small to estimate variance meaningfully. */
24385
+ const MIN_SIGNALS_FOR_RATIOS$1 = 10;
24386
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
24387
+ const MIN_CALENDAR_SPAN_DAYS$1 = 14;
24388
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
24389
+ const MAX_TRADES_PER_YEAR$1 = 365;
24390
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
24391
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
24392
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
24393
+ const MAX_EXPECTED_YEARLY_RETURNS$1 = 100;
24394
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
24395
+ const MAX_CALMAR_RATIO$1 = 1000;
24396
+ /** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
24397
+ * float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
24398
+ * astronomical magnitudes (avgPnl / epsilon). */
24399
+ const STDDEV_EPSILON$1 = 1e-9;
24211
24400
  /**
24212
24401
  * Storage class for accumulating all tick events per strategy.
24213
24402
  * Maintains a chronological list of all events (idle, opened, active, closed).
@@ -24491,84 +24680,190 @@ let ReportStorage$9 = class ReportStorage {
24491
24680
  };
24492
24681
  }
24493
24682
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
24494
- const totalClosed = closedEvents.length;
24495
- const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
24496
- const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
24497
- // Calculate basic statistics
24498
- const avgPnl = totalClosed > 0
24499
- ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
24683
+ // Valid closed set — single source of truth. Events must have numeric pnl AND valid
24684
+ // timestamps. Win/loss counts, returns, calendar span, equity curve — all derived
24685
+ // from this set so they cannot disagree.
24686
+ const validClosed = closedEvents.filter((e) => typeof e.pnl === "number" &&
24687
+ typeof e.timestamp === "number" &&
24688
+ e.timestamp > 0 &&
24689
+ typeof (e.pendingAt ?? e.timestamp) === "number");
24690
+ const totalClosed = validClosed.length;
24691
+ const winCount = validClosed.filter((e) => e.pnl > 0).length;
24692
+ const lossCount = validClosed.filter((e) => e.pnl < 0).length;
24693
+ const returns = validClosed.map((e) => e.pnl);
24694
+ const avgPnl = returns.length > 0
24695
+ ? returns.reduce((sum, r) => sum + r, 0) / returns.length
24500
24696
  : 0;
24501
- const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
24502
- const winRate = (winCount / totalClosed) * 100;
24503
- // Calculate Sharpe Ratio (risk-free rate = 0)
24504
- let sharpeRatio = 0;
24505
- let stdDev = 0;
24506
- if (totalClosed > 0) {
24507
- const returns = closedEvents.map((e) => e.pnl || 0);
24508
- const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
24509
- stdDev = Math.sqrt(variance);
24510
- sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
24511
- }
24512
- const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
24513
- // Calculate Certainty Ratio
24514
- let certaintyRatio = 0;
24515
- if (totalClosed > 0) {
24516
- const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
24517
- const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
24697
+ const totalPnl = returns.reduce((sum, r) => sum + r, 0);
24698
+ // Win rate excludes break-even trades from both numerator and denominator.
24699
+ const decisiveTrades = winCount + lossCount;
24700
+ const winRate = decisiveTrades > 0 ? (winCount / decisiveTrades) * 100 : 0;
24701
+ // Trade frequency from calendar span — gated by minimum span and sample size to
24702
+ // suppress absurd annualization on short / sparse runs. Span built from validClosed
24703
+ // so denominator (calendarSpanDays) and numerator (returns.length) come from the
24704
+ // same event set.
24705
+ let firstPendingAt = Infinity;
24706
+ let lastCloseAt = -Infinity;
24707
+ for (const e of validClosed) {
24708
+ const startAt = e.pendingAt ?? e.timestamp;
24709
+ if (startAt < firstPendingAt)
24710
+ firstPendingAt = startAt;
24711
+ if (e.timestamp > lastCloseAt)
24712
+ lastCloseAt = e.timestamp;
24713
+ }
24714
+ const calendarSpanDays = validClosed.length > 0
24715
+ ? (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24)
24716
+ : 0;
24717
+ // tradesPerYear uses the RAW observed frequency — no clipping. Clipping would
24718
+ // silently understate Sharpe / Calmar / expectedYearlyReturns. Instead, if the
24719
+ // raw frequency exceeds MAX_TRADES_PER_YEAR we treat the sample as too clustered
24720
+ // for reliable annualization and surface every annualized metric as null.
24721
+ const rawTradesPerYear = returns.length >= MIN_SIGNALS_FOR_ANNUALIZATION$1 &&
24722
+ calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS$1
24723
+ ? (returns.length / calendarSpanDays) * 365
24724
+ : 0;
24725
+ const canAnnualize = rawTradesPerYear > 0 && rawTradesPerYear <= MAX_TRADES_PER_YEAR$1;
24726
+ const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
24727
+ // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
24728
+ // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
24729
+ // are too noisy to publish (high chance of spurious ±Sharpe).
24730
+ const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
24731
+ const stdDev = canComputeRatios
24732
+ ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
24733
+ : 0;
24734
+ // STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
24735
+ // returns producing spuriously astronomical sharpe.
24736
+ const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
24737
+ ? avgPnl / stdDev
24738
+ : null;
24739
+ // Annualize only when gate passes; otherwise null.
24740
+ const annualizedSharpeRatio = canAnnualize && sharpeRatio !== null
24741
+ ? sharpeRatio * Math.sqrt(tradesPerYear)
24742
+ : null;
24743
+ // Certainty Ratio: null (not zero) when there are no losing trades — a flawless
24744
+ // strategy has undefined Certainty Ratio, not "worst case zero". Computed on
24745
+ // validClosed for consistency with other ratios.
24746
+ // Gated below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as Sharpe/Sortino,
24747
+ // so the report doesn't surface certainty on a handful of trades while
24748
+ // withholding the rest.
24749
+ let certaintyRatio = null;
24750
+ if (canComputeRatios && totalClosed > 0) {
24751
+ const wins = validClosed.filter((e) => e.pnl > 0);
24752
+ const losses = validClosed.filter((e) => e.pnl < 0);
24518
24753
  const avgWin = wins.length > 0
24519
- ? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
24754
+ ? wins.reduce((sum, e) => sum + e.pnl, 0) / wins.length
24520
24755
  : 0;
24521
24756
  const avgLoss = losses.length > 0
24522
- ? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
24757
+ ? losses.reduce((sum, e) => sum + e.pnl, 0) / losses.length
24523
24758
  : 0;
24524
- certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
24525
- }
24526
- // Calculate Expected Yearly Returns
24527
- let expectedYearlyReturns = 0;
24528
- if (totalClosed > 0) {
24529
- const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
24530
- const avgDurationDays = avgDurationMin / (60 * 24);
24531
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
24532
- expectedYearlyReturns = avgPnl * tradesPerYear;
24533
- }
24534
- const avgPeakPnl = totalClosed > 0
24535
- ? closedEvents.reduce((sum, e) => sum + (e.peakPnl || 0), 0) / totalClosed
24536
- : 0;
24537
- const avgFallPnl = totalClosed > 0
24538
- ? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
24539
- : 0;
24540
- // Downside per signal: fallPnl captures the worst intra-trade dip (maxDrawdown.pnlPercentage)
24541
- const fallReturns = closedEvents.map((e) => e.fallPnl || 0);
24542
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
24543
- let sortinoRatio = 0;
24544
- if (totalClosed > 0) {
24545
- const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalClosed;
24546
- const fallDeviation = Math.sqrt(fallVariance);
24547
- sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
24548
- }
24549
- // Max absolute drawdown across all signals — denominator for Calmar and Recovery
24550
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
24551
- const calmarRatio = maxAbsFall > 0 ? expectedYearlyReturns / maxAbsFall : 0;
24552
- const recoveryFactor = maxAbsFall > 0 ? totalPnl / maxAbsFall : 0;
24759
+ // STDDEV_EPSILON guard on |avgLoss| protects against float-artifact
24760
+ // losses producing spurious astronomical certaintyRatio.
24761
+ certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
24762
+ ? avgWin / Math.abs(avgLoss)
24763
+ : null;
24764
+ }
24765
+ // Average only over signals that have the value — do not dilute the mean with zeros.
24766
+ // Use validClosed to keep all metric denominators consistent.
24767
+ const peakValues = validClosed
24768
+ .map((e) => e.peakPnl)
24769
+ .filter((v) => typeof v === "number");
24770
+ const fallValues = validClosed
24771
+ .map((e) => e.fallPnl)
24772
+ .filter((v) => typeof v === "number");
24773
+ const avgPeakPnl = peakValues.length > 0
24774
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
24775
+ : null;
24776
+ const avgFallPnl = fallValues.length > 0
24777
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
24778
+ : null;
24779
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
24780
+ // downsideDev = ( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
24781
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
24782
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
24783
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
24784
+ // strategies.
24785
+ const sortinoRatio = (() => {
24786
+ if (!canComputeRatios)
24787
+ return null;
24788
+ const negativeReturns = returns.filter((r) => r < 0);
24789
+ if (negativeReturns.length === 0)
24790
+ return null;
24791
+ const downsideVariance = negativeReturns.reduce((sum, r) => sum + r * r, 0) / returns.length;
24792
+ const downsideDeviation = Math.sqrt(downsideVariance);
24793
+ // Same epsilon guard as Sharpe — protects against float-artifact downsideDev.
24794
+ return downsideDeviation > STDDEV_EPSILON$1 ? avgPnl / downsideDeviation : null;
24795
+ })();
24796
+ // Equity-curve max drawdown via compounded equity (multiplicative). Returns are per-trade
24797
+ // on cost basis — compounding assumes equal capital allocation per trade ("as-if 100%").
24798
+ // If equity ≤ 0 (leveraged short with r < -100%) — account blown, fix DD at 100%.
24799
+ // Built from validClosed (newest-first), iterated reverse for chronological order.
24800
+ const chronologicalReturns = [];
24801
+ for (let i = validClosed.length - 1; i >= 0; i--) {
24802
+ chronologicalReturns.push(validClosed[i].pnl);
24803
+ }
24804
+ let equity = 1;
24805
+ let peak = 1;
24806
+ let equityMaxDrawdown = 0;
24807
+ let blown = false;
24808
+ for (const r of chronologicalReturns) {
24809
+ equity *= 1 + r / 100;
24810
+ if (equity <= 0) {
24811
+ equityMaxDrawdown = 100;
24812
+ blown = true;
24813
+ break;
24814
+ }
24815
+ if (equity > peak)
24816
+ peak = equity;
24817
+ const dd = (peak - equity) / peak * 100;
24818
+ if (dd > equityMaxDrawdown)
24819
+ equityMaxDrawdown = dd;
24820
+ }
24821
+ const equityFinal = blown ? 0 : equity;
24822
+ // Compounded yearly return via geometric mean of equity curve:
24823
+ // equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
24824
+ // If account is blown, full loss. If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS,
24825
+ // return null rather than showing the cap — capped numbers mislead users.
24826
+ const expectedYearlyReturns = canAnnualize
24827
+ ? blown
24828
+ ? -100
24829
+ : (() => {
24830
+ const raw = (Math.pow(equityFinal, tradesPerYear / returns.length) - 1) * 100;
24831
+ return Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS$1 ? null : raw;
24832
+ })()
24833
+ : null;
24834
+ // Calmar — cap |value| at MAX_CALMAR_RATIO to prevent explosion when DD is near zero.
24835
+ const calmarRatio = equityMaxDrawdown > 0 && expectedYearlyReturns !== null
24836
+ ? Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, expectedYearlyReturns / equityMaxDrawdown))
24837
+ : null;
24838
+ // Recovery Factor: numerator must be the compounded total return, not arithmetic totalPnl —
24839
+ // denominator is from the compounded equity curve, so mixing units inflates Recovery.
24840
+ // Null below MIN_SIGNALS_FOR_RATIOS — same sample-size gate as the other ratios,
24841
+ // so a 3-trade run doesn't surface a Recovery Factor while Sharpe/Calmar are N/A.
24842
+ // Null when account is blown.
24843
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both are compounded-profit/DD ratios
24844
+ // and explode the same way when DD is near zero.
24845
+ const recoveryFactor = !canComputeRatios || blown || equityMaxDrawdown <= 0
24846
+ ? null
24847
+ : Math.max(-MAX_CALMAR_RATIO$1, Math.min(MAX_CALMAR_RATIO$1, ((equityFinal - 1) * 100) / equityMaxDrawdown));
24553
24848
  return {
24554
24849
  eventList: this._eventList,
24555
24850
  totalEvents: this._eventList.length,
24556
24851
  totalClosed,
24557
24852
  winCount,
24558
24853
  lossCount,
24559
- winRate: isUnsafe$2(winRate) ? null : winRate,
24560
- avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
24561
- totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
24562
- stdDev: isUnsafe$2(stdDev) ? null : stdDev,
24563
- sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
24564
- annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
24565
- certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
24566
- expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
24567
- avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
24568
- avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
24569
- sortinoRatio: isUnsafe$2(sortinoRatio) ? null : sortinoRatio,
24570
- calmarRatio: isUnsafe$2(calmarRatio) ? null : calmarRatio,
24571
- recoveryFactor: isUnsafe$2(recoveryFactor) ? null : recoveryFactor,
24854
+ winRate: isUnsafe$3(winRate) ? null : winRate,
24855
+ avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
24856
+ totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
24857
+ stdDev: isUnsafe$3(stdDev) ? null : stdDev,
24858
+ sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
24859
+ annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
24860
+ certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
24861
+ expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
24862
+ avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
24863
+ avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
24864
+ sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
24865
+ calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
24866
+ recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
24572
24867
  };
24573
24868
  }
24574
24869
  /**
@@ -24616,18 +24911,20 @@ let ReportStorage$9 = class ReportStorage {
24616
24911
  `**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
24617
24912
  `**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
24618
24913
  `**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
24619
- `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better, theoretical)`}`,
24914
+ `**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
24620
24915
  `**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
24621
24916
  "",
24622
24917
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24623
24918
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24624
- `*Annualized Sharpe Ratio: theoretical maximum assuming continuous trading. Real-world value is lower due to idle periods.*`,
24625
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24919
+ `*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.*`,
24920
+ `*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".*`,
24626
24921
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
24627
- `*Expected Yearly Returns: theoretical maximum assuming all capital is deployed continuously with no idle time.*`,
24628
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
24629
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
24630
- `*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.*`,
24922
+ `*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.*`,
24923
+ `*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}.*`,
24924
+ `*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.*`,
24925
+ `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24926
+ `*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.*`,
24927
+ `*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.*`,
24631
24928
  ].join("\n");
24632
24929
  }
24633
24930
  /**
@@ -25006,7 +25303,9 @@ let ReportStorage$8 = class ReportStorage {
25006
25303
  */
25007
25304
  addOpenedEvent(data) {
25008
25305
  const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
25009
- const durationMin = Math.round(durationMs / 60000);
25306
+ // Keep fractional minutes rounding to whole minutes zeroed out sub-30s durations,
25307
+ // which dragged high-frequency averages towards zero.
25308
+ const durationMin = durationMs / 60000;
25010
25309
  const newEvent = {
25011
25310
  timestamp: data.signal.pendingAt,
25012
25311
  action: "opened",
@@ -25042,7 +25341,8 @@ let ReportStorage$8 = class ReportStorage {
25042
25341
  */
25043
25342
  addCancelledEvent(data) {
25044
25343
  const durationMs = data.closeTimestamp - data.signal.scheduledAt;
25045
- const durationMin = Math.round(durationMs / 60000);
25344
+ // Keep fractional minutes rounding to whole minutes zeroed out sub-30s durations.
25345
+ const durationMin = durationMs / 60000;
25046
25346
  const newEvent = {
25047
25347
  timestamp: data.closeTimestamp,
25048
25348
  action: "cancelled",
@@ -25098,19 +25398,33 @@ let ReportStorage$8 = class ReportStorage {
25098
25398
  const totalScheduled = scheduledEvents.length;
25099
25399
  const totalOpened = openedEvents.length;
25100
25400
  const totalCancelled = cancelledEvents.length;
25101
- // Calculate cancellation rate
25102
- const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
25103
- // Calculate activation rate
25104
- const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
25105
- // Calculate average wait time for cancelled signals
25106
- const avgWaitTime = totalCancelled > 0
25107
- ? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
25108
- totalCancelled
25401
+ // Rate denominators must include only scheduled events whose outcome (opened/cancelled)
25402
+ // is also in the buffer. Otherwise a sliding window of 250 entries can drop the
25403
+ // "scheduled" record before its outcome arrives, inflating rates above 100% or
25404
+ // causing one rate to fire without the other. Match by signalId.
25405
+ const scheduledIds = new Set(scheduledEvents.map((e) => e.signalId).filter((id) => typeof id === "string"));
25406
+ const openedFromScheduled = openedEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
25407
+ const cancelledFromScheduled = cancelledEvents.filter((e) => typeof e.signalId === "string" && scheduledIds.has(e.signalId));
25408
+ const resolvedScheduled = openedFromScheduled.length + cancelledFromScheduled.length;
25409
+ const cancellationRate = resolvedScheduled > 0
25410
+ ? (cancelledFromScheduled.length / resolvedScheduled) * 100
25411
+ : null;
25412
+ const activationRate = resolvedScheduled > 0
25413
+ ? (openedFromScheduled.length / resolvedScheduled) * 100
25414
+ : null;
25415
+ // Average durations — include only events with a numeric duration, do not dilute
25416
+ // the mean with zeros for missing values.
25417
+ const cancelledDurations = cancelledEvents
25418
+ .map((e) => e.duration)
25419
+ .filter((d) => typeof d === "number");
25420
+ const openedDurations = openedEvents
25421
+ .map((e) => e.duration)
25422
+ .filter((d) => typeof d === "number");
25423
+ const avgWaitTime = cancelledDurations.length > 0
25424
+ ? cancelledDurations.reduce((sum, d) => sum + d, 0) / cancelledDurations.length
25109
25425
  : null;
25110
- // Calculate average activation time for opened signals
25111
- const avgActivationTime = totalOpened > 0
25112
- ? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
25113
- totalOpened
25426
+ const avgActivationTime = openedDurations.length > 0
25427
+ ? openedDurations.reduce((sum, d) => sum + d, 0) / openedDurations.length
25114
25428
  : null;
25115
25429
  return {
25116
25430
  eventList: this._eventList,
@@ -25157,13 +25471,15 @@ let ReportStorage$8 = class ReportStorage {
25157
25471
  table,
25158
25472
  "",
25159
25473
  `**Total events:** ${stats.totalEvents}`,
25160
- `**Scheduled signals:** ${stats.totalScheduled}`,
25474
+ `**Scheduled signals (raw):** ${stats.totalScheduled}`,
25161
25475
  `**Opened signals:** ${stats.totalOpened}`,
25162
25476
  `**Cancelled signals:** ${stats.totalCancelled}`,
25163
25477
  `**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
25164
25478
  `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
25165
25479
  `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
25166
- `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
25480
+ `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
25481
+ "",
25482
+ `*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.*`
25167
25483
  ].join("\n");
25168
25484
  }
25169
25485
  /**
@@ -25468,13 +25784,37 @@ const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, ti
25468
25784
  return `${parts.join("_")}-${timestamp}.md`;
25469
25785
  };
25470
25786
  /**
25471
- * Calculates percentile value from sorted array.
25787
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
25788
+ */
25789
+ function isUnsafe$2(value) {
25790
+ if (typeof value !== "number") {
25791
+ return true;
25792
+ }
25793
+ if (isNaN(value)) {
25794
+ return true;
25795
+ }
25796
+ if (!isFinite(value)) {
25797
+ return true;
25798
+ }
25799
+ return false;
25800
+ }
25801
+ /**
25802
+ * Calculates percentile value from sorted array using linear interpolation
25803
+ * between adjacent ranks (equivalent to numpy.percentile with default linear method).
25804
+ * Falls back to nearest-rank for length 0/1.
25472
25805
  */
25473
25806
  function percentile(sortedArray, p) {
25474
25807
  if (sortedArray.length === 0)
25475
25808
  return 0;
25476
- const index = Math.ceil((sortedArray.length * p) / 100) - 1;
25477
- return sortedArray[Math.max(0, index)];
25809
+ if (sortedArray.length === 1)
25810
+ return sortedArray[0];
25811
+ const rank = (p / 100) * (sortedArray.length - 1);
25812
+ const lower = Math.floor(rank);
25813
+ const upper = Math.ceil(rank);
25814
+ if (lower === upper)
25815
+ return sortedArray[lower];
25816
+ const fraction = rank - lower;
25817
+ return sortedArray[lower] * (1 - fraction) + sortedArray[upper] * fraction;
25478
25818
  }
25479
25819
  /**
25480
25820
  * Storage class for accumulating performance metrics per strategy.
@@ -25530,10 +25870,12 @@ class PerformanceStorage {
25530
25870
  const durations = events.map((e) => e.duration).sort((a, b) => a - b);
25531
25871
  const totalDuration = durations.reduce((sum, d) => sum + d, 0);
25532
25872
  const avgDuration = totalDuration / durations.length;
25533
- // Calculate standard deviation
25534
- const variance = durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
25535
- durations.length;
25536
- const stdDev = Math.sqrt(variance);
25873
+ // Sample standard deviation (Bessel correction: divide by N-1, not N) — consistent
25874
+ // with Sharpe/Sortino calculations in Backtest/Live/Heat services.
25875
+ const stdDev = durations.length > 1
25876
+ ? Math.sqrt(durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
25877
+ (durations.length - 1))
25878
+ : 0;
25537
25879
  // Calculate wait times between events
25538
25880
  const waitTimes = [];
25539
25881
  for (let i = 0; i < events.length; i++) {
@@ -25606,9 +25948,13 @@ class PerformanceStorage {
25606
25948
  const rows = await Promise.all(sortedMetrics.map(async (metric, index) => Promise.all(visibleColumns.map((col) => col.format(metric, index)))));
25607
25949
  const tableData = [header, separator, ...rows];
25608
25950
  const summaryTable = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
25609
- // Calculate percentage of total time for each metric
25951
+ // Calculate percentage of total time for each metric. Guard against zero total
25952
+ // duration (all-instant operations) to avoid NaN% in the rendered report.
25610
25953
  const percentages = sortedMetrics.map((metric) => {
25611
- const pct = (metric.totalDuration / stats.totalDuration) * 100;
25954
+ const pctRaw = stats.totalDuration > 0
25955
+ ? (metric.totalDuration / stats.totalDuration) * 100
25956
+ : 0;
25957
+ const pct = isUnsafe$2(pctRaw) ? 0 : pctRaw;
25612
25958
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
25613
25959
  });
25614
25960
  return [
@@ -26377,6 +26723,25 @@ function isUnsafe(value) {
26377
26723
  }
26378
26724
  return false;
26379
26725
  }
26726
+ /** Minimum closed signals required to annualize Sharpe / yearly returns / Calmar. */
26727
+ const MIN_SIGNALS_FOR_ANNUALIZATION = 10;
26728
+ /** Minimum signals required for ANY ratio metric (Sharpe / Sortino / stdDev). Below this,
26729
+ * sample size is too small to estimate variance meaningfully. */
26730
+ const MIN_SIGNALS_FOR_RATIOS = 10;
26731
+ /** Minimum calendar span (days) for trade-frequency extrapolation. */
26732
+ const MIN_CALENDAR_SPAN_DAYS = 14;
26733
+ /** Hard cap on tradesPerYear — prevents absurd extrapolation from short windows / clustered trades. */
26734
+ const MAX_TRADES_PER_YEAR = 365;
26735
+ /** Hard cap on |expectedYearlyReturns| percent. Compound interest on high avgPnl × frequency
26736
+ * blows up to mathematically correct but business-unrealistic values. ±100% = 2x equity —
26737
+ * anything above this we suspect is a noisy estimate, not a genuine edge. Above the cap → null. */
26738
+ const MAX_EXPECTED_YEARLY_RETURNS = 100;
26739
+ /** Hard cap on |calmarRatio|. Prevents explosion when equityMaxDrawdown is near zero. */
26740
+ const MAX_CALMAR_RATIO = 1000;
26741
+ /** Minimum stdDev required for Sharpe/Sortino. Identical-returns series produce
26742
+ * float-artifact stdDev (~1e-17) that's > 0 but spuriously inflates sharpe to
26743
+ * astronomical magnitudes (avgPnl / epsilon). */
26744
+ const STDDEV_EPSILON = 1e-9;
26380
26745
  /**
26381
26746
  * Storage class for accumulating closed signals per strategy and generating heatmap.
26382
26747
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -26418,7 +26783,7 @@ class HeatmapStorage {
26418
26783
  * - **totalPnl** — sum of `pnlPercentage` across all signals
26419
26784
  * - **avgPnl** — arithmetic mean of `pnlPercentage`
26420
26785
  * - **stdDev** — population standard deviation of `pnlPercentage`
26421
- * - **sharpeRatio** — `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
26786
+ * - **sharpeRatio** — per-trade Sharpe: `avgPnl / stdDev`; requires ≥ 2 signals and `stdDev > 0`
26422
26787
  * - **maxDrawdown** — largest cumulative loss streak (absolute value of peak negative equity)
26423
26788
  * - **profitFactor** — `sumWins / |sumLosses|`; requires at least one win and one loss
26424
26789
  * - **avgWin / avgLoss** — mean of positive / negative trades respectively
@@ -26434,10 +26799,12 @@ class HeatmapStorage {
26434
26799
  const totalTrades = signals.length;
26435
26800
  const winCount = signals.filter((s) => s.pnl.pnlPercentage > 0).length;
26436
26801
  const lossCount = signals.filter((s) => s.pnl.pnlPercentage < 0).length;
26437
- // Calculate win rate
26802
+ // Win rate excludes break-even trades from both numerator and denominator —
26803
+ // they are neither wins nor losses.
26438
26804
  let winRate = null;
26439
- if (totalTrades > 0) {
26440
- winRate = (winCount / totalTrades) * 100;
26805
+ const decisiveTrades = winCount + lossCount;
26806
+ if (decisiveTrades > 0) {
26807
+ winRate = (winCount / decisiveTrades) * 100;
26441
26808
  }
26442
26809
  // Calculate total PNL
26443
26810
  let totalPnl = null;
@@ -26449,36 +26816,47 @@ class HeatmapStorage {
26449
26816
  if (signals.length > 0) {
26450
26817
  avgPnl = totalPnl / signals.length;
26451
26818
  }
26452
- // Calculate standard deviation
26819
+ // Sample standard deviation (Bessel correction: divide by N-1, not N).
26820
+ // Per-symbol ratios are gated by MIN_SIGNALS_FOR_RATIOS — variance estimates from
26821
+ // tiny samples are too noisy to publish.
26822
+ const canComputeRatios = signals.length >= MIN_SIGNALS_FOR_RATIOS;
26453
26823
  let stdDev = null;
26454
- if (signals.length > 1 && avgPnl !== null) {
26455
- const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / signals.length;
26824
+ if (canComputeRatios && avgPnl !== null) {
26825
+ const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / (signals.length - 1);
26456
26826
  stdDev = Math.sqrt(variance);
26457
26827
  }
26458
- // Calculate Sharpe Ratio
26828
+ // Per-trade Sharpe Ratio
26459
26829
  let sharpeRatio = null;
26460
- if (avgPnl !== null && stdDev !== null && stdDev !== 0) {
26830
+ // STDDEV_EPSILON guard protects against float-artifact stdDev producing
26831
+ // spuriously astronomical sharpe on identical-returns symbols.
26832
+ if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
26461
26833
  sharpeRatio = avgPnl / stdDev;
26462
26834
  }
26463
- // Calculate Maximum Drawdown
26835
+ // Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
26836
+ // Signals are stored newest-first (unshift in addSignal), so iterate in reverse.
26837
+ // If equity ≤ 0 — account blown, fix DD at 100%. equityFinal feeds expectedYearlyReturns.
26464
26838
  let maxDrawdown = null;
26839
+ let equityFinal = 1;
26840
+ let blown = false;
26465
26841
  if (signals.length > 0) {
26466
- let peak = 0;
26467
- let currentDrawdown = 0;
26842
+ let equity = 1;
26843
+ let peak = 1;
26468
26844
  let maxDD = 0;
26469
- for (const signal of signals) {
26470
- peak += signal.pnl.pnlPercentage;
26471
- if (peak > 0) {
26472
- currentDrawdown = 0;
26473
- }
26474
- else {
26475
- currentDrawdown = Math.abs(peak);
26476
- if (currentDrawdown > maxDD) {
26477
- maxDD = currentDrawdown;
26478
- }
26845
+ for (let i = signals.length - 1; i >= 0; i--) {
26846
+ equity *= 1 + signals[i].pnl.pnlPercentage / 100;
26847
+ if (equity <= 0) {
26848
+ maxDD = 100;
26849
+ blown = true;
26850
+ break;
26479
26851
  }
26852
+ if (equity > peak)
26853
+ peak = equity;
26854
+ const dd = (peak - equity) / peak * 100;
26855
+ if (dd > maxDD)
26856
+ maxDD = dd;
26480
26857
  }
26481
26858
  maxDrawdown = maxDD;
26859
+ equityFinal = blown ? 0 : equity;
26482
26860
  }
26483
26861
  // Calculate Profit Factor
26484
26862
  let profitFactor = null;
@@ -26489,7 +26867,9 @@ class HeatmapStorage {
26489
26867
  const sumLosses = Math.abs(signals
26490
26868
  .filter((s) => s.pnl.pnlPercentage < 0)
26491
26869
  .reduce((acc, s) => acc + s.pnl.pnlPercentage, 0));
26492
- if (sumLosses > 0) {
26870
+ // STDDEV_EPSILON guard — float-artifact losses (≈1e-15) would otherwise
26871
+ // produce spurious astronomical profitFactor (≈1e14).
26872
+ if (sumLosses > STDDEV_EPSILON) {
26493
26873
  profitFactor = sumWins / sumLosses;
26494
26874
  }
26495
26875
  }
@@ -26529,45 +26909,110 @@ class HeatmapStorage {
26529
26909
  }
26530
26910
  }
26531
26911
  }
26532
- // Calculate Expectancy
26912
+ // Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
26533
26913
  let expectancy = null;
26534
- if (winRate !== null && avgWin !== null && avgLoss !== null) {
26535
- const lossRate = 100 - winRate;
26536
- expectancy = (winRate / 100) * avgWin + (lossRate / 100) * avgLoss;
26914
+ if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
26915
+ const winProb = winCount / totalTrades;
26916
+ const lossProb = lossCount / totalTrades;
26917
+ expectancy = winProb * avgWin + lossProb * avgLoss;
26918
+ }
26919
+ else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
26920
+ // No losing trades — expectancy is just average win frequency × avgWin
26921
+ expectancy = (winCount / totalTrades) * avgWin;
26922
+ }
26923
+ else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
26924
+ expectancy = (lossCount / totalTrades) * avgLoss;
26537
26925
  }
26538
- // Calculate average peak and fall PNL
26926
+ // Average only over signals that have the value — do not dilute the mean with zeros.
26539
26927
  let avgPeakPnl = null;
26540
26928
  let avgFallPnl = null;
26541
26929
  if (signals.length > 0) {
26542
- avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / signals.length;
26543
- avgFallPnl = signals.reduce((acc, s) => acc + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / signals.length;
26930
+ const peakValues = signals
26931
+ .map((s) => s.signal.peakProfit?.pnlPercentage)
26932
+ .filter((v) => typeof v === "number");
26933
+ const fallValues = signals
26934
+ .map((s) => s.signal.maxDrawdown?.pnlPercentage)
26935
+ .filter((v) => typeof v === "number");
26936
+ avgPeakPnl = peakValues.length > 0
26937
+ ? peakValues.reduce((sum, v) => sum + v, 0) / peakValues.length
26938
+ : null;
26939
+ avgFallPnl = fallValues.length > 0
26940
+ ? fallValues.reduce((sum, v) => sum + v, 0) / fallValues.length
26941
+ : null;
26544
26942
  }
26545
- // Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
26546
- const fallReturns = signals.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
26547
- // Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
26943
+ // Sortino (canonical, Sortino 1991): (avgPnl - MAR) / downside deviation, where
26944
+ // downsideDev = ( Σ min(0, r - MAR)² / N_total ). We use MAR = 0 (risk-free target),
26945
+ // so the numerator reduces to avgPnl and the squared term to r² for r < 0.
26946
+ // Dividing by N_total (not N_negative) properly penalises strategies with frequent
26947
+ // losses; the "modified" form (N_negative) hides frequency risk in catastrophic-tail
26948
+ // strategies.
26548
26949
  let sortinoRatio = null;
26549
- if (signals.length > 0 && avgPnl !== null) {
26550
- const fallVariance = fallReturns.reduce((acc, r) => acc + Math.pow(r, 2), 0) / signals.length;
26551
- const fallDeviation = Math.sqrt(fallVariance);
26552
- if (fallDeviation > 0) {
26553
- sortinoRatio = avgPnl / fallDeviation;
26554
- }
26555
- }
26556
- // Max absolute drawdown across all signals denominator for Calmar and Recovery
26557
- const maxAbsFall = fallReturns.reduce((max, r) => Math.max(max, Math.abs(r)), 0);
26558
- // Expected yearly returns — needed for Calmar
26559
- let expectedYearlyReturns = 0;
26560
- if (signals.length > 0 && avgPnl !== null) {
26561
- const avgDurationMs = signals.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / signals.length;
26562
- const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
26563
- const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
26564
- expectedYearlyReturns = avgPnl * tradesPerYear;
26950
+ if (canComputeRatios && avgPnl !== null) {
26951
+ const negativeReturns = signals
26952
+ .map((s) => s.pnl.pnlPercentage)
26953
+ .filter((r) => r < 0);
26954
+ if (negativeReturns.length > 0) {
26955
+ const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / signals.length;
26956
+ const downsideDeviation = Math.sqrt(downsideVariance);
26957
+ // Same epsilon guard as Sharpeprotects against float-artifact downsideDev.
26958
+ if (downsideDeviation > STDDEV_EPSILON) {
26959
+ sortinoRatio = avgPnl / downsideDeviation;
26960
+ }
26961
+ }
26565
26962
  }
26963
+ // Expected yearly returns via geometric mean of equity curve.
26964
+ // equityFinal^(tradesPerYear / N) - 1 — accounts for volatility drag.
26965
+ // Gated by sample size and calendar span; if account blown → full loss.
26966
+ let expectedYearlyReturns = null;
26967
+ let tradesPerYear = null;
26968
+ if (signals.length >= MIN_SIGNALS_FOR_ANNUALIZATION) {
26969
+ let firstPendingAt = Infinity;
26970
+ let lastCloseAt = -Infinity;
26971
+ for (const s of signals) {
26972
+ if (s.signal.pendingAt < firstPendingAt)
26973
+ firstPendingAt = s.signal.pendingAt;
26974
+ if (s.closeTimestamp > lastCloseAt)
26975
+ lastCloseAt = s.closeTimestamp;
26976
+ }
26977
+ const calendarSpanDays = (lastCloseAt - firstPendingAt) / (1000 * 60 * 60 * 24);
26978
+ if (calendarSpanDays >= MIN_CALENDAR_SPAN_DAYS) {
26979
+ // tradesPerYear uses RAW observed frequency — no clipping. If the raw value
26980
+ // exceeds MAX_TRADES_PER_YEAR the sample is too clustered for reliable
26981
+ // annualization, and we leave the annualized metric null instead of silently
26982
+ // understating it with a clipped frequency.
26983
+ const rawTradesPerYear = (signals.length / calendarSpanDays) * 365;
26984
+ if (rawTradesPerYear <= MAX_TRADES_PER_YEAR) {
26985
+ tradesPerYear = rawTradesPerYear;
26986
+ if (blown) {
26987
+ expectedYearlyReturns = -100;
26988
+ }
26989
+ else {
26990
+ // If raw value exceeds MAX_EXPECTED_YEARLY_RETURNS, leave null rather than
26991
+ // show the cap — capped numbers mislead users into trusting them.
26992
+ const raw = (Math.pow(equityFinal, tradesPerYear / signals.length) - 1) * 100;
26993
+ expectedYearlyReturns = Math.abs(raw) > MAX_EXPECTED_YEARLY_RETURNS ? null : raw;
26994
+ }
26995
+ }
26996
+ }
26997
+ }
26998
+ // Calmar = annualized return / equity-curve max drawdown, capped at ±MAX_CALMAR_RATIO.
26999
+ // Recovery Factor uses the compounded total return (equityFinal-1)*100, not arithmetic
27000
+ // totalPnl — denominator is compounded so numerator must match. Null when account blown.
26566
27001
  let calmarRatio = null;
26567
27002
  let recoveryFactor = null;
26568
- if (maxAbsFall > 0 && totalPnl !== null) {
26569
- calmarRatio = expectedYearlyReturns / maxAbsFall;
26570
- recoveryFactor = totalPnl / maxAbsFall;
27003
+ if (maxDrawdown !== null && maxDrawdown > 0) {
27004
+ if (expectedYearlyReturns !== null) {
27005
+ const raw = expectedYearlyReturns / maxDrawdown;
27006
+ calmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, raw));
27007
+ }
27008
+ if (!blown && canComputeRatios) {
27009
+ // Gated below MIN_SIGNALS_FOR_RATIOS like Sharpe — a Recovery Factor on
27010
+ // a handful of trades is statistically meaningless, so don't surface it
27011
+ // per-symbol while Sharpe is N/A.
27012
+ // Same MAX_CALMAR_RATIO clamp as Calmar — both compounded-profit/DD ratios.
27013
+ const rawRec = ((equityFinal - 1) * 100) / maxDrawdown;
27014
+ recoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
27015
+ }
26571
27016
  }
26572
27017
  // Apply safe math checks
26573
27018
  if (isUnsafe(winRate))
@@ -26632,12 +27077,18 @@ class HeatmapStorage {
26632
27077
  * 2. Sorts symbols by `sharpeRatio` descending — best performers first,
26633
27078
  * symbols with `null` sharpeRatio placed at the end.
26634
27079
  * 3. Computes portfolio-wide aggregates:
26635
- * - `portfolioTotalPnl` — sum of all per-symbol `totalPnl` values (treats `null` as 0)
26636
- * - `portfolioTotalTrades` sum of all per-symbol `totalTrades`
26637
- * - `portfolioSharpeRatio` trade-count-weighted average of per-symbol sharpe ratios
26638
- *
26639
- * @returns Promise resolving to `HeatmapStatisticsModel` with per-symbol rows and
26640
- * portfolio-wide `portfolioTotalPnl`, `portfolioSharpeRatio`, `portfolioTotalTrades`
27080
+ * - `portfolioTotalPnl` — sum of per-symbol `totalPnl` values, skipping `null` entries
27081
+ * (so a symbol with no data does not silently contribute 0). If every symbol's
27082
+ * `totalPnl` is null, the portfolio value is null.
27083
+ * - `portfolioTotalTrades` — sum of per-symbol `totalTrades`
27084
+ * - `portfolioSharpeRatio` POOLED Sharpe over all trades across symbols (sample
27085
+ * stddev, N-1). NOT a Markowitz portfolio Sharpe — ignores cross-symbol
27086
+ * correlations and capital allocation. Rendered as "Pooled Sharpe" in the report.
27087
+ * Gated by `MIN_SIGNALS_FOR_RATIOS` on the pooled count.
27088
+ * - `portfolioAvgPeakPnl` / `portfolioAvgFallPnl` — trade-count-weighted means
27089
+ * over symbols that have non-null values.
27090
+ *
27091
+ * @returns Promise resolving to `HeatmapStatisticsModel`
26641
27092
  */
26642
27093
  async getData() {
26643
27094
  const symbols = [];
@@ -26656,31 +27107,53 @@ class HeatmapStorage {
26656
27107
  return -1;
26657
27108
  return b.sharpeRatio - a.sharpeRatio;
26658
27109
  });
26659
- // Calculate portfolio-wide metrics
27110
+ // Portfolio totals — sum only over symbols with non-null totalPnl. `s.totalPnl || 0`
27111
+ // would silently treat a missing value as zero and hide that some symbols had no data.
26660
27112
  const totalSymbols = symbols.length;
26661
27113
  let portfolioTotalPnl = null;
26662
27114
  let portfolioTotalTrades = 0;
26663
27115
  if (symbols.length > 0) {
26664
- portfolioTotalPnl = symbols.reduce((acc, s) => acc + (s.totalPnl || 0), 0);
27116
+ const validTotalPnls = symbols.filter((s) => s.totalPnl !== null);
27117
+ portfolioTotalPnl = validTotalPnls.length > 0
27118
+ ? validTotalPnls.reduce((acc, s) => acc + s.totalPnl, 0)
27119
+ : null;
26665
27120
  portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
26666
27121
  }
26667
- // Calculate portfolio Sharpe Ratio (weighted by number of trades)
27122
+ // Pooled Sharpe over all returns across symbols. NOTE: this is NOT a Markowitz
27123
+ // portfolio Sharpe — it ignores cross-symbol correlations and treats trades as a
27124
+ // single pooled sample. Gated by MIN_SIGNALS_FOR_RATIOS so a 2-trade pool cannot
27125
+ // produce a noisy ±Sharpe.
26668
27126
  let portfolioSharpeRatio = null;
26669
- const validSharpes = symbols.filter((s) => s.sharpeRatio !== null);
26670
- if (validSharpes.length > 0 && portfolioTotalTrades > 0) {
26671
- const weightedSum = validSharpes.reduce((acc, s) => acc + s.sharpeRatio * s.totalTrades, 0);
26672
- portfolioSharpeRatio = weightedSum / portfolioTotalTrades;
27127
+ const allReturns = [];
27128
+ for (const signals of this.symbolData.values()) {
27129
+ for (const s of signals) {
27130
+ allReturns.push(s.pnl.pnlPercentage);
27131
+ }
27132
+ }
27133
+ if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
27134
+ const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
27135
+ const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
27136
+ (allReturns.length - 1);
27137
+ const portfolioStdDev = Math.sqrt(portfolioVariance);
27138
+ // STDDEV_EPSILON guard — same protection as per-symbol Sharpe.
27139
+ if (portfolioStdDev > STDDEV_EPSILON) {
27140
+ portfolioSharpeRatio = portfolioAvg / portfolioStdDev;
27141
+ }
26673
27142
  }
26674
- // Calculate portfolio-wide weighted average peak/fall PNL
27143
+ // Portfolio-wide weighted average peak/fall PNL. Denominator must include only
27144
+ // symbols that contributed a value — otherwise trade-count-weighted mean is diluted
27145
+ // by symbols without the metric.
26675
27146
  let portfolioAvgPeakPnl = null;
26676
27147
  let portfolioAvgFallPnl = null;
26677
27148
  const validPeak = symbols.filter((s) => s.avgPeakPnl !== null);
26678
27149
  const validFall = symbols.filter((s) => s.avgFallPnl !== null);
26679
- if (validPeak.length > 0 && portfolioTotalTrades > 0) {
26680
- portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / portfolioTotalTrades;
27150
+ const peakTradesTotal = validPeak.reduce((acc, s) => acc + s.totalTrades, 0);
27151
+ const fallTradesTotal = validFall.reduce((acc, s) => acc + s.totalTrades, 0);
27152
+ if (validPeak.length > 0 && peakTradesTotal > 0) {
27153
+ portfolioAvgPeakPnl = validPeak.reduce((acc, s) => acc + s.avgPeakPnl * s.totalTrades, 0) / peakTradesTotal;
26681
27154
  }
26682
- if (validFall.length > 0 && portfolioTotalTrades > 0) {
26683
- portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / portfolioTotalTrades;
27155
+ if (validFall.length > 0 && fallTradesTotal > 0) {
27156
+ portfolioAvgFallPnl = validFall.reduce((acc, s) => acc + s.avgFallPnl * s.totalTrades, 0) / fallTradesTotal;
26684
27157
  }
26685
27158
  // Apply safe math
26686
27159
  if (isUnsafe(portfolioTotalPnl))
@@ -26708,7 +27181,7 @@ class HeatmapStorage {
26708
27181
  * ```
26709
27182
  * # Portfolio Heatmap: {strategyName}
26710
27183
  *
26711
- * **Total Symbols:** N | **Portfolio PNL:** X% | **Portfolio Sharpe:** Y | **Total Trades:** Z
27184
+ * **Total Symbols:** N | **Portfolio PNL:** X% | **Pooled Sharpe:** Y | **Total Trades:** Z
26712
27185
  *
26713
27186
  * | col1 | col2 | ... |
26714
27187
  * | --- | --- | ... |
@@ -26747,18 +27220,21 @@ class HeatmapStorage {
26747
27220
  return [
26748
27221
  `# Portfolio Heatmap: ${strategyName}`,
26749
27222
  "",
26750
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
27223
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
26751
27224
  "",
26752
27225
  table,
26753
27226
  "",
26754
27227
  `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
27228
+ `*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.*`,
26755
27229
  `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
26756
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
27230
+ `*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".*`,
26757
27231
  `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
26758
27232
  `*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
26759
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Based on theoretical yearly returns.*`,
26760
- `*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good.*`,
26761
- `*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.*`,
27233
+ `*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}.*`,
27234
+ `*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.*`,
27235
+ `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
27236
+ `*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.*`,
27237
+ `*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.*`,
26762
27238
  ].join("\n");
26763
27239
  }
26764
27240
  /**
@@ -26953,7 +27429,7 @@ class HeatMarkdownService {
26953
27429
  * console.log(markdown);
26954
27430
  * // # Portfolio Heatmap: my-strategy
26955
27431
  * //
26956
- * // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
27432
+ * // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Pooled Sharpe:** 1.85 | **Total Trades:** 120
26957
27433
  * //
26958
27434
  * // | Symbol | Total PNL | Sharpe | Max DD | Trades |
26959
27435
  * // | --- | --- | --- | --- | --- |
@@ -53948,7 +54424,7 @@ const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
53948
54424
  const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
53949
54425
  const REPORT_UTILS_METHOD_NAME_CLEAR = "ReportUtils.clear";
53950
54426
  /** Logger service injected as DI singleton */
53951
- const LOGGER_SERVICE$2 = new LoggerService();
54427
+ const LOGGER_SERVICE$3 = new LoggerService();
53952
54428
  /**
53953
54429
  * Default configuration that enables all report services.
53954
54430
  * Used when no specific configuration is provided to enable().
@@ -54005,7 +54481,7 @@ class ReportUtils {
54005
54481
  * @returns Cleanup function that unsubscribes from all enabled services
54006
54482
  */
54007
54483
  this.enable = functoolsKit.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) => {
54008
- LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
54484
+ LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
54009
54485
  backtest: bt,
54010
54486
  breakeven,
54011
54487
  heat,
@@ -54100,7 +54576,7 @@ class ReportUtils {
54100
54576
  * ```
54101
54577
  */
54102
54578
  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) => {
54103
- LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
54579
+ LOGGER_SERVICE$3.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
54104
54580
  backtest: bt,
54105
54581
  breakeven,
54106
54582
  heat,
@@ -54180,7 +54656,7 @@ class ReportAdapter extends ReportUtils {
54180
54656
  * @param Ctor - Constructor for report storage adapter
54181
54657
  */
54182
54658
  useReportAdapter(Ctor) {
54183
- LOGGER_SERVICE$2.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
54659
+ LOGGER_SERVICE$3.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
54184
54660
  ReportWriter.useReportAdapter(Ctor);
54185
54661
  }
54186
54662
  /**
@@ -54189,7 +54665,7 @@ class ReportAdapter extends ReportUtils {
54189
54665
  * so new storage instances are created with the updated base path.
54190
54666
  */
54191
54667
  clear() {
54192
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_CLEAR);
54668
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_CLEAR);
54193
54669
  ReportWriter.clear();
54194
54670
  }
54195
54671
  /**
@@ -54197,7 +54673,7 @@ class ReportAdapter extends ReportUtils {
54197
54673
  * All future report writes will be no-ops.
54198
54674
  */
54199
54675
  useDummy() {
54200
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
54676
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
54201
54677
  ReportWriter.useDummy();
54202
54678
  }
54203
54679
  /**
@@ -54205,7 +54681,7 @@ class ReportAdapter extends ReportUtils {
54205
54681
  * All future report writes will use JSONL storage.
54206
54682
  */
54207
54683
  useJsonl() {
54208
- LOGGER_SERVICE$2.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
54684
+ LOGGER_SERVICE$3.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
54209
54685
  ReportWriter.useJsonl();
54210
54686
  }
54211
54687
  }
@@ -54223,7 +54699,7 @@ const MARKDOWN_METHOD_NAME_USE_JSONL = "MarkdownAdapter.useJsonl";
54223
54699
  const MARKDOWN_METHOD_NAME_USE_DUMMY = "MarkdownAdapter.useDummy";
54224
54700
  const MARKDOWN_METHOD_NAME_CLEAR = "MarkdownAdapter.clear";
54225
54701
  /** Logger service injected as DI singleton */
54226
- const LOGGER_SERVICE$1 = new LoggerService();
54702
+ const LOGGER_SERVICE$2 = new LoggerService();
54227
54703
  /**
54228
54704
  * Default configuration that enables all markdown services.
54229
54705
  * Used when no specific configuration is provided to `enable()`.
@@ -54280,7 +54756,7 @@ class MarkdownUtils {
54280
54756
  * @returns Cleanup function that unsubscribes from all enabled services
54281
54757
  */
54282
54758
  this.enable = functoolsKit.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) => {
54283
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_ENABLE, {
54759
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_ENABLE, {
54284
54760
  backtest: bt,
54285
54761
  breakeven,
54286
54762
  heat,
@@ -54377,7 +54853,7 @@ class MarkdownUtils {
54377
54853
  * ```
54378
54854
  */
54379
54855
  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) => {
54380
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_DISABLE, {
54856
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_DISABLE, {
54381
54857
  backtest: bt,
54382
54858
  breakeven,
54383
54859
  heat,
@@ -54463,7 +54939,7 @@ class MarkdownUtils {
54463
54939
  * @param config.max_drawdown - Clear max drawdown report data
54464
54940
  */
54465
54941
  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) => {
54466
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_CLEAR, {
54942
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_CLEAR, {
54467
54943
  backtest: bt,
54468
54944
  breakeven,
54469
54945
  heat,
@@ -54538,7 +55014,7 @@ class MarkdownAdapter extends MarkdownUtils {
54538
55014
  * @param Ctor - Constructor for markdown storage adapter
54539
55015
  */
54540
55016
  useMarkdownAdapter(Ctor) {
54541
- LOGGER_SERVICE$1.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
55017
+ LOGGER_SERVICE$2.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
54542
55018
  return MarkdownWriter.useMarkdownAdapter(Ctor);
54543
55019
  }
54544
55020
  /**
@@ -54547,7 +55023,7 @@ class MarkdownAdapter extends MarkdownUtils {
54547
55023
  * Each dump creates a separate .md file.
54548
55024
  */
54549
55025
  useMd() {
54550
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_MD);
55026
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_MD);
54551
55027
  MarkdownWriter.useMd();
54552
55028
  }
54553
55029
  /**
@@ -54556,7 +55032,7 @@ class MarkdownAdapter extends MarkdownUtils {
54556
55032
  * All dumps append to a single .jsonl file per markdown type.
54557
55033
  */
54558
55034
  useJsonl() {
54559
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
55035
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_JSONL);
54560
55036
  MarkdownWriter.useJsonl();
54561
55037
  }
54562
55038
  /**
@@ -54564,7 +55040,7 @@ class MarkdownAdapter extends MarkdownUtils {
54564
55040
  * All future markdown writes will be no-ops.
54565
55041
  */
54566
55042
  useDummy() {
54567
- LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
55043
+ LOGGER_SERVICE$2.debug(MARKDOWN_METHOD_NAME_USE_DUMMY);
54568
55044
  MarkdownWriter.useDummy();
54569
55045
  }
54570
55046
  }
@@ -63253,6 +63729,503 @@ class IntervalUtils {
63253
63729
  */
63254
63730
  const Interval = new IntervalUtils();
63255
63731
 
63732
+ const CRON_METHOD_NAME_REGISTER = "CronUtils.register";
63733
+ const CRON_METHOD_NAME_UNREGISTER = "CronUtils.unregister";
63734
+ const CRON_METHOD_NAME_CLEAR = "CronUtils.clear";
63735
+ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
63736
+ const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
63737
+ const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
63738
+ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
63739
+ /**
63740
+ * Local logger instance.
63741
+ *
63742
+ * Created directly rather than resolved from the DI container so that
63743
+ * `CronUtils` has no compile-time dependency on the rest of the framework
63744
+ * being bootstrapped — `Cron` can be imported and used in isolation.
63745
+ */
63746
+ const LOGGER_SERVICE$1 = new LoggerService();
63747
+ /**
63748
+ * Utility class for registering periodic tasks that fire on candle-interval
63749
+ * boundaries of the virtual time produced by parallel backtests.
63750
+ *
63751
+ * Exported as singleton instance `Cron` for convenient usage.
63752
+ *
63753
+ * Key property — **singleshot coordination across parallel backtests**:
63754
+ * when several `Backtest.background(symbol, ...)` runs hit the same aligned
63755
+ * boundary concurrently, the handler is invoked exactly once. Every parallel
63756
+ * `tick` for that boundary awaits the same in-flight promise and is released
63757
+ * together when the promise settles. After settlement the slot is cleared and
63758
+ * the next boundary produces a fresh promise.
63759
+ *
63760
+ * Typical wiring:
63761
+ *
63762
+ * @example
63763
+ * ```typescript
63764
+ * import { Cron, Backtest } from "backtest-kit";
63765
+ *
63766
+ * Cron.register({
63767
+ * name: "tg-signal-parser",
63768
+ * interval: "1h",
63769
+ * handler: async (symbol, when, backtest) => {
63770
+ * await parseTelegramSignalsToMongo(when);
63771
+ * },
63772
+ * });
63773
+ *
63774
+ * // Subscribe Cron to the engine's lifecycle subjects (beforeStart,
63775
+ * // idlePing, activePing, schedulePing) once at startup. After this every
63776
+ * // strategy tick is forwarded into Cron automatically.
63777
+ * Cron.enable();
63778
+ *
63779
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "TRXUSDT"]) {
63780
+ * Backtest.background(symbol, { strategyName, exchangeName, frameName });
63781
+ * }
63782
+ *
63783
+ * // On shutdown:
63784
+ * // Cron.disable();
63785
+ * ```
63786
+ */
63787
+ class CronUtils {
63788
+ constructor() {
63789
+ /**
63790
+ * Registered entries by `name`.
63791
+ *
63792
+ * Each record carries a monotonically increasing `generation` counter that
63793
+ * is bumped on every `register(entry)` call for the same name. The
63794
+ * generation participates in `firedKey` so writes from a still-in-flight
63795
+ * handler of a previous incarnation cannot poison `_firedOnce` for the
63796
+ * current incarnation — their key has a different generation suffix and
63797
+ * is simply ignored on lookup.
63798
+ */
63799
+ this._entries = new Map();
63800
+ /** Monotonic counter used to mint new entry generations on `register`. */
63801
+ this._generationCounter = 0;
63802
+ /**
63803
+ * In-flight handler slots.
63804
+ *
63805
+ * Slot key shape (always includes the generation suffix `:g${generation}`;
63806
+ * the `:${symbol}` scope is present only in fan-out mode):
63807
+ * - Periodic global: `${name}:${alignedMs}:g${generation}`.
63808
+ * - Periodic fan-out: `${name}:${alignedMs}:${symbol}:g${generation}`.
63809
+ * - Fire-once global: `${name}:once:g${generation}`.
63810
+ * - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
63811
+ *
63812
+ * Value is the shared in-flight handler promise. Every parallel `tick` for
63813
+ * the same slot key awaits this exact promise (mutex semantics) and is
63814
+ * released together when it settles. `_inFlight` is owned exclusively by
63815
+ * `_runEntry` — `clear()` does **not** touch it, so the singleshot promise
63816
+ * survives concurrent `clear` calls and continues to coordinate parallel
63817
+ * ticks until it settles.
63818
+ */
63819
+ this._inFlight = new Map();
63820
+ /**
63821
+ * Keys of fire-once entries whose handler has already settled successfully.
63822
+ *
63823
+ * Key shape (always includes the entry generation suffix `:g${generation}`):
63824
+ * - Global fire-once: `${name}:g${generation}`.
63825
+ * - Fan-out fire-once: `${name}:${symbol}:g${generation}` — one entry per
63826
+ * whitelisted symbol.
63827
+ *
63828
+ * The generation suffix isolates incarnations of the same `name`: writes
63829
+ * landing from a still-in-flight handler of a previous `register()` carry
63830
+ * the old generation and are never matched by the new entry's lookup.
63831
+ * Stale entries are pruned by `_clearFiredOnceFor` on `register`/`unregister`
63832
+ * and wiped by `clear()`.
63833
+ *
63834
+ * Looked up by `_tick` to decide whether to skip; written by `_runEntry`
63835
+ * on successful settle.
63836
+ */
63837
+ this._firedOnce = new Set();
63838
+ /**
63839
+ * Register a periodic cron entry.
63840
+ *
63841
+ * Idempotent on `name`: re-registering the same name replaces the previous
63842
+ * entry (interval/symbols/handler can all change). Re-registration does
63843
+ * **not** clear in-flight promises — entries still resolving complete with
63844
+ * the previous handler.
63845
+ *
63846
+ * @param entry - Entry configuration; see {@link CronEntry}.
63847
+ * @returns Disposer function — call it to unregister the entry.
63848
+ *
63849
+ * @example
63850
+ * ```typescript
63851
+ * const dispose = Cron.register({
63852
+ * name: "fetch-funding",
63853
+ * interval: "8h",
63854
+ * symbols: ["BTCUSDT", "ETHUSDT"],
63855
+ * handler: async (symbol, when, backtest) => { ... },
63856
+ * });
63857
+ * // Later:
63858
+ * dispose();
63859
+ * ```
63860
+ */
63861
+ this.register = (entry) => {
63862
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_REGISTER, {
63863
+ name: entry.name,
63864
+ interval: entry.interval,
63865
+ symbols: entry.symbols,
63866
+ });
63867
+ if (!entry.name) {
63868
+ throw new Error("CronUtils.register requires a non-empty name");
63869
+ }
63870
+ if (entry.name.includes(":")) {
63871
+ throw new Error(`CronUtils.register: name must not contain ':' (got "${entry.name}"). ` +
63872
+ `':' is reserved as the segment separator in slot keys.`);
63873
+ }
63874
+ if (entry.symbols) {
63875
+ for (const symbol of entry.symbols) {
63876
+ if (symbol.includes(":")) {
63877
+ throw new Error(`CronUtils.register: symbols[] entry must not contain ':' (got "${symbol}"). ` +
63878
+ `':' is reserved as the segment separator in slot keys.`);
63879
+ }
63880
+ }
63881
+ }
63882
+ this._clearFiredOnceFor(entry.name);
63883
+ const generation = ++this._generationCounter;
63884
+ this._entries.set(entry.name, { entry, generation });
63885
+ return () => this.unregister(entry.name);
63886
+ };
63887
+ /**
63888
+ * Remove a registered entry by name.
63889
+ *
63890
+ * Does not cancel handlers already in flight — those resolve on their own
63891
+ * and clear their slot via `.finally()`.
63892
+ *
63893
+ * @param name - Name passed to `register`.
63894
+ */
63895
+ this.unregister = (name) => {
63896
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
63897
+ this._entries.delete(name);
63898
+ this._clearFiredOnceFor(name);
63899
+ };
63900
+ /**
63901
+ * Clear fire-once marks so that fire-once entries can fire again.
63902
+ *
63903
+ * Does **not** touch `_inFlight` — that map holds shared in-flight handler
63904
+ * promises through which parallel `tick`s coordinate. Wiping it mid-flight
63905
+ * would let a new `tick` start a second handler for a boundary that's
63906
+ * already running, breaking the singleshot contract.
63907
+ *
63908
+ * Two modes:
63909
+ * - **Per-symbol** (`symbol` provided): clears only fan-out fire-once
63910
+ * marks for that symbol — keys of the shape `${name}:${symbol}:g${gen}`.
63911
+ * Global fire-once marks (`${name}:g${gen}`, no symbol component) are
63912
+ * left intact, since they are not attributable to a single symbol.
63913
+ * Useful for re-arming fan-out fire-once entries when a particular
63914
+ * symbol's run finishes and you want a future re-run to fire again.
63915
+ * - **All** (no argument): wipes every fire-once mark across all entries
63916
+ * and symbols. Registered entries are not removed — use `unregister`
63917
+ * (or the disposer returned by `register`) for that.
63918
+ *
63919
+ * **Race with in-flight handlers.** `_firedOnce` is written in
63920
+ * `_runEntry`'s `.finally()`, which can run *after* a concurrent
63921
+ * `clear()` call. In that case the fire-once mark reappears immediately
63922
+ * after being wiped, and the next tick will treat the entry as already
63923
+ * fired. This is consistent with the singleshot promise itself surviving
63924
+ * `clear()` — the handler is allowed to finish — and the entry's
63925
+ * generation suffix in `firedKey` guarantees the stale mark cannot
63926
+ * outlive a subsequent `register()` of the same name. If you need a hard
63927
+ * re-arm, `unregister` + `register` bumps the generation and makes any
63928
+ * late write a no-op.
63929
+ *
63930
+ * @param symbol - Optional symbol filter; if omitted, clears all fire-once
63931
+ * marks.
63932
+ */
63933
+ this.clear = (symbol) => {
63934
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_CLEAR, { symbol });
63935
+ if (!symbol) {
63936
+ this._firedOnce.clear();
63937
+ return;
63938
+ }
63939
+ const symbolSegment = `:${symbol}:`;
63940
+ for (const key of this._firedOnce) {
63941
+ if (key.includes(symbolSegment)) {
63942
+ this._firedOnce.delete(key);
63943
+ }
63944
+ }
63945
+ };
63946
+ /**
63947
+ * Process a virtual-time tick for `symbol` and fire any due cron entries.
63948
+ *
63949
+ * **Private.** Invoked exclusively by the lifecycle bridge installed in
63950
+ * {@link enable} — `beforeStart` / `idlePing` / `activePing` / `schedulePing`
63951
+ * are funneled here through a shared `singlerun` queue, so calls to
63952
+ * `_tick` are serialised end-to-end. Do not call directly.
63953
+ *
63954
+ * Algorithm (per registered entry):
63955
+ * 0. Base-align the incoming `when` down to the 1-minute boundary (`ts`).
63956
+ * Lifecycle subjects may emit with sub-second jitter; rounding here
63957
+ * guarantees that `beforeStart` / `idlePing` / `activePing` /
63958
+ * `schedulePing` for the same virtual minute all hash to the same
63959
+ * slot key.
63960
+ * 1. If `entry.symbols` is non-empty and does not include `symbol`, skip.
63961
+ * 2. Decide scope from `entry.symbols`:
63962
+ * - Empty/undefined → **global** (slot key has no symbol component).
63963
+ * - Non-empty → **fan-out**, slot key carries `:${symbol}` so each
63964
+ * whitelisted symbol gets its own slot and handler invocation.
63965
+ * 3. Append the current entry generation suffix `:g${generation}` to both
63966
+ * slot key and fired-once key. This isolates incarnations of the same
63967
+ * `name`: a `register()` after an in-flight handler bumps the
63968
+ * generation, so the late `_firedOnce` write from the old handler can
63969
+ * never block the new entry.
63970
+ * 4. **Fire-once** (`entry.interval === undefined`):
63971
+ * - If the entry's fired-once key is already in `_firedOnce`, skip.
63972
+ * - Slot key: `${name}:once` (+ scope) (+ gen).
63973
+ * - `aligned` = the 1-minute-aligned `when` from step 0.
63974
+ * 5. **Periodic** (`entry.interval` set):
63975
+ * - Align `when` further to the entry's interval via {@link alignToInterval}.
63976
+ * - If `ts !== alignedMs`, the tick is mid-interval — skip.
63977
+ * (This is the "remainder === 0" boundary check from the spec;
63978
+ * since `ts` is already on the 1-minute boundary, the check is exact
63979
+ * for `1m` and consistent for higher intervals.)
63980
+ * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
63981
+ * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
63982
+ * already exists, `await` the same promise. Otherwise invoke
63983
+ * `entry.handler`, store the promise, and `await` it. The slot is
63984
+ * removed in `.finally()` so the next boundary creates a fresh promise;
63985
+ * for fire-once entries the fired-once key is also added to
63986
+ * `_firedOnce` on success so subsequent ticks skip it.
63987
+ *
63988
+ * Errors thrown by `handler` are caught, logged via `console.error`, and
63989
+ * **not** rethrown — a failing handler must not break the per-symbol
63990
+ * tick loop or unblock other parallel backtests with an unhandled
63991
+ * rejection. A failed fire-once handler is **not** marked as fired and
63992
+ * will retry on the next tick.
63993
+ *
63994
+ * Requires active method context and execution context.
63995
+ *
63996
+ * @param symbol - Trading symbol from the current tick.
63997
+ * @param when - Virtual time of the current tick.
63998
+ * @param backtest - `true` for backtest ticks, `false` for live ticks.
63999
+ * Forwarded as the third argument to `entry.handler`. Only the value
64000
+ * from the tick that **opens** a given slot is observed by all parallel
64001
+ * awaiters of that slot.
64002
+ * @throws Error if method or execution context is missing.
64003
+ */
64004
+ this._tick = async (symbol, when, backtest) => {
64005
+ LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
64006
+ symbol,
64007
+ when,
64008
+ });
64009
+ if (!MethodContextService.hasContext()) {
64010
+ throw new Error("CronUtils _tick requires method context");
64011
+ }
64012
+ if (!ExecutionContextService.hasContext()) {
64013
+ throw new Error("CronUtils _tick requires execution context");
64014
+ }
64015
+ const ts = alignToInterval(when, "1m").getTime();
64016
+ const taskList = [];
64017
+ for (const { entry, generation } of this._entries.values()) {
64018
+ if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
64019
+ continue;
64020
+ }
64021
+ const perSymbol = !!entry.symbols?.length;
64022
+ const scope = perSymbol ? `:${symbol}` : "";
64023
+ const genSuffix = `:g${generation}`;
64024
+ let aligned;
64025
+ let alignedMs;
64026
+ let slotKey;
64027
+ let firedKey;
64028
+ if (entry.interval === undefined) {
64029
+ const onceKey = `${entry.name}${scope}${genSuffix}`;
64030
+ if (this._firedOnce.has(onceKey)) {
64031
+ continue;
64032
+ }
64033
+ aligned = alignToInterval(when, "1m");
64034
+ alignedMs = ts;
64035
+ slotKey = `${entry.name}:once${scope}${genSuffix}`;
64036
+ firedKey = onceKey;
64037
+ }
64038
+ else {
64039
+ aligned = alignToInterval(when, entry.interval);
64040
+ alignedMs = aligned.getTime();
64041
+ if (ts !== alignedMs) {
64042
+ continue;
64043
+ }
64044
+ slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
64045
+ firedKey = null;
64046
+ }
64047
+ let pending = this._inFlight.get(slotKey);
64048
+ if (!pending) {
64049
+ pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64050
+ this._inFlight.set(slotKey, pending);
64051
+ }
64052
+ taskList.push(pending);
64053
+ }
64054
+ await Promise.all(taskList);
64055
+ };
64056
+ /**
64057
+ * Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
64058
+ * entries fire automatically — no manual wiring of `listenTickBacktest` /
64059
+ * `listenSchedulePing` etc. needed.
64060
+ *
64061
+ * Subjects funneled into {@link _tick}:
64062
+ * - `beforeStartSubject` — first event of every run.
64063
+ * - `idlePingSubject` — every tick when no signal is pending or scheduled.
64064
+ * - `activePingSubject` — every tick while a pending signal is being monitored.
64065
+ * - `schedulePingSubject` — every tick while a scheduled signal is being monitored.
64066
+ *
64067
+ * All four subjects are subscribed to a single `singlerun`-wrapped
64068
+ * handler that builds `_tick(event.symbol, new Date(event.timestamp),
64069
+ * event.backtest)`. `singlerun` merges the four streams into one serial
64070
+ * queue: at most one `_tick` runs at a time, the next waits. This matters
64071
+ * because the engine can emit `beforeStart` and an immediate `idlePing`
64072
+ * on the very same minute, and concurrent `_tick`s on the same
64073
+ * `(symbol, minute)` would otherwise race to open the same `_inFlight`
64074
+ * slot before either commit. Together these four sources cover every
64075
+ * tick the engine processes for every `(symbol, virtual-minute)` pair
64076
+ * regardless of whether the strategy is idle, active, or scheduled.
64077
+ *
64078
+ * `enable` itself is wrapped in `singleshot`, so calling it repeatedly is
64079
+ * a no-op — subsequent calls return the same disposer. The disposer
64080
+ * unsubscribes from every subject and resets the singleshot so a future
64081
+ * `enable()` can re-subscribe cleanly. Equivalent to the
64082
+ * `RecentAdapter.enable` pattern.
64083
+ *
64084
+ * The `.subscribe` callbacks are synchronous wrappers around the
64085
+ * `singlerun`-async handler; `_tick`'s returned promise is awaited inside
64086
+ * `singlerun` to enforce ordering but not bubbled back to the subject.
64087
+ * Errors are caught and logged inside `_runEntry`.
64088
+ *
64089
+ * @returns Cleanup function that unsubscribes from all four subjects and
64090
+ * resets the singleshot. Idempotent.
64091
+ *
64092
+ * @example
64093
+ * ```typescript
64094
+ * import { Cron } from "backtest-kit";
64095
+ *
64096
+ * Cron.register({ name: "tg-parser", interval: "1h", handler });
64097
+ * Cron.enable(); // wire once at startup
64098
+ * // ... run backtests / live as usual
64099
+ * Cron.disable(); // on shutdown
64100
+ * ```
64101
+ */
64102
+ this.enable = functoolsKit.singleshot(() => {
64103
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
64104
+ const handleTick = functoolsKit.singlerun(async (event) => {
64105
+ return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
64106
+ });
64107
+ const unBeforeStart = beforeStartSubject.subscribe(handleTick);
64108
+ const unIdlePing = idlePingSubject.subscribe(handleTick);
64109
+ const unActivePing = activePingSubject.subscribe(handleTick);
64110
+ const unSchedulePing = schedulePingSubject.subscribe(handleTick);
64111
+ return functoolsKit.compose(() => unBeforeStart(), () => unIdlePing(), () => unActivePing(), () => unSchedulePing(), () => this.enable.clear());
64112
+ });
64113
+ /**
64114
+ * Tear down the lifecycle subscriptions installed by {@link enable}.
64115
+ *
64116
+ * Safe to call multiple times and safe to call before `enable()` — both
64117
+ * are no-ops. Does **not** unregister entries, does **not** touch
64118
+ * `_inFlight`, and does **not** wipe `_firedOnce` (use `unregister` or
64119
+ * `clear()` for those).
64120
+ */
64121
+ this.disable = () => {
64122
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISABLE);
64123
+ if (this.enable.hasValue()) {
64124
+ const lastSubscription = this.enable();
64125
+ lastSubscription();
64126
+ }
64127
+ };
64128
+ /**
64129
+ * Hard-reset the entire `Cron` state.
64130
+ *
64131
+ * Performs in order:
64132
+ * 1. {@link disable} — tears down lifecycle subscriptions and resets the
64133
+ * `enable` singleshot so a future `enable()` re-subscribes cleanly.
64134
+ * 2. Wipes `_entries` — every {@link register}'ed entry is forgotten.
64135
+ * Disposers returned by previous `register()` calls become no-ops
64136
+ * (their `unregister(name)` will not find anything to remove).
64137
+ * 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
64138
+ * re-registration of the same `name` fires again on the next matching
64139
+ * tick.
64140
+ * 4. Does **not** touch `_inFlight` — in-flight handlers continue to
64141
+ * settle in the background and clear their own slots via `.finally()`.
64142
+ * Their final `_firedOnce.add(firedKey)` writes carry old-generation
64143
+ * keys and are harmless (lookup uses the post-dispose generation).
64144
+ *
64145
+ * Use from a CLI/session teardown when you want to throw away every
64146
+ * registration along with the lifecycle wiring — e.g. between two
64147
+ * independent runner scopes. For "just snap the subscriptions but keep
64148
+ * registrations" use {@link disable} instead; for "just re-arm fire-once
64149
+ * marks" use {@link clear}.
64150
+ *
64151
+ * Idempotent. Safe to call multiple times and safe to call before
64152
+ * `enable()` / without any registrations.
64153
+ */
64154
+ this.dispose = () => {
64155
+ LOGGER_SERVICE$1.info(CRON_METHOD_NAME_DISPOSE);
64156
+ this.disable();
64157
+ this._entries.clear();
64158
+ this._firedOnce.clear();
64159
+ };
64160
+ }
64161
+ /**
64162
+ * Garbage-collect every `_firedOnce` key that belongs to the entry `name`
64163
+ * (any generation, global or fan-out).
64164
+ *
64165
+ * Called from `register`/`unregister` to free memory; **not** required
64166
+ * for correctness — the generation suffix already isolates re-registrations,
64167
+ * so leftover keys from old generations can never block a new entry.
64168
+ * They just sit unused until they are GC'd here or wiped by `clear()`.
64169
+ */
64170
+ _clearFiredOnceFor(name) {
64171
+ if (!name) {
64172
+ return;
64173
+ }
64174
+ const prefix = `${name}:`;
64175
+ for (const key of this._firedOnce) {
64176
+ if (key === name || key.startsWith(prefix)) {
64177
+ this._firedOnce.delete(key);
64178
+ }
64179
+ }
64180
+ }
64181
+ /**
64182
+ * Build the singleshot promise for a single in-flight slot.
64183
+ *
64184
+ * Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
64185
+ * any error via `console.error`, and clears the `_inFlight` slot
64186
+ * in `.finally()` so the next boundary produces a fresh promise. For
64187
+ * fire-once entries `firedKey` is added to `_firedOnce` on success so
64188
+ * subsequent ticks skip it.
64189
+ *
64190
+ * @param firedKey - Key to add to `_firedOnce` on success, or `null` for
64191
+ * periodic entries (which never populate `_firedOnce`).
64192
+ * @param backtest - Value forwarded as the third handler argument; the
64193
+ * "winner" tick's flag is what all parallel awaiters of this slot see.
64194
+ */
64195
+ async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
64196
+ let failed = false;
64197
+ try {
64198
+ await entry.handler(symbol, aligned, backtest);
64199
+ }
64200
+ catch (err) {
64201
+ failed = true;
64202
+ console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
64203
+ }
64204
+ finally {
64205
+ this._inFlight.delete(slotKey);
64206
+ if (!failed && firedKey !== null) {
64207
+ this._firedOnce.add(firedKey);
64208
+ }
64209
+ }
64210
+ }
64211
+ }
64212
+ /**
64213
+ * Singleton instance of {@link CronUtils} for registering periodic tasks
64214
+ * coordinated across parallel `Backtest.background` runs.
64215
+ *
64216
+ * @example
64217
+ * ```typescript
64218
+ * import { Cron } from "backtest-kit";
64219
+ *
64220
+ * Cron.register({
64221
+ * name: "tg-parser",
64222
+ * interval: "1h",
64223
+ * handler: async (symbol, when, backtest) => { ... },
64224
+ * });
64225
+ * ```
64226
+ */
64227
+ const Cron = new CronUtils();
64228
+
63256
64229
  const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
63257
64230
  const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
63258
64231
  const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
@@ -64335,7 +65308,7 @@ const validateSignal = (signal, currentPrice) => {
64335
65308
  }
64336
65309
  }
64337
65310
  if (errors.length > 0) {
64338
- console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
65311
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
64339
65312
  return false;
64340
65313
  }
64341
65314
  try {
@@ -64408,7 +65381,7 @@ const validateSignal = (signal, currentPrice) => {
64408
65381
  }
64409
65382
  }
64410
65383
  if (errors.length > 0) {
64411
- console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
65384
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
64412
65385
  }
64413
65386
  return !errors.length;
64414
65387
  };
@@ -64420,6 +65393,7 @@ exports.Broker = Broker;
64420
65393
  exports.BrokerBase = BrokerBase;
64421
65394
  exports.Cache = Cache;
64422
65395
  exports.Constant = Constant;
65396
+ exports.Cron = Cron;
64423
65397
  exports.Dump = Dump;
64424
65398
  exports.Exchange = Exchange;
64425
65399
  exports.ExecutionContextService = ExecutionContextService;
@@ -64694,6 +65668,7 @@ exports.shutdown = shutdown;
64694
65668
  exports.slPercentShiftToPrice = slPercentShiftToPrice;
64695
65669
  exports.slPriceToPercentShift = slPriceToPercentShift;
64696
65670
  exports.stopStrategy = stopStrategy;
65671
+ exports.toPlainString = toPlainString;
64697
65672
  exports.toProfitLossDto = toProfitLossDto;
64698
65673
  exports.tpPercentShiftToPrice = tpPercentShiftToPrice;
64699
65674
  exports.tpPriceToPercentShift = tpPriceToPercentShift;