backtest-kit 12.8.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -981,6 +981,12 @@ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writ
981
981
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
982
982
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
983
983
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR = "PersistScheduleUtils.clear";
984
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER = "PersistStrategyUtils.usePersistStrategyAdapter";
985
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA = "PersistStrategyUtils.readStrategyData";
986
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA = "PersistStrategyUtils.writeStrategyData";
987
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON = "PersistStrategyUtils.useJson";
988
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY = "PersistStrategyUtils.useDummy";
989
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR = "PersistStrategyUtils.clear";
984
990
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
985
991
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
986
992
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
@@ -1844,6 +1850,212 @@ class PersistScheduleUtils {
1844
1850
  * ```
1845
1851
  */
1846
1852
  const PersistScheduleAdapter = new PersistScheduleUtils();
1853
+ /**
1854
+ * Default file-based implementation of IPersistStrategyInstance.
1855
+ *
1856
+ * Features:
1857
+ * - Wraps PersistBase for atomic JSON writes
1858
+ * - Uses fixed entity ID "strategy" within a per-context PersistBase
1859
+ * - Crash-safe via atomic writes
1860
+ *
1861
+ * @example
1862
+ * ```typescript
1863
+ * const instance = new PersistStrategyInstance("BTCUSDT", "my-strategy", "binance");
1864
+ * await instance.waitForInit(true);
1865
+ * await instance.writeStrategyData(snapshot);
1866
+ * const restored = await instance.readStrategyData();
1867
+ * ```
1868
+ */
1869
+ class PersistStrategyInstance {
1870
+ /**
1871
+ * Creates new deferred strategy state persistence instance.
1872
+ *
1873
+ * @param symbol - Trading pair symbol
1874
+ * @param strategyName - Strategy identifier
1875
+ * @param exchangeName - Exchange identifier
1876
+ */
1877
+ constructor(symbol, strategyName, exchangeName) {
1878
+ this.symbol = symbol;
1879
+ this.strategyName = strategyName;
1880
+ this.exchangeName = exchangeName;
1881
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/strategy/`);
1882
+ }
1883
+ /**
1884
+ * Initializes the underlying PersistBase storage.
1885
+ *
1886
+ * @param initial - Whether this is the first initialization
1887
+ * @returns Promise that resolves when initialization is complete
1888
+ */
1889
+ async waitForInit(initial) {
1890
+ await this._storage.waitForInit(initial);
1891
+ }
1892
+ /**
1893
+ * Reads the persisted strategy state snapshot using the fixed STORAGE_KEY.
1894
+ *
1895
+ * @returns Promise resolving to strategy state snapshot or null if not found
1896
+ */
1897
+ async readStrategyData() {
1898
+ if (await this._storage.hasValue(PersistStrategyInstance.STORAGE_KEY)) {
1899
+ const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
1900
+ // JSON serializes Infinity as null, so an eternal-hold signal
1901
+ // (minuteEstimatedTime: Infinity) reads back as null — restore it.
1902
+ for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
1903
+ if (signal && signal.minuteEstimatedTime == null) {
1904
+ signal.minuteEstimatedTime = Infinity;
1905
+ }
1906
+ }
1907
+ return strategyData;
1908
+ }
1909
+ return null;
1910
+ }
1911
+ /**
1912
+ * Writes the strategy state snapshot (or null to clear) using the fixed STORAGE_KEY.
1913
+ *
1914
+ * @param row - Strategy state snapshot to persist, or null to clear
1915
+ * @returns Promise that resolves when write is complete
1916
+ */
1917
+ async writeStrategyData(row) {
1918
+ await this._storage.writeValue(PersistStrategyInstance.STORAGE_KEY, row);
1919
+ }
1920
+ }
1921
+ /** Fixed entity key for storing the strategy state snapshot */
1922
+ PersistStrategyInstance.STORAGE_KEY = "strategy";
1923
+ /**
1924
+ * No-op IPersistStrategyInstance implementation used by PersistStrategyUtils.useDummy().
1925
+ * All reads return null, all writes are discarded.
1926
+ */
1927
+ class PersistStrategyDummyInstance {
1928
+ /**
1929
+ * No-op constructor.
1930
+ * Context arguments are accepted to satisfy TPersistStrategyInstanceCtor.
1931
+ */
1932
+ constructor(_symbol, _strategyName, _exchangeName) { }
1933
+ /**
1934
+ * No-op initialization.
1935
+ * @returns Promise that resolves immediately
1936
+ */
1937
+ async waitForInit(_initial) { }
1938
+ /**
1939
+ * Always returns null (no persisted strategy state).
1940
+ * @returns Promise resolving to null
1941
+ */
1942
+ async readStrategyData() { return null; }
1943
+ /**
1944
+ * No-op write (discards strategy state).
1945
+ * @returns Promise that resolves immediately
1946
+ */
1947
+ async writeStrategyData(_row) { }
1948
+ }
1949
+ /**
1950
+ * Utility class for managing deferred strategy state persistence.
1951
+ *
1952
+ * Features:
1953
+ * - Memoized storage instances per strategy
1954
+ * - Custom adapter support
1955
+ * - Atomic read/write operations for the deferred state snapshot
1956
+ * - Crash-safe in-flight broker operation state management
1957
+ *
1958
+ * Used by ClientStrategy for live mode persistence of the commit queue and deferred
1959
+ * user actions (_commitQueue, _closedSignal, _cancelledSignal, _activatedSignal).
1960
+ */
1961
+ class PersistStrategyUtils {
1962
+ constructor() {
1963
+ /**
1964
+ * Constructor used to create per-context strategy state instances.
1965
+ * Replaceable via usePersistStrategyAdapter() / useJson() / useDummy().
1966
+ */
1967
+ this.PersistStrategyInstanceCtor = PersistStrategyInstance;
1968
+ /**
1969
+ * Memoized factory creating one IPersistStrategyInstance per (symbol, strategy, exchange) triple.
1970
+ */
1971
+ this.getStrategyStorage = memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistStrategyInstanceCtor, [symbol, strategyName, exchangeName]));
1972
+ /**
1973
+ * Reads persisted deferred strategy state for the given context.
1974
+ * Lazily initializes the instance on first access.
1975
+ *
1976
+ * @param symbol - Trading pair symbol
1977
+ * @param strategyName - Strategy identifier
1978
+ * @param exchangeName - Exchange identifier
1979
+ * @returns Promise resolving to strategy state snapshot or null if none persisted
1980
+ */
1981
+ this.readStrategyData = async (symbol, strategyName, exchangeName) => {
1982
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA);
1983
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
1984
+ const isInitial = !this.getStrategyStorage.has(key);
1985
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
1986
+ await instance.waitForInit(isInitial);
1987
+ return instance.readStrategyData();
1988
+ };
1989
+ /**
1990
+ * Writes deferred strategy state (or null to clear) for the given context.
1991
+ * Lazily initializes the instance on first access.
1992
+ *
1993
+ * @param strategyRow - Strategy state snapshot to persist, or null to clear
1994
+ * @param symbol - Trading pair symbol
1995
+ * @param strategyName - Strategy identifier
1996
+ * @param exchangeName - Exchange identifier
1997
+ * @returns Promise that resolves when write is complete
1998
+ */
1999
+ this.writeStrategyData = async (strategyRow, symbol, strategyName, exchangeName) => {
2000
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA);
2001
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
2002
+ const isInitial = !this.getStrategyStorage.has(key);
2003
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
2004
+ await instance.waitForInit(isInitial);
2005
+ return instance.writeStrategyData(strategyRow);
2006
+ };
2007
+ }
2008
+ /**
2009
+ * Registers a custom IPersistStrategyInstance constructor.
2010
+ * Clears the memoization cache so subsequent calls use the new adapter.
2011
+ *
2012
+ * @param Ctor - Custom IPersistStrategyInstance constructor
2013
+ */
2014
+ usePersistStrategyAdapter(Ctor) {
2015
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER);
2016
+ this.PersistStrategyInstanceCtor = Ctor;
2017
+ this.getStrategyStorage.clear();
2018
+ }
2019
+ /**
2020
+ * Clears the memoized instance cache.
2021
+ * Call when process.cwd() changes between strategy iterations.
2022
+ */
2023
+ clear() {
2024
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR);
2025
+ this.getStrategyStorage.clear();
2026
+ }
2027
+ /**
2028
+ * Switches to the default file-based PersistStrategyInstance.
2029
+ */
2030
+ useJson() {
2031
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON);
2032
+ this.usePersistStrategyAdapter(PersistStrategyInstance);
2033
+ }
2034
+ /**
2035
+ * Switches to PersistStrategyDummyInstance (all operations are no-ops).
2036
+ */
2037
+ useDummy() {
2038
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY);
2039
+ this.usePersistStrategyAdapter(PersistStrategyDummyInstance);
2040
+ }
2041
+ }
2042
+ /**
2043
+ * Global singleton instance of PersistStrategyUtils.
2044
+ * Used by ClientStrategy for deferred strategy state persistence.
2045
+ *
2046
+ * @example
2047
+ * ```typescript
2048
+ * // Custom adapter
2049
+ * PersistStrategyAdapter.usePersistStrategyAdapter(RedisPersist);
2050
+ *
2051
+ * // Read strategy state
2052
+ * const state = await PersistStrategyAdapter.readStrategyData("BTCUSDT", "my-strategy", "binance");
2053
+ *
2054
+ * // Write strategy state
2055
+ * await PersistStrategyAdapter.writeStrategyData(state, "BTCUSDT", "my-strategy", "binance");
2056
+ * ```
2057
+ */
2058
+ const PersistStrategyAdapter = new PersistStrategyUtils();
1847
2059
  /**
1848
2060
  * Default file-based implementation of IPersistPartialInstance.
1849
2061
  *
@@ -6771,6 +6983,9 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
6771
6983
  {
6772
6984
  self._commitQueue = [];
6773
6985
  }
6986
+ // Persist the now-empty queue so a crash after draining does not replay commits
6987
+ // that were already forwarded to the broker on the next restart.
6988
+ await PERSIST_STRATEGY_FN(self);
6774
6989
  if (!self._pendingSignal) {
6775
6990
  return;
6776
6991
  }
@@ -7227,6 +7442,19 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7227
7442
  self._lastPendingId = recentSignal.id;
7228
7443
  }
7229
7444
  }
7445
+ // Restore deferred strategy state (commit queue + deferred user actions) so any
7446
+ // confirmed-but-not-yet-forwarded broker operation survives a live crash and is
7447
+ // re-drained on the next tick. Placed before the pending/scheduled restore which
7448
+ // may early-return on exchange/strategy mismatch.
7449
+ {
7450
+ const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7451
+ if (strategyData) {
7452
+ self._commitQueue = strategyData.commitQueue ?? [];
7453
+ self._closedSignal = strategyData.closedSignal;
7454
+ self._cancelledSignal = strategyData.cancelledSignal;
7455
+ self._activatedSignal = strategyData.activatedSignal;
7456
+ }
7457
+ }
7230
7458
  // Restore pending signal
7231
7459
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7232
7460
  if (pendingSignal) {
@@ -7276,6 +7504,28 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
7276
7504
  self.params.logger.debug("ClientStrategy dispose");
7277
7505
  await self.params.onDispose(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, self.params.execution.context.backtest);
7278
7506
  };
7507
+ /**
7508
+ * Persists the deferred strategy state snapshot (commit queue + deferred user actions)
7509
+ * to disk in live mode.
7510
+ *
7511
+ * These fields carry confirmed-but-not-yet-forwarded broker operations and survive the
7512
+ * gap between ticks (drained at the start of the next tick). Without persistence a live
7513
+ * crash in that window silently loses the pending broker operation while _pendingSignal
7514
+ * (already mutated and saved) claims it happened. Skipped in backtest mode.
7515
+ *
7516
+ * @param self - ClientStrategy instance
7517
+ */
7518
+ const PERSIST_STRATEGY_FN = async (self) => {
7519
+ if (self.params.backtest) {
7520
+ return;
7521
+ }
7522
+ await PersistStrategyAdapter.writeStrategyData({
7523
+ commitQueue: self._commitQueue,
7524
+ closedSignal: self._closedSignal,
7525
+ cancelledSignal: self._cancelledSignal,
7526
+ activatedSignal: self._activatedSignal,
7527
+ }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7528
+ };
7279
7529
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
7280
7530
  // Initialize partial array if not present
7281
7531
  if (!signal._partial)
@@ -8900,14 +9150,15 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
8900
9150
  const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
8901
9151
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
8902
9152
  if (!syncCloseAllowed) {
8903
- self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
9153
+ // Sync close rejected (e.g. broker rejected the order) keep _closedSignal intact
9154
+ // and return null so the candle loop re-attempts on the next candle. Mirrors live
9155
+ // tick, which keeps _closedSignal and returns idle on a rejected user close,
9156
+ // re-trying on the following tick.
9157
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry on next candle", {
8904
9158
  symbol: self.params.execution.context.symbol,
8905
9159
  signalId: closedSignal.id,
8906
9160
  });
