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.cjs CHANGED
@@ -1001,6 +1001,12 @@ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writ
1001
1001
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
1002
1002
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
1003
1003
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR = "PersistScheduleUtils.clear";
1004
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER = "PersistStrategyUtils.usePersistStrategyAdapter";
1005
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA = "PersistStrategyUtils.readStrategyData";
1006
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA = "PersistStrategyUtils.writeStrategyData";
1007
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON = "PersistStrategyUtils.useJson";
1008
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY = "PersistStrategyUtils.useDummy";
1009
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR = "PersistStrategyUtils.clear";
1004
1010
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
1005
1011
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
1006
1012
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
@@ -1864,6 +1870,212 @@ class PersistScheduleUtils {
1864
1870
  * ```
1865
1871
  */
1866
1872
  const PersistScheduleAdapter = new PersistScheduleUtils();
1873
+ /**
1874
+ * Default file-based implementation of IPersistStrategyInstance.
1875
+ *
1876
+ * Features:
1877
+ * - Wraps PersistBase for atomic JSON writes
1878
+ * - Uses fixed entity ID "strategy" within a per-context PersistBase
1879
+ * - Crash-safe via atomic writes
1880
+ *
1881
+ * @example
1882
+ * ```typescript
1883
+ * const instance = new PersistStrategyInstance("BTCUSDT", "my-strategy", "binance");
1884
+ * await instance.waitForInit(true);
1885
+ * await instance.writeStrategyData(snapshot);
1886
+ * const restored = await instance.readStrategyData();
1887
+ * ```
1888
+ */
1889
+ class PersistStrategyInstance {
1890
+ /**
1891
+ * Creates new deferred strategy state persistence instance.
1892
+ *
1893
+ * @param symbol - Trading pair symbol
1894
+ * @param strategyName - Strategy identifier
1895
+ * @param exchangeName - Exchange identifier
1896
+ */
1897
+ constructor(symbol, strategyName, exchangeName) {
1898
+ this.symbol = symbol;
1899
+ this.strategyName = strategyName;
1900
+ this.exchangeName = exchangeName;
1901
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/strategy/`);
1902
+ }
1903
+ /**
1904
+ * Initializes the underlying PersistBase storage.
1905
+ *
1906
+ * @param initial - Whether this is the first initialization
1907
+ * @returns Promise that resolves when initialization is complete
1908
+ */
1909
+ async waitForInit(initial) {
1910
+ await this._storage.waitForInit(initial);
1911
+ }
1912
+ /**
1913
+ * Reads the persisted strategy state snapshot using the fixed STORAGE_KEY.
1914
+ *
1915
+ * @returns Promise resolving to strategy state snapshot or null if not found
1916
+ */
1917
+ async readStrategyData() {
1918
+ if (await this._storage.hasValue(PersistStrategyInstance.STORAGE_KEY)) {
1919
+ const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
1920
+ // JSON serializes Infinity as null, so an eternal-hold signal
1921
+ // (minuteEstimatedTime: Infinity) reads back as null — restore it.
1922
+ for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
1923
+ if (signal && signal.minuteEstimatedTime == null) {
1924
+ signal.minuteEstimatedTime = Infinity;
1925
+ }
1926
+ }
1927
+ return strategyData;
1928
+ }
1929
+ return null;
1930
+ }
1931
+ /**
1932
+ * Writes the strategy state snapshot (or null to clear) using the fixed STORAGE_KEY.
1933
+ *
1934
+ * @param row - Strategy state snapshot to persist, or null to clear
1935
+ * @returns Promise that resolves when write is complete
1936
+ */
1937
+ async writeStrategyData(row) {
1938
+ await this._storage.writeValue(PersistStrategyInstance.STORAGE_KEY, row);
1939
+ }
1940
+ }
1941
+ /** Fixed entity key for storing the strategy state snapshot */
1942
+ PersistStrategyInstance.STORAGE_KEY = "strategy";
1943
+ /**
1944
+ * No-op IPersistStrategyInstance implementation used by PersistStrategyUtils.useDummy().
1945
+ * All reads return null, all writes are discarded.
1946
+ */
1947
+ class PersistStrategyDummyInstance {
1948
+ /**
1949
+ * No-op constructor.
1950
+ * Context arguments are accepted to satisfy TPersistStrategyInstanceCtor.
1951
+ */
1952
+ constructor(_symbol, _strategyName, _exchangeName) { }
1953
+ /**
1954
+ * No-op initialization.
1955
+ * @returns Promise that resolves immediately
1956
+ */
1957
+ async waitForInit(_initial) { }
1958
+ /**
1959
+ * Always returns null (no persisted strategy state).
1960
+ * @returns Promise resolving to null
1961
+ */
1962
+ async readStrategyData() { return null; }
1963
+ /**
1964
+ * No-op write (discards strategy state).
1965
+ * @returns Promise that resolves immediately
1966
+ */
1967
+ async writeStrategyData(_row) { }
1968
+ }
1969
+ /**
1970
+ * Utility class for managing deferred strategy state persistence.
1971
+ *
1972
+ * Features:
1973
+ * - Memoized storage instances per strategy
1974
+ * - Custom adapter support
1975
+ * - Atomic read/write operations for the deferred state snapshot
1976
+ * - Crash-safe in-flight broker operation state management
1977
+ *
1978
+ * Used by ClientStrategy for live mode persistence of the commit queue and deferred
1979
+ * user actions (_commitQueue, _closedSignal, _cancelledSignal, _activatedSignal).
1980
+ */
1981
+ class PersistStrategyUtils {
1982
+ constructor() {
1983
+ /**
1984
+ * Constructor used to create per-context strategy state instances.
1985
+ * Replaceable via usePersistStrategyAdapter() / useJson() / useDummy().
1986
+ */
1987
+ this.PersistStrategyInstanceCtor = PersistStrategyInstance;
1988
+ /**
1989
+ * Memoized factory creating one IPersistStrategyInstance per (symbol, strategy, exchange) triple.
1990
+ */
1991
+ this.getStrategyStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistStrategyInstanceCtor, [symbol, strategyName, exchangeName]));
1992
+ /**
1993
+ * Reads persisted deferred strategy state for the given context.
1994
+ * Lazily initializes the instance on first access.
1995
+ *
1996
+ * @param symbol - Trading pair symbol
1997
+ * @param strategyName - Strategy identifier
1998
+ * @param exchangeName - Exchange identifier
1999
+ * @returns Promise resolving to strategy state snapshot or null if none persisted
2000
+ */
2001
+ this.readStrategyData = async (symbol, strategyName, exchangeName) => {
2002
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA);
2003
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
2004
+ const isInitial = !this.getStrategyStorage.has(key);
2005
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
2006
+ await instance.waitForInit(isInitial);
2007
+ return instance.readStrategyData();
2008
+ };
2009
+ /**
2010
+ * Writes deferred strategy state (or null to clear) for the given context.
2011
+ * Lazily initializes the instance on first access.
2012
+ *
2013
+ * @param strategyRow - Strategy state snapshot to persist, or null to clear
2014
+ * @param symbol - Trading pair symbol
2015
+ * @param strategyName - Strategy identifier
2016
+ * @param exchangeName - Exchange identifier
2017
+ * @returns Promise that resolves when write is complete
2018
+ */
2019
+ this.writeStrategyData = async (strategyRow, symbol, strategyName, exchangeName) => {
2020
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA);
2021
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
2022
+ const isInitial = !this.getStrategyStorage.has(key);
2023
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
2024
+ await instance.waitForInit(isInitial);
2025
+ return instance.writeStrategyData(strategyRow);
2026
+ };
2027
+ }
2028
+ /**
2029
+ * Registers a custom IPersistStrategyInstance constructor.
2030
+ * Clears the memoization cache so subsequent calls use the new adapter.
2031
+ *
2032
+ * @param Ctor - Custom IPersistStrategyInstance constructor
2033
+ */
2034
+ usePersistStrategyAdapter(Ctor) {
2035
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER);
2036
+ this.PersistStrategyInstanceCtor = Ctor;
2037
+ this.getStrategyStorage.clear();
2038
+ }
2039
+ /**
2040
+ * Clears the memoized instance cache.
2041
+ * Call when process.cwd() changes between strategy iterations.
2042
+ */
2043
+ clear() {
2044
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR);
2045
+ this.getStrategyStorage.clear();
2046
+ }
2047
+ /**
2048
+ * Switches to the default file-based PersistStrategyInstance.
2049
+ */
2050
+ useJson() {
2051
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON);
2052
+ this.usePersistStrategyAdapter(PersistStrategyInstance);
2053
+ }
2054
+ /**
2055
+ * Switches to PersistStrategyDummyInstance (all operations are no-ops).
2056
+ */
2057
+ useDummy() {
2058
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY);
2059
+ this.usePersistStrategyAdapter(PersistStrategyDummyInstance);
2060
+ }
2061
+ }
2062
+ /**
2063
+ * Global singleton instance of PersistStrategyUtils.
2064
+ * Used by ClientStrategy for deferred strategy state persistence.
2065
+ *
2066
+ * @example
2067
+ * ```typescript
2068
+ * // Custom adapter
2069
+ * PersistStrategyAdapter.usePersistStrategyAdapter(RedisPersist);
2070
+ *
2071
+ * // Read strategy state
2072
+ * const state = await PersistStrategyAdapter.readStrategyData("BTCUSDT", "my-strategy", "binance");
2073
+ *
2074
+ * // Write strategy state
2075
+ * await PersistStrategyAdapter.writeStrategyData(state, "BTCUSDT", "my-strategy", "binance");
2076
+ * ```
2077
+ */
2078
+ const PersistStrategyAdapter = new PersistStrategyUtils();
1867
2079
  /**
1868
2080
  * Default file-based implementation of IPersistPartialInstance.
1869
2081
  *
@@ -6791,6 +7003,9 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
6791
7003
  {
6792
7004
  self._commitQueue = [];
6793
7005
  }
7006
+ // Persist the now-empty queue so a crash after draining does not replay commits
7007
+ // that were already forwarded to the broker on the next restart.
7008
+ await PERSIST_STRATEGY_FN(self);
6794
7009
  if (!self._pendingSignal) {
6795
7010
  return;
6796
7011
  }
@@ -7247,6 +7462,19 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7247
7462
  self._lastPendingId = recentSignal.id;
7248
7463
  }
7249
7464
  }
7465
+ // Restore deferred strategy state (commit queue + deferred user actions) so any
7466
+ // confirmed-but-not-yet-forwarded broker operation survives a live crash and is
7467
+ // re-drained on the next tick. Placed before the pending/scheduled restore which
7468
+ // may early-return on exchange/strategy mismatch.
7469
+ {
7470
+ const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7471
+ if (strategyData) {
7472
+ self._commitQueue = strategyData.commitQueue ?? [];
7473
+ self._closedSignal = strategyData.closedSignal;
7474
+ self._cancelledSignal = strategyData.cancelledSignal;
7475
+ self._activatedSignal = strategyData.activatedSignal;
7476
+ }
7477
+ }
7250
7478
  // Restore pending signal
7251
7479
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7252
7480
  if (pendingSignal) {
@@ -7296,6 +7524,28 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
7296
7524
  self.params.logger.debug("ClientStrategy dispose");
7297
7525
  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);
7298
7526
  };
7527
+ /**
7528
+ * Persists the deferred strategy state snapshot (commit queue + deferred user actions)
7529
+ * to disk in live mode.
7530
+ *
7531
+ * These fields carry confirmed-but-not-yet-forwarded broker operations and survive the
7532
+ * gap between ticks (drained at the start of the next tick). Without persistence a live
7533
+ * crash in that window silently loses the pending broker operation while _pendingSignal
7534
+ * (already mutated and saved) claims it happened. Skipped in backtest mode.
7535
+ *
7536
+ * @param self - ClientStrategy instance
7537
+ */
7538
+ const PERSIST_STRATEGY_FN = async (self) => {
7539
+ if (self.params.backtest) {
7540
+ return;
7541
+ }
7542
+ await PersistStrategyAdapter.writeStrategyData({
7543
+ commitQueue: self._commitQueue,
7544
+ closedSignal: self._closedSignal,
7545
+ cancelledSignal: self._cancelledSignal,
7546
+ activatedSignal: self._activatedSignal,
7547
+ }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7548
+ };
7299
7549
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
7300
7550
  // Initialize partial array if not present
7301
7551
  if (!signal._partial)
@@ -8920,14 +9170,15 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
8920
9170
  const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
8921
9171
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
8922
9172
  if (!syncCloseAllowed) {
8923
- self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
9173
+ // Sync close rejected (e.g. broker rejected the order) keep _closedSignal intact
9174
+ // and return null so the candle loop re-attempts on the next candle. Mirrors live
9175
+ // tick, which keeps _closedSignal and returns idle on a rejected user close,
9176
+ // re-trying on the following tick.
9177
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry on next candle", {
8924
9178
  symbol: self.params.execution.context.symbol,
8925
9179
  signalId: closedSignal.id,
8926
9180
  });