8907
- self._closedSignal = null;
8908
- self._pendingSignal = closedSignal;
8909
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
8910
- `Retry backtest() with new candle data.`);
9161
+ return null;
8911
9162
  }
8912
9163
  self._closedSignal = null;
8913
9164
  const publicSignal = TO_PUBLIC_SIGNAL("pending", closedSignal, averagePrice);
@@ -9159,7 +9410,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9159
9410
  }
9160
9411
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
9161
9412
  if (self._closedSignal) {
9162
- return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9413
+ const userCloseResult = await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9414
+ // Sync close accepted — position closed, return. If rejected, _closedSignal is
9415
+ // kept and we skip this candle: live tick returns idle on a rejected user close
9416
+ // (it does NOT fall into the TP/SL or active-monitoring path), so the candle is
9417
+ // skipped here and the close is re-attempted on the next candle.
9418
+ if (userCloseResult) {
9419
+ return userCloseResult;
9420
+ }
9421
+ continue;
9163
9422
  }
9164
9423
  let shouldClose = false;
9165
9424
  let closeReason;
@@ -9210,7 +9469,19 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9210
9469
  else {
9211
9470
  closePrice = averagePrice; // time_expired uses VWAP
9212
9471
  }
9213
- return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9472
+ const closeResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9473
+ // Sync close accepted — position closed, return.
9474
+ if (closeResult) {
9475
+ return closeResult;
9476
+ }
9477
+ // Sync close rejected (e.g. broker rejected the order) — _pendingSignal is left
9478
+ // intact (not cleared on rejection). Do NOT return/continue: fall through to the
9479
+ // active-monitoring block below so this candle is processed exactly like live
9480
+ // tick, which runs RETURN_PENDING_SIGNAL_ACTIVE_FN when CLOSE_PENDING_SIGNAL_FN
9481
+ // returns null (updates _peak/_fall, fires active ping / breakeven / partial
9482
+ // callbacks, drains the commit queue). The close is re-attempted on the next
9483
+ // candle; for time_expired this eventually reaches the loop-exhausted close
9484
+ // (which throws if still rejected).
9214
9485
  }