8927
- self._closedSignal = null;
8928
- self._pendingSignal = closedSignal;
8929
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
8930
- `Retry backtest() with new candle data.`);
9181
+ return null;
8931
9182
  }
8932
9183
  self._closedSignal = null;
8933
9184
  const publicSignal = TO_PUBLIC_SIGNAL("pending", closedSignal, averagePrice);
@@ -9179,7 +9430,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9179
9430
  }
9180
9431
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
9181
9432
  if (self._closedSignal) {
9182
- return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9433
+ const userCloseResult = await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9434
+ // Sync close accepted — position closed, return. If rejected, _closedSignal is
9435
+ // kept and we skip this candle: live tick returns idle on a rejected user close
9436
+ // (it does NOT fall into the TP/SL or active-monitoring path), so the candle is
9437
+ // skipped here and the close is re-attempted on the next candle.
9438
+ if (userCloseResult) {
9439
+ return userCloseResult;
9440
+ }
9441
+ continue;
9183
9442
  }
9184
9443
  let shouldClose = false;
9185
9444
  let closeReason;
@@ -9230,7 +9489,19 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9230
9489
  else {
9231
9490
  closePrice = averagePrice; // time_expired uses VWAP
9232
9491
  }
9233
- return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9492
+ const closeResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9493
+ // Sync close accepted — position closed, return.
9494
+ if (closeResult) {
9495
+ return closeResult;
9496
+ }
9497
+ // Sync close rejected (e.g. broker rejected the order) — _pendingSignal is left
9498
+ // intact (not cleared on rejection). Do NOT return/continue: fall through to the
9499
+ // active-monitoring block below so this candle is processed exactly like live
9500
+ // tick, which runs RETURN_PENDING_SIGNAL_ACTIVE_FN when CLOSE_PENDING_SIGNAL_FN
9501
+ // returns null (updates _peak/_fall, fires active ping / breakeven / partial
9502
+ // callbacks, drains the commit queue). The close is re-attempted on the next
9503
+ // candle; for time_expired this eventually reaches the loop-exhausted close
9504
+ // (which throws if still rejected).
9234
9505
  }
9235
9506
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
9236
9507
  // Calculate percentage of path to TP/SL
@@ -9652,7 +9923,7 @@ class ClientStrategy {
9652
9923
  async getStopped(symbol) {
9653
9924
  this.params.logger.debug("ClientStrategy getStopped", {
9654
9925
  symbol,
9655
- strategyName: this.params.method.context.strategyName,
9926
+ strategyName: this.params.strategyName,
9656
9927
  });
9657
9928
  return this._isStopped;
9658
9929
  }
@@ -10335,6 +10606,8 @@ class ClientStrategy {
10335
10606
  if (this._cancelledSignal) {
10336
10607
  const cancelledSignal = this._cancelledSignal;
10337
10608
  this._cancelledSignal = null; // Clear after emitting
10609
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10610
+ await PERSIST_STRATEGY_FN(this);
10338
10611
  this.params.logger.info("ClientStrategy tick: scheduled signal was cancelled", {
10339
10612
  symbol: this.params.execution.context.symbol,
10340
10613
  signalId: cancelledSignal.id,
@@ -10393,6 +10666,8 @@ class ClientStrategy {
10393
10666
  return await RETURN_IDLE_FN(this, currentPrice);
10394
10667
  }
10395
10668
  this._closedSignal = null; // Clear only after sync confirmed
10669
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10670
+ await PERSIST_STRATEGY_FN(this);
10396
10671
  this.params.logger.info("ClientStrategy tick: pending signal was closed", {
10397
10672
  symbol: this.params.execution.context.symbol,
10398
10673
  signalId: closedSignal.id,
@@ -10448,6 +10723,8 @@ class ClientStrategy {
10448
10723
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
10449
10724
  const activatedSignal = this._activatedSignal;
10450
10725
  this._activatedSignal = null; // Clear after emitting
10726
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10727
+ await PERSIST_STRATEGY_FN(this);
10451
10728
  this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
10452
10729
  symbol: this.params.execution.context.symbol,
10453
10730
  signalId: activatedSignal.id,
@@ -10705,18 +10982,20 @@ class ClientStrategy {
10705
10982
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
10706
10983
  const closedSignal = this._closedSignal;
10707
10984
  const closeTimestamp = this.params.execution.context.when.getTime();
10708
- // Sync close: if external system rejects — restore _pendingSignal, retry on next backtest() call
10985
+ // Sync close: if external system rejects — keep the close pending and re-attempt
10986
+ // it inside the candle loop below (PROCESS_PENDING_SIGNAL_CANDLES_FN handles
10987
+ // _closedSignal per candle). Mirrors live tick, which keeps _closedSignal and
10988
+ // retries on the next tick instead of failing.
10709
10989
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, currentPrice, "closed", closedSignal, this);
10710
10990
  if (!syncCloseAllowed) {
10711
- this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
10991
+ this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry in candle loop", {
10712
10992
  symbol: this.params.execution.context.symbol,
10713
10993
  signalId: closedSignal.id,
10714
10994
  });
10715
- // Restore _pendingSignal so next backtest() call can process it normally
10716
- this._closedSignal = null;
10995
+ // Restore _pendingSignal so the candle loop processes the position normally;
10996
+ // _closedSignal is kept so the loop re-attempts the close on each candle.
10717
10997
  this._pendingSignal = closedSignal;
10718
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
10719
- `Retry backtest() with new candle data.`);
10998
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, this._pendingSignal, candles, frameEndTime);
10720
10999
  }
10721
11000
  this._closedSignal = null; // Clear only after sync confirmed
10722
11001
  // Emit commit with correct timestamp from backtest context
@@ -10898,7 +11177,7 @@ class ClientStrategy {
10898
11177
  if (backtest) {
10899
11178
  return;
10900
11179
  }
10901
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11180
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
10902
11181
  }
10903
11182
  /**
10904
11183
  * Cancels the scheduled signal without stopping the strategy.
@@ -10942,7 +11221,9 @@ class ClientStrategy {
10942
11221
  // Commit will be emitted in backtest() with correct candle timestamp
10943
11222
  return;
10944
11223
  }
10945
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11224
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11225
+ // Persist deferred _cancelledSignal so a crash before the next tick does not lose it
11226
+ await PERSIST_STRATEGY_FN(this);
10946
11227
  // Commit will be emitted in tick() with correct currentTime
10947
11228
  }
10948
11229
  /**
@@ -10993,7 +11274,9 @@ class ClientStrategy {
10993
11274
  // Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
10994
11275
  return;
10995
11276
  }
10996
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11277
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11278
+ // Persist deferred _activatedSignal so a crash before the next tick does not lose it
11279
+ await PERSIST_STRATEGY_FN(this);
10997
11280
  // Commit will be emitted AFTER successful risk check in tick()
10998
11281
  }
10999
11282
  /**
@@ -11038,6 +11321,8 @@ class ClientStrategy {
11038
11321
  return;
11039
11322
  }
11040
11323
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11324
+ // Persist deferred _closedSignal so a crash before the next tick does not lose it
11325
+ await PERSIST_STRATEGY_FN(this);
11041
11326
  // Commit will be emitted in tick() with correct currentTime
11042
11327
  }
11043
11328
  /**
@@ -11208,10 +11493,10 @@ class ClientStrategy {
11208
11493
  });
11209
11494
  // Call onWrite callback for testing persist storage
11210
11495
  if (this.params.callbacks?.onWrite) {
11211
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11496
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11212
11497
  }
11213
11498
  if (!backtest) {
11214
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11499
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11215
11500
  }
11216
11501
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11217
11502
  this._commitQueue.push({
@@ -11221,6 +11506,8 @@ class ClientStrategy {
11221
11506
  percentToClose,
11222
11507
  currentPrice,
11223
11508
  });
11509
+ // Persist the queued commit so a crash before the next tick does not lose it
11510
+ await PERSIST_STRATEGY_FN(this);
11224
11511
  return true;
11225
11512
  }
11226
11513
  /**
@@ -11391,10 +11678,10 @@ class ClientStrategy {
11391
11678
  });
11392
11679
  // Call onWrite callback for testing persist storage
11393
11680
  if (this.params.callbacks?.onWrite) {
11394
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11681
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11395
11682
  }
11396
11683
  if (!backtest) {
11397
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11684
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11398
11685
  }
11399
11686
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11400
11687
  this._commitQueue.push({
@@ -11404,6 +11691,8 @@ class ClientStrategy {
11404
11691
  percentToClose,
11405
11692
  currentPrice,
11406
11693
  });
11694
+ // Persist the queued commit so a crash before the next tick does not lose it
11695
+ await PERSIST_STRATEGY_FN(this);
11407
11696
  return true;
11408
11697
  }
11409
11698
  /**
@@ -11590,10 +11879,10 @@ class ClientStrategy {
11590
11879
  // Call onWrite callback for testing persist storage
11591
11880
  if (this.params.callbacks?.onWrite) {
11592
11881
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11593
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
11882
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11594
11883
  }
11595
11884
  if (!backtest) {
11596
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11885
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11597
11886
  }
11598
11887
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11599
11888
  this._commitQueue.push({
@@ -11602,6 +11891,8 @@ class ClientStrategy {
11602
11891
  backtest,
11603
11892
  currentPrice,
11604
11893
  });
11894
+ // Persist the queued commit so a crash before the next tick does not lose it
11895
+ await PERSIST_STRATEGY_FN(this);
11605
11896
  return true;
11606
11897
  }
11607
11898
  /**
@@ -11839,10 +12130,10 @@ class ClientStrategy {
11839
12130
  // Call onWrite callback for testing persist storage
11840
12131
  if (this.params.callbacks?.onWrite) {
11841
12132
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11842
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12133
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11843
12134
  }
11844
12135
  if (!backtest) {
11845
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12136
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11846
12137
  }
11847
12138
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11848
12139
  this._commitQueue.push({
@@ -11852,6 +12143,8 @@ class ClientStrategy {
11852
12143
  percentShift,
11853
12144
  currentPrice,
11854
12145
  });
12146
+ // Persist the queued commit so a crash before the next tick does not lose it
12147
+ await PERSIST_STRATEGY_FN(this);
11855
12148
  return true;
11856
12149
  }
11857
12150
  /**
@@ -12075,10 +12368,10 @@ class ClientStrategy {
12075
12368
  // Call onWrite callback for testing persist storage
12076
12369
  if (this.params.callbacks?.onWrite) {
12077
12370
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
12078
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12371
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
12079
12372
  }
12080
12373
  if (!backtest) {
12081
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12374
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12082
12375
  }
12083
12376
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12084
12377
  this._commitQueue.push({
@@ -12088,6 +12381,8 @@ class ClientStrategy {
12088
12381
  percentShift,
12089
12382
  currentPrice,
12090
12383
  });
12384
+ // Persist the queued commit so a crash before the next tick does not lose it
12385
+ await PERSIST_STRATEGY_FN(this);
12091
12386
  return true;
12092
12387
  }
12093
12388
  /**
@@ -12166,10 +12461,10 @@ class ClientStrategy {
12166
12461
  });
12167
12462
  // Call onWrite callback for testing persist storage
12168
12463
  if (this.params.callbacks?.onWrite) {
12169
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12464
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12170
12465
  }
12171
12466
  if (!backtest) {
12172
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12467
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12173
12468
  }
12174
12469
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12175
12470
  this._commitQueue.push({
@@ -12180,6 +12475,8 @@ class ClientStrategy {
12180
12475
  cost,
12181
12476
  totalEntries: this._pendingSignal._entry?.length ?? 1,
12182
12477
  });
12478
+ // Persist the queued commit so a crash before the next tick does not lose it
12479
+ await PERSIST_STRATEGY_FN(this);
12183
12480
  return true;
12184
12481
  }
12185
12482
  }
@@ -22920,32 +23217,32 @@ const heat_columns = [
22920
23217
  {
22921
23218
  key: "totalPnl",
22922
23219
  label: "Total PNL",
22923
- format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
23220
+ format: (data) => data.totalPnl !== null ? `${data.totalPnl.toFixed(2)}%` : "N/A",
22924
23221
  isVisible: () => true,
22925
23222
  },
22926
23223
  {
22927
23224
  key: "sharpeRatio",
22928
23225
  label: "Sharpe",
22929
- format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
23226
+ format: (data) => data.sharpeRatio !== null ? data.sharpeRatio.toFixed(3) : "N/A",
22930
23227
  isVisible: () => true,
22931
23228
  },
22932
23229
  {
22933
23230
  key: "annualizedSharpeRatio",
22934
23231
  label: "Ann Sharpe",
22935
- format: (data) => data.annualizedSharpeRatio !== null ? functoolsKit.str(data.annualizedSharpeRatio) : "N/A",
23232
+ format: (data) => data.annualizedSharpeRatio !== null ? data.annualizedSharpeRatio.toFixed(3) : "N/A",
22936
23233
  isVisible: () => true,
22937
23234
  },
22938
23235
  {
22939
23236
  key: "certaintyRatio",
22940
23237
  label: "Certainty",
22941
- format: (data) => data.certaintyRatio !== null ? functoolsKit.str(data.certaintyRatio) : "N/A",
23238
+ format: (data) => data.certaintyRatio !== null ? data.certaintyRatio.toFixed(3) : "N/A",
22942
23239
  isVisible: () => true,
22943
23240
  },
22944
23241
  {
22945
23242
  key: "expectedYearlyReturns",
22946
23243
  label: "Exp Yearly",
22947
23244
  format: (data) => data.expectedYearlyReturns !== null
22948
- ? functoolsKit.str(data.expectedYearlyReturns, "%")
23245
+ ? `${data.expectedYearlyReturns.toFixed(2)}%`
22949
23246
  : "N/A",
22950
23247
  isVisible: () => true,
22951
23248
  },
@@ -22958,37 +23255,37 @@ const heat_columns = [
22958
23255
  {
22959
23256
  key: "profitFactor",
22960
23257
  label: "PF",
22961
- format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
23258
+ format: (data) => data.profitFactor !== null ? data.profitFactor.toFixed(3) : "N/A",
22962
23259
  isVisible: () => true,
22963
23260
  },
22964
23261
  {
22965
23262
  key: "expectancy",
22966
23263
  label: "Expect",
22967
- format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
23264
+ format: (data) => data.expectancy !== null ? `${data.expectancy.toFixed(2)}%` : "N/A",
22968
23265
  isVisible: () => true,
22969
23266
  },
22970
23267
  {
22971
23268
  key: "winRate",
22972
23269
  label: "WR",
22973
- format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
23270
+ format: (data) => data.winRate !== null ? `${data.winRate.toFixed(2)}%` : "N/A",
22974
23271
  isVisible: () => true,
22975
23272
  },
22976
23273
  {
22977
23274
  key: "avgWin",
22978
23275
  label: "Avg Win",
22979
- format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
23276
+ format: (data) => data.avgWin !== null ? `${data.avgWin.toFixed(2)}%` : "N/A",
22980
23277
  isVisible: () => true,
22981
23278
  },
22982
23279
  {
22983
23280
  key: "avgLoss",
22984
23281
  label: "Avg Loss",
22985
- format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
23282
+ format: (data) => data.avgLoss !== null ? `${data.avgLoss.toFixed(2)}%` : "N/A",
22986
23283
  isVisible: () => true,
22987
23284
  },
22988
23285
  {
22989
23286
  key: "maxDrawdown",
22990
23287
  label: "Max DD",
22991
- format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
23288
+ format: (data) => data.maxDrawdown !== null ? `${(-data.maxDrawdown).toFixed(2)}%` : "N/A",
22992
23289
  isVisible: () => true,
22993
23290
  },
22994
23291
  {
@@ -23012,31 +23309,31 @@ const heat_columns = [
23012
23309
  {
23013
23310
  key: "avgPeakPnl",
23014
23311
  label: "Avg Peak PNL",
23015
- format: (data) => data.avgPeakPnl !== null ? functoolsKit.str(data.avgPeakPnl, "%") : "N/A",
23312
+ format: (data) => data.avgPeakPnl !== null ? `${data.avgPeakPnl.toFixed(2)}%` : "N/A",
23016
23313
  isVisible: () => true,
23017
23314
  },
23018
23315
  {
23019
23316
  key: "avgFallPnl",
23020
23317
  label: "Avg DD PNL",
23021
- format: (data) => data.avgFallPnl !== null ? functoolsKit.str(data.avgFallPnl, "%") : "N/A",
23318
+ format: (data) => data.avgFallPnl !== null ? `${data.avgFallPnl.toFixed(2)}%` : "N/A",
23022
23319
  isVisible: () => true,
23023
23320
  },
23024
23321
  {
23025
23322
  key: "peakProfitPnl",
23026
23323
  label: "Peak Profit PNL",
23027
- format: (data) => data.peakProfitPnl !== null ? functoolsKit.str(data.peakProfitPnl, "%") : "N/A",
23324
+ format: (data) => data.peakProfitPnl !== null ? `${data.peakProfitPnl.toFixed(2)}%` : "N/A",
23028
23325
  isVisible: () => true,
23029
23326
  },
23030
23327
  {
23031
23328
  key: "maxDrawdownPnl",
23032
23329
  label: "Max DD PNL",
23033
- format: (data) => data.maxDrawdownPnl !== null ? functoolsKit.str(data.maxDrawdownPnl, "%") : "N/A",
23330
+ format: (data) => data.maxDrawdownPnl !== null ? `${data.maxDrawdownPnl.toFixed(2)}%` : "N/A",
23034
23331
  isVisible: () => true,
23035
23332
  },
23036
23333
  {
23037
23334
  key: "medianPnl",
23038
23335
  label: "Median PNL",
23039
- format: (data) => data.medianPnl !== null ? functoolsKit.str(data.medianPnl, "%") : "N/A",
23336
+ format: (data) => data.medianPnl !== null ? `${data.medianPnl.toFixed(2)}%` : "N/A",
23040
23337
  isVisible: () => true,
23041
23338
  },
23042
23339
  {
@@ -23061,7 +23358,7 @@ const heat_columns = [
23061
23358
  key: "avgConsecutiveWinPnl",
23062
23359
  label: "Avg Win Streak PNL",
23063
23360
  format: (data) => data.avgConsecutiveWinPnl !== null
23064
- ? functoolsKit.str(data.avgConsecutiveWinPnl, "%")
23361
+ ? `${data.avgConsecutiveWinPnl.toFixed(2)}%`
23065
23362
  : "N/A",
23066
23363
  isVisible: () => true,
23067
23364
  },
@@ -23069,7 +23366,7 @@ const heat_columns = [
23069
23366
  key: "avgConsecutiveLossPnl",
23070
23367
  label: "Avg Loss Streak PNL",
23071
23368
  format: (data) => data.avgConsecutiveLossPnl !== null
23072
- ? functoolsKit.str(data.avgConsecutiveLossPnl, "%")
23369
+ ? `${data.avgConsecutiveLossPnl.toFixed(2)}%`
23073
23370
  : "N/A",
23074
23371
  isVisible: () => true,
23075
23372
  },
@@ -23082,7 +23379,7 @@ const heat_columns = [
23082
23379
  {
23083
23380
  key: "trendStrength",
23084
23381
  label: "Trend %/d",
23085
- format: (data) => data.trendStrength !== null ? functoolsKit.str(data.trendStrength, "%") : "N/A",
23382
+ format: (data) => data.trendStrength !== null ? `${data.trendStrength.toFixed(2)}%` : "N/A",
23086
23383
  isVisible: () => true,
23087
23384
  },
23088
23385
  {
@@ -23135,25 +23432,25 @@ const heat_columns = [
23135
23432
  {
23136
23433
  key: "medianStepSize",
23137
23434
  label: "Median Step",
23138
- format: (data) => data.medianStepSize !== null ? functoolsKit.str(data.medianStepSize, "%") : "N/A",
23435
+ format: (data) => data.medianStepSize !== null ? `${data.medianStepSize.toFixed(2)}%` : "N/A",
23139
23436
  isVisible: () => true,
23140
23437
  },
23141
23438
  {
23142
23439
  key: "sortinoRatio",
23143
23440
  label: "Sortino",
23144
- format: (data) => data.sortinoRatio !== null ? functoolsKit.str(data.sortinoRatio) : "N/A",
23441
+ format: (data) => data.sortinoRatio !== null ? data.sortinoRatio.toFixed(3) : "N/A",
23145
23442
  isVisible: () => true,
23146
23443
  },
23147
23444
  {
23148
23445
  key: "calmarRatio",
23149
23446
  label: "Calmar",
23150
- format: (data) => data.calmarRatio !== null ? functoolsKit.str(data.calmarRatio) : "N/A",
23447
+ format: (data) => data.calmarRatio !== null ? data.calmarRatio.toFixed(3) : "N/A",
23151
23448
  isVisible: () => true,
23152
23449
  },
23153
23450
  {
23154
23451
  key: "recoveryFactor",
23155
23452
  label: "Recovery",
23156
- format: (data) => data.recoveryFactor !== null ? functoolsKit.str(data.recoveryFactor) : "N/A",
23453
+ format: (data) => data.recoveryFactor !== null ? data.recoveryFactor.toFixed(3) : "N/A",
23157
23454
  isVisible: () => true,
23158
23455
  },
23159
23456
  ];
@@ -30329,28 +30626,28 @@ class HeatmapStorage {
30329
30626
  `# Portfolio Heatmap: ${strategyName}`,
30330
30627
  "",
30331
30628
  `**Total Symbols:** ${data.totalSymbols}`,
30332
- `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"}`,
30333
- `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"}`,
30334
- `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? functoolsKit.str(data.portfolioAnnualizedSharpeRatio) : "N/A"}`,
30335
- `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? functoolsKit.str(data.portfolioCertaintyRatio) : "N/A"}`,
30336
- `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? functoolsKit.str(data.portfolioExpectedYearlyReturns, "%") : "N/A"}`,
30629
+ `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? `${data.portfolioTotalPnl.toFixed(2)} %` : "N/A"}`,
30630
+ `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? data.portfolioSharpeRatio.toFixed(3) : "N/A"}`,
30631
+ `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? data.portfolioAnnualizedSharpeRatio.toFixed(3) : "N/A"}`,
30632
+ `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? data.portfolioCertaintyRatio.toFixed(3) : "N/A"}`,
30633
+ `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? `${data.portfolioExpectedYearlyReturns.toFixed(2)} %` : "N/A"}`,
30337
30634
  `**Trades Per Year:** ${data.portfolioTradesPerYear !== null ? data.portfolioTradesPerYear.toFixed(1) : "N/A"}`,
30338
30635
  `**Total Trades:** ${data.portfolioTotalTrades}`,
30339
- `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"}`,
30340
- `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
30341
- `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? functoolsKit.str(data.portfolioPeakProfitPnl, "%") : "N/A"}`,
30342
- `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? functoolsKit.str(data.portfolioMaxDrawdownPnl, "%") : "N/A"}`,
30343
- `**Median PNL:** ${data.portfolioMedianPnl !== null ? functoolsKit.str(data.portfolioMedianPnl, "%") : "N/A"}`,
30636
+ `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? `${data.portfolioAvgPeakPnl.toFixed(2)} %` : "N/A"}`,
30637
+ `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? `${data.portfolioAvgFallPnl.toFixed(2)} %` : "N/A"}`,
30638
+ `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? `${data.portfolioPeakProfitPnl.toFixed(2)} %` : "N/A"}`,
30639
+ `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? `${data.portfolioMaxDrawdownPnl.toFixed(2)} %` : "N/A"}`,
30640
+ `**Median PNL:** ${data.portfolioMedianPnl !== null ? `${data.portfolioMedianPnl.toFixed(2)} %` : "N/A"}`,
30344
30641
  `**Avg Duration:** ${data.portfolioAvgDuration !== null ? `${data.portfolioAvgDuration.toFixed(1)} min` : "N/A"}`,
30345
30642
  `**Avg Win Duration:** ${data.portfolioAvgWinDuration !== null ? `${data.portfolioAvgWinDuration.toFixed(1)} min` : "N/A"}`,
30346
30643
  `**Avg Loss Duration:** ${data.portfolioAvgLossDuration !== null ? `${data.portfolioAvgLossDuration.toFixed(1)} min` : "N/A"}`,
30347
- `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? functoolsKit.str(data.portfolioAvgConsecutiveWinPnl, "%") : "N/A"}`,
30348
- `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? functoolsKit.str(data.portfolioAvgConsecutiveLossPnl, "%") : "N/A"}`,
30349
- `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? functoolsKit.str(data.portfolioStdDev, "%") : "N/A"}`,
30350
- `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? functoolsKit.str(data.portfolioSortinoRatio) : "N/A"}`,
30351
- `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? functoolsKit.str(data.portfolioCalmarRatio) : "N/A"}`,
30352
- `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? functoolsKit.str(data.portfolioRecoveryFactor) : "N/A"}`,
30353
- `**Expectancy:** ${data.portfolioExpectancy !== null ? functoolsKit.str(data.portfolioExpectancy, "%") : "N/A"}`,
30644
+ `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? `${data.portfolioAvgConsecutiveWinPnl.toFixed(2)} %` : "N/A"}`,
30645
+ `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? `${data.portfolioAvgConsecutiveLossPnl.toFixed(2)} %` : "N/A"}`,
30646
+ `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? `${data.portfolioStdDev.toFixed(2)} %` : "N/A"}`,
30647
+ `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? data.portfolioSortinoRatio.toFixed(3) : "N/A"}`,
30648
+ `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? data.portfolioCalmarRatio.toFixed(3) : "N/A"}`,
30649
+ `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? data.portfolioRecoveryFactor.toFixed(3) : "N/A"}`,
30650
+ `**Expectancy:** ${data.portfolioExpectancy !== null ? `${data.portfolioExpectancy.toFixed(2)} %` : "N/A"}`,
30354
30651
  "",
30355
30652
  table,
30356
30653
  "",
@@ -67716,6 +68013,8 @@ exports.PersistStateAdapter = PersistStateAdapter;
67716
68013
  exports.PersistStateInstance = PersistStateInstance;
67717
68014
  exports.PersistStorageAdapter = PersistStorageAdapter;
67718
68015
  exports.PersistStorageInstance = PersistStorageInstance;
68016
+ exports.PersistStrategyAdapter = PersistStrategyAdapter;
68017
+ exports.PersistStrategyInstance = PersistStrategyInstance;
67719
68018
  exports.Position = Position;
67720
68019
  exports.PositionSize = PositionSize;
67721
68020
  exports.Recent = Recent;