9215
9486
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
9216
9487
  // Calculate percentage of path to TP/SL
@@ -9632,7 +9903,7 @@ class ClientStrategy {
9632
9903
  async getStopped(symbol) {
9633
9904
  this.params.logger.debug("ClientStrategy getStopped", {
9634
9905
  symbol,
9635
- strategyName: this.params.method.context.strategyName,
9906
+ strategyName: this.params.strategyName,
9636
9907
  });
9637
9908
  return this._isStopped;
9638
9909
  }
@@ -10315,6 +10586,8 @@ class ClientStrategy {
10315
10586
  if (this._cancelledSignal) {
10316
10587
  const cancelledSignal = this._cancelledSignal;
10317
10588
  this._cancelledSignal = null; // Clear after emitting
10589
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10590
+ await PERSIST_STRATEGY_FN(this);
10318
10591
  this.params.logger.info("ClientStrategy tick: scheduled signal was cancelled", {
10319
10592
  symbol: this.params.execution.context.symbol,
10320
10593
  signalId: cancelledSignal.id,
@@ -10373,6 +10646,8 @@ class ClientStrategy {
10373
10646
  return await RETURN_IDLE_FN(this, currentPrice);
10374
10647
  }
10375
10648
  this._closedSignal = null; // Clear only after sync confirmed
10649
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10650
+ await PERSIST_STRATEGY_FN(this);
10376
10651
  this.params.logger.info("ClientStrategy tick: pending signal was closed", {
10377
10652
  symbol: this.params.execution.context.symbol,
10378
10653
  signalId: closedSignal.id,
@@ -10428,6 +10703,8 @@ class ClientStrategy {
10428
10703
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
10429
10704
  const activatedSignal = this._activatedSignal;
10430
10705
  this._activatedSignal = null; // Clear after emitting
10706
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10707
+ await PERSIST_STRATEGY_FN(this);
10431
10708
  this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
10432
10709
  symbol: this.params.execution.context.symbol,
10433
10710
  signalId: activatedSignal.id,
@@ -10685,18 +10962,20 @@ class ClientStrategy {
10685
10962
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
10686
10963
  const closedSignal = this._closedSignal;
10687
10964
  const closeTimestamp = this.params.execution.context.when.getTime();
10688
- // Sync close: if external system rejects — restore _pendingSignal, retry on next backtest() call
10965
+ // Sync close: if external system rejects — keep the close pending and re-attempt
10966
+ // it inside the candle loop below (PROCESS_PENDING_SIGNAL_CANDLES_FN handles
10967
+ // _closedSignal per candle). Mirrors live tick, which keeps _closedSignal and
10968
+ // retries on the next tick instead of failing.
10689
10969
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, currentPrice, "closed", closedSignal, this);
10690
10970
  if (!syncCloseAllowed) {
10691
- this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
10971
+ this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry in candle loop", {
10692
10972
  symbol: this.params.execution.context.symbol,
10693
10973
  signalId: closedSignal.id,
10694
10974
  });
10695
- // Restore _pendingSignal so next backtest() call can process it normally
10696
- this._closedSignal = null;
10975
+ // Restore _pendingSignal so the candle loop processes the position normally;
10976
+ // _closedSignal is kept so the loop re-attempts the close on each candle.
10697
10977
  this._pendingSignal = closedSignal;
10698
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
10699
- `Retry backtest() with new candle data.`);
10978
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, this._pendingSignal, candles, frameEndTime);
10700
10979
  }
10701
10980
  this._closedSignal = null; // Clear only after sync confirmed
10702
10981
  // Emit commit with correct timestamp from backtest context
@@ -10878,7 +11157,7 @@ class ClientStrategy {
10878
11157
  if (backtest) {
10879
11158
  return;
10880
11159
  }
10881
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11160
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
10882
11161
  }
10883
11162
  /**
10884
11163
  * Cancels the scheduled signal without stopping the strategy.
@@ -10922,7 +11201,9 @@ class ClientStrategy {
10922
11201
  // Commit will be emitted in backtest() with correct candle timestamp
10923
11202
  return;
10924
11203
  }
10925
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11204
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11205
+ // Persist deferred _cancelledSignal so a crash before the next tick does not lose it
11206
+ await PERSIST_STRATEGY_FN(this);
10926
11207
  // Commit will be emitted in tick() with correct currentTime
10927
11208
  }
10928
11209
  /**
@@ -10973,7 +11254,9 @@ class ClientStrategy {
10973
11254
  // Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
10974
11255
  return;
10975
11256
  }
10976
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11257
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11258
+ // Persist deferred _activatedSignal so a crash before the next tick does not lose it
11259
+ await PERSIST_STRATEGY_FN(this);
10977
11260
  // Commit will be emitted AFTER successful risk check in tick()
10978
11261
  }
10979
11262
  /**
@@ -11018,6 +11301,8 @@ class ClientStrategy {
11018
11301
  return;
11019
11302
  }
11020
11303
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11304
+ // Persist deferred _closedSignal so a crash before the next tick does not lose it
11305
+ await PERSIST_STRATEGY_FN(this);
11021
11306
  // Commit will be emitted in tick() with correct currentTime
11022
11307
  }
11023
11308
  /**
@@ -11188,10 +11473,10 @@ class ClientStrategy {
11188
11473
  });
11189
11474
  // Call onWrite callback for testing persist storage
11190
11475
  if (this.params.callbacks?.onWrite) {
11191
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11476
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11192
11477
  }
11193
11478
  if (!backtest) {
11194
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11479
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11195
11480
  }
11196
11481
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11197
11482
  this._commitQueue.push({
@@ -11201,6 +11486,8 @@ class ClientStrategy {
11201
11486
  percentToClose,
11202
11487
  currentPrice,
11203
11488
  });
11489
+ // Persist the queued commit so a crash before the next tick does not lose it
11490
+ await PERSIST_STRATEGY_FN(this);
11204
11491
  return true;
11205
11492
  }
11206
11493
  /**
@@ -11371,10 +11658,10 @@ class ClientStrategy {
11371
11658
  });
11372
11659
  // Call onWrite callback for testing persist storage
11373
11660
  if (this.params.callbacks?.onWrite) {
11374
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11661
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11375
11662
  }
11376
11663
  if (!backtest) {
11377
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11664
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11378
11665
  }
11379
11666
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11380
11667
  this._commitQueue.push({
@@ -11384,6 +11671,8 @@ class ClientStrategy {
11384
11671
  percentToClose,
11385
11672
  currentPrice,
11386
11673
  });
11674
+ // Persist the queued commit so a crash before the next tick does not lose it
11675
+ await PERSIST_STRATEGY_FN(this);
11387
11676
  return true;
11388
11677
  }
11389
11678
  /**
@@ -11570,10 +11859,10 @@ class ClientStrategy {
11570
11859
  // Call onWrite callback for testing persist storage
11571
11860
  if (this.params.callbacks?.onWrite) {
11572
11861
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11573
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
11862
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11574
11863
  }
11575
11864
  if (!backtest) {
11576
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11865
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11577
11866
  }
11578
11867
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11579
11868
  this._commitQueue.push({
@@ -11582,6 +11871,8 @@ class ClientStrategy {
11582
11871
  backtest,
11583
11872
  currentPrice,
11584
11873
  });
11874
+ // Persist the queued commit so a crash before the next tick does not lose it
11875
+ await PERSIST_STRATEGY_FN(this);
11585
11876
  return true;
11586
11877
  }
11587
11878
  /**
@@ -11819,10 +12110,10 @@ class ClientStrategy {
11819
12110
  // Call onWrite callback for testing persist storage
11820
12111
  if (this.params.callbacks?.onWrite) {
11821
12112
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11822
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12113
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11823
12114
  }
11824
12115
  if (!backtest) {
11825
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12116
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11826
12117
  }
11827
12118
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11828
12119
  this._commitQueue.push({
@@ -11832,6 +12123,8 @@ class ClientStrategy {
11832
12123
  percentShift,
11833
12124
  currentPrice,
11834
12125
  });
12126
+ // Persist the queued commit so a crash before the next tick does not lose it
12127
+ await PERSIST_STRATEGY_FN(this);
11835
12128
  return true;
11836
12129
  }
11837
12130
  /**
@@ -12055,10 +12348,10 @@ class ClientStrategy {
12055
12348
  // Call onWrite callback for testing persist storage
12056
12349
  if (this.params.callbacks?.onWrite) {
12057
12350
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
12058
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12351
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
12059
12352
  }
12060
12353
  if (!backtest) {
12061
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12354
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12062
12355
  }
12063
12356
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12064
12357
  this._commitQueue.push({
@@ -12068,6 +12361,8 @@ class ClientStrategy {
12068
12361
  percentShift,
12069
12362
  currentPrice,
12070
12363
  });
12364
+ // Persist the queued commit so a crash before the next tick does not lose it
12365
+ await PERSIST_STRATEGY_FN(this);
12071
12366
  return true;
12072
12367
  }
12073
12368
  /**
@@ -12146,10 +12441,10 @@ class ClientStrategy {
12146
12441
  });
12147
12442
  // Call onWrite callback for testing persist storage
12148
12443
  if (this.params.callbacks?.onWrite) {
12149
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12444
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12150
12445
  }
12151
12446
  if (!backtest) {
12152
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12447
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12153
12448
  }
12154
12449
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12155
12450
  this._commitQueue.push({
@@ -12160,6 +12455,8 @@ class ClientStrategy {
12160
12455
  cost,
12161
12456
  totalEntries: this._pendingSignal._entry?.length ?? 1,
12162
12457
  });
12458
+ // Persist the queued commit so a crash before the next tick does not lose it
12459
+ await PERSIST_STRATEGY_FN(this);
12163
12460
  return true;
12164
12461
  }
12165
12462
  }
@@ -22900,32 +23197,32 @@ const heat_columns = [
22900
23197
  {
22901
23198
  key: "totalPnl",
22902
23199
  label: "Total PNL",
22903
- format: (data) => data.totalPnl !== null ? str(data.totalPnl, "%") : "N/A",
23200
+ format: (data) => data.totalPnl !== null ? `${data.totalPnl.toFixed(2)}%` : "N/A",
22904
23201
  isVisible: () => true,
22905
23202
  },
22906
23203
  {
22907
23204
  key: "sharpeRatio",
22908
23205
  label: "Sharpe",
22909
- format: (data) => data.sharpeRatio !== null ? str(data.sharpeRatio) : "N/A",
23206
+ format: (data) => data.sharpeRatio !== null ? data.sharpeRatio.toFixed(3) : "N/A",
22910
23207
  isVisible: () => true,
22911
23208
  },
22912
23209
  {
22913
23210
  key: "annualizedSharpeRatio",
22914
23211
  label: "Ann Sharpe",
22915
- format: (data) => data.annualizedSharpeRatio !== null ? str(data.annualizedSharpeRatio) : "N/A",
23212
+ format: (data) => data.annualizedSharpeRatio !== null ? data.annualizedSharpeRatio.toFixed(3) : "N/A",
22916
23213
  isVisible: () => true,
22917
23214
  },
22918
23215
  {
22919
23216
  key: "certaintyRatio",
22920
23217
  label: "Certainty",
22921
- format: (data) => data.certaintyRatio !== null ? str(data.certaintyRatio) : "N/A",
23218
+ format: (data) => data.certaintyRatio !== null ? data.certaintyRatio.toFixed(3) : "N/A",
22922
23219
  isVisible: () => true,
22923
23220
  },
22924
23221
  {
22925
23222
  key: "expectedYearlyReturns",
22926
23223
  label: "Exp Yearly",
22927
23224
  format: (data) => data.expectedYearlyReturns !== null
22928
- ? str(data.expectedYearlyReturns, "%")
23225
+ ? `${data.expectedYearlyReturns.toFixed(2)}%`
22929
23226
  : "N/A",
22930
23227
  isVisible: () => true,
22931
23228
  },
@@ -22938,37 +23235,37 @@ const heat_columns = [
22938
23235
  {
22939
23236
  key: "profitFactor",
22940
23237
  label: "PF",
22941
- format: (data) => data.profitFactor !== null ? str(data.profitFactor) : "N/A",
23238
+ format: (data) => data.profitFactor !== null ? data.profitFactor.toFixed(3) : "N/A",
22942
23239
  isVisible: () => true,
22943
23240
  },
22944
23241
  {
22945
23242
  key: "expectancy",
22946
23243
  label: "Expect",
22947
- format: (data) => data.expectancy !== null ? str(data.expectancy, "%") : "N/A",
23244
+ format: (data) => data.expectancy !== null ? `${data.expectancy.toFixed(2)}%` : "N/A",
22948
23245
  isVisible: () => true,
22949
23246
  },
22950
23247
  {
22951
23248
  key: "winRate",
22952
23249
  label: "WR",
22953
- format: (data) => data.winRate !== null ? str(data.winRate, "%") : "N/A",
23250
+ format: (data) => data.winRate !== null ? `${data.winRate.toFixed(2)}%` : "N/A",
22954
23251
  isVisible: () => true,
22955
23252
  },
22956
23253
  {
22957
23254
  key: "avgWin",
22958
23255
  label: "Avg Win",
22959
- format: (data) => data.avgWin !== null ? str(data.avgWin, "%") : "N/A",
23256
+ format: (data) => data.avgWin !== null ? `${data.avgWin.toFixed(2)}%` : "N/A",
22960
23257
  isVisible: () => true,
22961
23258
  },
22962
23259
  {
22963
23260
  key: "avgLoss",
22964
23261
  label: "Avg Loss",
22965
- format: (data) => data.avgLoss !== null ? str(data.avgLoss, "%") : "N/A",
23262
+ format: (data) => data.avgLoss !== null ? `${data.avgLoss.toFixed(2)}%` : "N/A",
22966
23263
  isVisible: () => true,
22967
23264
  },
22968
23265
  {
22969
23266
  key: "maxDrawdown",
22970
23267
  label: "Max DD",
22971
- format: (data) => data.maxDrawdown !== null ? str(-data.maxDrawdown, "%") : "N/A",
23268
+ format: (data) => data.maxDrawdown !== null ? `${(-data.maxDrawdown).toFixed(2)}%` : "N/A",
22972
23269
  isVisible: () => true,
22973
23270
  },
22974
23271
  {
@@ -22992,31 +23289,31 @@ const heat_columns = [
22992
23289
  {
22993
23290
  key: "avgPeakPnl",
22994
23291
  label: "Avg Peak PNL",
22995
- format: (data) => data.avgPeakPnl !== null ? str(data.avgPeakPnl, "%") : "N/A",
23292
+ format: (data) => data.avgPeakPnl !== null ? `${data.avgPeakPnl.toFixed(2)}%` : "N/A",
22996
23293
  isVisible: () => true,
22997
23294
  },
22998
23295
  {
22999
23296
  key: "avgFallPnl",
23000
23297
  label: "Avg DD PNL",
23001
- format: (data) => data.avgFallPnl !== null ? str(data.avgFallPnl, "%") : "N/A",
23298
+ format: (data) => data.avgFallPnl !== null ? `${data.avgFallPnl.toFixed(2)}%` : "N/A",
23002
23299
  isVisible: () => true,
23003
23300
  },
23004
23301
  {
23005
23302
  key: "peakProfitPnl",
23006
23303
  label: "Peak Profit PNL",
23007
- format: (data) => data.peakProfitPnl !== null ? str(data.peakProfitPnl, "%") : "N/A",
23304
+ format: (data) => data.peakProfitPnl !== null ? `${data.peakProfitPnl.toFixed(2)}%` : "N/A",
23008
23305
  isVisible: () => true,
23009
23306
  },
23010
23307
  {
23011
23308
  key: "maxDrawdownPnl",
23012
23309
  label: "Max DD PNL",
23013
- format: (data) => data.maxDrawdownPnl !== null ? str(data.maxDrawdownPnl, "%") : "N/A",
23310
+ format: (data) => data.maxDrawdownPnl !== null ? `${data.maxDrawdownPnl.toFixed(2)}%` : "N/A",
23014
23311
  isVisible: () => true,
23015
23312
  },
23016
23313
  {
23017
23314
  key: "medianPnl",
23018
23315
  label: "Median PNL",
23019
- format: (data) => data.medianPnl !== null ? str(data.medianPnl, "%") : "N/A",
23316
+ format: (data) => data.medianPnl !== null ? `${data.medianPnl.toFixed(2)}%` : "N/A",
23020
23317
  isVisible: () => true,
23021
23318
  },
23022
23319
  {
@@ -23041,7 +23338,7 @@ const heat_columns = [
23041
23338
  key: "avgConsecutiveWinPnl",
23042
23339
  label: "Avg Win Streak PNL",
23043
23340
  format: (data) => data.avgConsecutiveWinPnl !== null
23044
- ? str(data.avgConsecutiveWinPnl, "%")
23341
+ ? `${data.avgConsecutiveWinPnl.toFixed(2)}%`
23045
23342
  : "N/A",
23046
23343
  isVisible: () => true,
23047
23344
  },
@@ -23049,7 +23346,7 @@ const heat_columns = [
23049
23346
  key: "avgConsecutiveLossPnl",
23050
23347
  label: "Avg Loss Streak PNL",
23051
23348
  format: (data) => data.avgConsecutiveLossPnl !== null
23052
- ? str(data.avgConsecutiveLossPnl, "%")
23349
+ ? `${data.avgConsecutiveLossPnl.toFixed(2)}%`
23053
23350
  : "N/A",
23054
23351
  isVisible: () => true,
23055
23352
  },
@@ -23062,7 +23359,7 @@ const heat_columns = [
23062
23359
  {
23063
23360
  key: "trendStrength",
23064
23361
  label: "Trend %/d",
23065
- format: (data) => data.trendStrength !== null ? str(data.trendStrength, "%") : "N/A",
23362
+ format: (data) => data.trendStrength !== null ? `${data.trendStrength.toFixed(2)}%` : "N/A",
23066
23363
  isVisible: () => true,
23067
23364
  },
23068
23365
  {
@@ -23115,25 +23412,25 @@ const heat_columns = [
23115
23412
  {
23116
23413
  key: "medianStepSize",
23117
23414
  label: "Median Step",
23118
- format: (data) => data.medianStepSize !== null ? str(data.medianStepSize, "%") : "N/A",
23415
+ format: (data) => data.medianStepSize !== null ? `${data.medianStepSize.toFixed(2)}%` : "N/A",
23119
23416
  isVisible: () => true,
23120
23417
  },
23121
23418
  {
23122
23419
  key: "sortinoRatio",
23123
23420
  label: "Sortino",
23124
- format: (data) => data.sortinoRatio !== null ? str(data.sortinoRatio) : "N/A",
23421
+ format: (data) => data.sortinoRatio !== null ? data.sortinoRatio.toFixed(3) : "N/A",
23125
23422
  isVisible: () => true,
23126
23423
  },
23127
23424
  {
23128
23425
  key: "calmarRatio",
23129
23426
  label: "Calmar",
23130
- format: (data) => data.calmarRatio !== null ? str(data.calmarRatio) : "N/A",
23427
+ format: (data) => data.calmarRatio !== null ? data.calmarRatio.toFixed(3) : "N/A",
23131
23428
  isVisible: () => true,
23132
23429
  },
23133
23430
  {
23134
23431
  key: "recoveryFactor",
23135
23432
  label: "Recovery",
23136
- format: (data) => data.recoveryFactor !== null ? str(data.recoveryFactor) : "N/A",
23433
+ format: (data) => data.recoveryFactor !== null ? data.recoveryFactor.toFixed(3) : "N/A",
23137
23434
  isVisible: () => true,
23138
23435
  },
23139
23436
  ];
@@ -30309,28 +30606,28 @@ class HeatmapStorage {
30309
30606
  `# Portfolio Heatmap: ${strategyName}`,
30310
30607
  "",
30311
30608
  `**Total Symbols:** ${data.totalSymbols}`,
30312
- `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"}`,
30313
- `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"}`,
30314
- `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? str(data.portfolioAnnualizedSharpeRatio) : "N/A"}`,
30315
- `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? str(data.portfolioCertaintyRatio) : "N/A"}`,
30316
- `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? str(data.portfolioExpectedYearlyReturns, "%") : "N/A"}`,
30609
+ `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? `${data.portfolioTotalPnl.toFixed(2)} %` : "N/A"}`,
30610
+ `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? data.portfolioSharpeRatio.toFixed(3) : "N/A"}`,
30611
+ `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? data.portfolioAnnualizedSharpeRatio.toFixed(3) : "N/A"}`,
30612
+ `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? data.portfolioCertaintyRatio.toFixed(3) : "N/A"}`,
30613
+ `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? `${data.portfolioExpectedYearlyReturns.toFixed(2)} %` : "N/A"}`,
30317
30614
  `**Trades Per Year:** ${data.portfolioTradesPerYear !== null ? data.portfolioTradesPerYear.toFixed(1) : "N/A"}`,
30318
30615
  `**Total Trades:** ${data.portfolioTotalTrades}`,
30319
- `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"}`,
30320
- `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
30321
- `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? str(data.portfolioPeakProfitPnl, "%") : "N/A"}`,
30322
- `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? str(data.portfolioMaxDrawdownPnl, "%") : "N/A"}`,
30323
- `**Median PNL:** ${data.portfolioMedianPnl !== null ? str(data.portfolioMedianPnl, "%") : "N/A"}`,
30616
+ `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? `${data.portfolioAvgPeakPnl.toFixed(2)} %` : "N/A"}`,
30617
+ `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? `${data.portfolioAvgFallPnl.toFixed(2)} %` : "N/A"}`,
30618
+ `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? `${data.portfolioPeakProfitPnl.toFixed(2)} %` : "N/A"}`,
30619
+ `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? `${data.portfolioMaxDrawdownPnl.toFixed(2)} %` : "N/A"}`,
30620
+ `**Median PNL:** ${data.portfolioMedianPnl !== null ? `${data.portfolioMedianPnl.toFixed(2)} %` : "N/A"}`,
30324
30621
  `**Avg Duration:** ${data.portfolioAvgDuration !== null ? `${data.portfolioAvgDuration.toFixed(1)} min` : "N/A"}`,
30325
30622
  `**Avg Win Duration:** ${data.portfolioAvgWinDuration !== null ? `${data.portfolioAvgWinDuration.toFixed(1)} min` : "N/A"}`,
30326
30623
  `**Avg Loss Duration:** ${data.portfolioAvgLossDuration !== null ? `${data.portfolioAvgLossDuration.toFixed(1)} min` : "N/A"}`,
30327
- `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? str(data.portfolioAvgConsecutiveWinPnl, "%") : "N/A"}`,
30328
- `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? str(data.portfolioAvgConsecutiveLossPnl, "%") : "N/A"}`,
30329
- `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? str(data.portfolioStdDev, "%") : "N/A"}`,
30330
- `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? str(data.portfolioSortinoRatio) : "N/A"}`,
30331
- `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? str(data.portfolioCalmarRatio) : "N/A"}`,
30332
- `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? str(data.portfolioRecoveryFactor) : "N/A"}`,
30333
- `**Expectancy:** ${data.portfolioExpectancy !== null ? str(data.portfolioExpectancy, "%") : "N/A"}`,
30624
+ `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? `${data.portfolioAvgConsecutiveWinPnl.toFixed(2)} %` : "N/A"}`,
30625
+ `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? `${data.portfolioAvgConsecutiveLossPnl.toFixed(2)} %` : "N/A"}`,
30626
+ `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? `${data.portfolioStdDev.toFixed(2)} %` : "N/A"}`,
30627
+ `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? data.portfolioSortinoRatio.toFixed(3) : "N/A"}`,
30628
+ `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? data.portfolioCalmarRatio.toFixed(3) : "N/A"}`,
30629
+ `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? data.portfolioRecoveryFactor.toFixed(3) : "N/A"}`,
30630
+ `**Expectancy:** ${data.portfolioExpectancy !== null ? `${data.portfolioExpectancy.toFixed(2)} %` : "N/A"}`,
30334
30631
  "",
30335
30632
  table,
30336
30633
  "",
@@ -67632,4 +67929,4 @@ const validateSignal = (signal, currentPrice) => {
67632
67929
  return !errors.length;
67633
67930
  };
67634
67931
 
67635
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
67932
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, PersistStrategyAdapter, PersistStrategyInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };