backtest-kit 12.8.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1997 -1997
  3. package/build/index.cjs +862 -189
  4. package/build/index.mjs +859 -190
  5. package/package.json +86 -86
  6. package/types.d.ts +435 -2
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
  *
@@ -6625,6 +6837,119 @@ const validateScheduledSignal = (signal, currentPrice) => {
6625
6837
  }
6626
6838
  };
6627
6839
 
6840
+ /**
6841
+ * Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
6842
+ *
6843
+ * When priceOpen is provided:
6844
+ * - If currentPrice already reached priceOpen (shouldActivateImmediately) →
6845
+ * validates as pending: currentPrice must be between SL and TP
6846
+ * - Otherwise → validates as scheduled: priceOpen must be between SL and TP
6847
+ *
6848
+ * When priceOpen is absent:
6849
+ * - Validates as pending: currentPrice must be between SL and TP
6850
+ *
6851
+ * Checks:
6852
+ * - currentPrice is a finite positive number
6853
+ * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
6854
+ * - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
6855
+ *
6856
+ * @param signal - Signal DTO returned by getSignal
6857
+ * @param currentPrice - Current market price at the moment of signal creation
6858
+ * @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
6859
+ */
6860
+ const validateSignal = (signal, currentPrice) => {
6861
+ const errors = [];
6862
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
6863
+ {
6864
+ if (typeof currentPrice !== "number") {
6865
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
6866
+ }
6867
+ if (!isFinite(currentPrice)) {
6868
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
6869
+ }
6870
+ if (isFinite(currentPrice) && currentPrice <= 0) {
6871
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
6872
+ }
6873
+ }
6874
+ if (errors.length > 0) {
6875
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6876
+ return false;
6877
+ }
6878
+ try {
6879
+ validateCommonSignal(signal);
6880
+ }
6881
+ catch (error) {
6882
+ console.error(functoolsKit.getErrorMessage(error));
6883
+ return false;
6884
+ }
6885
+ // Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
6886
+ // - нет priceOpen → pending (открывается по currentPrice)
6887
+ // - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
6888
+ // - priceOpen задан и ещё не достигнут → scheduled
6889
+ const hasPriceOpen = signal.priceOpen !== undefined;
6890
+ const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
6891
+ (signal.position === "short" && currentPrice >= signal.priceOpen));
6892
+ const isScheduled = hasPriceOpen && !shouldActivateImmediately;
6893
+ if (isScheduled) {
6894
+ // Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
6895
+ if (signal.position === "long") {
6896
+ if (isFinite(signal.priceOpen)) {
6897
+ if (signal.priceOpen <= signal.priceStopLoss) {
6898
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
6899
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
6900
+ }
6901
+ if (signal.priceOpen >= signal.priceTakeProfit) {
6902
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
6903
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
6904
+ }
6905
+ }
6906
+ }
6907
+ if (signal.position === "short") {
6908
+ if (isFinite(signal.priceOpen)) {
6909
+ if (signal.priceOpen >= signal.priceStopLoss) {
6910
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
6911
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
6912
+ }
6913
+ if (signal.priceOpen <= signal.priceTakeProfit) {
6914
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
6915
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
6916
+ }
6917
+ }
6918
+ }
6919
+ }
6920
+ else {
6921
+ // Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
6922
+ if (signal.position === "long") {
6923
+ if (isFinite(currentPrice)) {
6924
+ if (currentPrice <= signal.priceStopLoss) {
6925
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
6926
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
6927
+ }
6928
+ if (currentPrice >= signal.priceTakeProfit) {
6929
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
6930
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
6931
+ }
6932
+ }
6933
+ }
6934
+ if (signal.position === "short") {
6935
+ if (isFinite(currentPrice)) {
6936
+ if (currentPrice >= signal.priceStopLoss) {
6937
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
6938
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
6939
+ }
6940
+ if (currentPrice <= signal.priceTakeProfit) {
6941
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
6942
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
6943
+ }
6944
+ }
6945
+ }
6946
+ }
6947
+ if (errors.length > 0) {
6948
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6949
+ }
6950
+ return !errors.length;
6951
+ };
6952
+
6628
6953
  const INTERVAL_MINUTES$8 = {
6629
6954
  "1m": 1,
6630
6955
  "3m": 3,
@@ -6791,6 +7116,9 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
6791
7116
  {
6792
7117
  self._commitQueue = [];
6793
7118
  }
7119
+ // Persist the now-empty queue so a crash after draining does not replay commits
7120
+ // that were already forwarded to the broker on the next restart.
7121
+ await PERSIST_STRATEGY_FN(self);
6794
7122
  if (!self._pendingSignal) {
6795
7123
  return;
6796
7124
  }
@@ -7088,11 +7416,27 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
7088
7416
  self._lastSignalTimestamp = alignedTime;
7089
7417
  }
7090
7418
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
7091
- const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
7092
- const signal = await Promise.race([
7093
- self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
7094
- functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
7095
- ]);
7419
+ // PRIORITY: a user-queued createPending/createScheduled DTO (set out of async-hooks
7420
+ // context) takes precedence over params.getSignal. When present it is consumed once
7421
+ // here — the slot is cleared and the snapshot rewritten — and then flows through the
7422
+ // exact same pipeline (risk check, priceOpen branching, onSignalSync on open) as a
7423
+ // signal returned by getSignal would.
7424
+ let signal;
7425
+ {
7426
+ if (!self._userSignal) {
7427
+ const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
7428
+ signal = await Promise.race([
7429
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
7430
+ functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
7431
+ ]);
7432
+ }
7433
+ if (self._userSignal) {
7434
+ signal = self._userSignal;
7435
+ self._userSignal = null;
7436
+ await PERSIST_STRATEGY_FN(self);
7437
+ }
7438
+ self._userSignal = null;
7439
+ }
7096
7440
  if (typeof signal === "symbol") {
7097
7441
  throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
7098
7442
  }
@@ -7247,6 +7591,21 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7247
7591
  self._lastPendingId = recentSignal.id;
7248
7592
  }
7249
7593
  }
7594
+ // Read deferred strategy state (commit queue + deferred user actions) so any
7595
+ // confirmed-but-not-yet-forwarded broker operation survives a live crash and is
7596
+ // re-drained on the next tick. Read here, before the pending restore which may
7597
+ // early-return on exchange/strategy mismatch.
7598
+ const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7599
+ if (strategyData) {
7600
+ // Deferred user actions are restored unconditionally: a deferred close belongs to
7601
+ // an already-cleared _pendingSignal, and cancel/activate belong to the scheduled
7602
+ // signal — none of them are tied to the currently-pending signal's id. A queued
7603
+ // createSignal is a not-yet-consumed signal source and likewise stands on its own.
7604
+ self._userSignal = strategyData.createSignal;
7605
+ self._closedSignal = strategyData.closedSignal;
7606
+ self._cancelledSignal = strategyData.cancelledSignal;
7607
+ self._activatedSignal = strategyData.activatedSignal;
7608
+ }
7250
7609
  // Restore pending signal
7251
7610
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7252
7611
  if (pendingSignal) {
@@ -7263,6 +7622,14 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7263
7622
  pendingSignal.minuteEstimatedTime = Infinity;
7264
7623
  }
7265
7624
  self._pendingSignal = pendingSignal;
7625
+ // Restore the commit queue only if the snapshot belongs to this exact pending
7626
+ // signal (pendingSignalId === restored id). The queue holds confirmed-but-not-yet
7627
+ // forwarded broker ops (average-buy / partial-* / trailing-* / breakeven) that are
7628
+ // always tied to the active position; a mismatch means the snapshot is stale and
7629
+ // the queue is dropped to avoid replaying ops against the wrong position.
7630
+ if (strategyData && strategyData.pendingSignalId === pendingSignal.id) {
7631
+ self._commitQueue = strategyData.commitQueue ?? [];
7632
+ }
7266
7633
  // Call onActive callback for restored signal
7267
7634
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
7268
7635
  const currentTime = self.params.execution.context.when.getTime();
@@ -7296,6 +7663,30 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
7296
7663
  self.params.logger.debug("ClientStrategy dispose");
7297
7664
  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
7665
  };
7666
+ /**
7667
+ * Persists the deferred strategy state snapshot (commit queue + deferred user actions)
7668
+ * to disk in live mode.
7669
+ *
7670
+ * These fields carry confirmed-but-not-yet-forwarded broker operations and survive the
7671
+ * gap between ticks (drained at the start of the next tick). Without persistence a live
7672
+ * crash in that window silently loses the pending broker operation while _pendingSignal
7673
+ * (already mutated and saved) claims it happened. Skipped in backtest mode.
7674
+ *
7675
+ * @param self - ClientStrategy instance
7676
+ */
7677
+ const PERSIST_STRATEGY_FN = async (self) => {
7678
+ if (self.params.backtest) {
7679
+ return;
7680
+ }
7681
+ await PersistStrategyAdapter.writeStrategyData({
7682
+ pendingSignalId: self._pendingSignal?.id ?? null,
7683
+ createSignal: self._userSignal,
7684
+ commitQueue: self._commitQueue,
7685
+ closedSignal: self._closedSignal,
7686
+ cancelledSignal: self._cancelledSignal,
7687
+ activatedSignal: self._activatedSignal,
7688
+ }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7689
+ };
7299
7690
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
7300
7691
  // Initialize partial array if not present
7301
7692
  if (!signal._partial)
@@ -8920,14 +9311,15 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
8920
9311
  const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
8921
9312
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
8922
9313
  if (!syncCloseAllowed) {
8923
- self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
9314
+ // Sync close rejected (e.g. broker rejected the order) keep _closedSignal intact
9315
+ // and return null so the candle loop re-attempts on the next candle. Mirrors live
9316
+ // tick, which keeps _closedSignal and returns idle on a rejected user close,
9317
+ // re-trying on the following tick.
9318
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry on next candle", {
8924
9319
  symbol: self.params.execution.context.symbol,
8925
9320
  signalId: closedSignal.id,
8926
9321
  });
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.`);
9322
+ return null;
8931
9323
  }
8932
9324
  self._closedSignal = null;
8933
9325
  const publicSignal = TO_PUBLIC_SIGNAL("pending", closedSignal, averagePrice);
@@ -9179,7 +9571,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9179
9571
  }
9180
9572
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
9181
9573
  if (self._closedSignal) {
9182
- return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9574
+ const userCloseResult = await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9575
+ // Sync close accepted — position closed, return. If rejected, _closedSignal is
9576
+ // kept and we skip this candle: live tick returns idle on a rejected user close
9577
+ // (it does NOT fall into the TP/SL or active-monitoring path), so the candle is
9578
+ // skipped here and the close is re-attempted on the next candle.
9579
+ if (userCloseResult) {
9580
+ return userCloseResult;
9581
+ }
9582
+ continue;
9183
9583
  }
9184
9584
  let shouldClose = false;
9185
9585
  let closeReason;
@@ -9230,7 +9630,19 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9230
9630
  else {
9231
9631
  closePrice = averagePrice; // time_expired uses VWAP
9232
9632
  }
9233
- return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9633
+ const closeResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9634
+ // Sync close accepted — position closed, return.
9635
+ if (closeResult) {
9636
+ return closeResult;
9637
+ }
9638
+ // Sync close rejected (e.g. broker rejected the order) — _pendingSignal is left
9639
+ // intact (not cleared on rejection). Do NOT return/continue: fall through to the
9640
+ // active-monitoring block below so this candle is processed exactly like live
9641
+ // tick, which runs RETURN_PENDING_SIGNAL_ACTIVE_FN when CLOSE_PENDING_SIGNAL_FN
9642
+ // returns null (updates _peak/_fall, fires active ping / breakeven / partial
9643
+ // callbacks, drains the commit queue). The close is re-attempted on the next
9644
+ // candle; for time_expired this eventually reaches the loop-exhausted close
9645
+ // (which throws if still rejected).
9234
9646
  }
9235
9647
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
9236
9648
  // Calculate percentage of path to TP/SL
@@ -9404,6 +9816,13 @@ class ClientStrategy {
9404
9816
  this._cancelledSignal = null;
9405
9817
  this._closedSignal = null;
9406
9818
  this._activatedSignal = null;
9819
+ /**
9820
+ * User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
9821
+ * params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
9822
+ * and the existing pipeline (priceOpen decides pending vs scheduled, onSignalSync on open)
9823
+ * is reused.
9824
+ */
9825
+ this._userSignal = null;
9407
9826
  /** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
9408
9827
  this._commitQueue = [];
9409
9828
  /**
@@ -9652,7 +10071,7 @@ class ClientStrategy {
9652
10071
  async getStopped(symbol) {
9653
10072
  this.params.logger.debug("ClientStrategy getStopped", {
9654
10073
  symbol,
9655
- strategyName: this.params.method.context.strategyName,
10074
+ strategyName: this.params.strategyName,
9656
10075
  });
9657
10076
  return this._isStopped;
9658
10077
  }
@@ -10335,6 +10754,8 @@ class ClientStrategy {
10335
10754
  if (this._cancelledSignal) {
10336
10755
  const cancelledSignal = this._cancelledSignal;
10337
10756
  this._cancelledSignal = null; // Clear after emitting
10757
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10758
+ await PERSIST_STRATEGY_FN(this);
10338
10759
  this.params.logger.info("ClientStrategy tick: scheduled signal was cancelled", {
10339
10760
  symbol: this.params.execution.context.symbol,
10340
10761
  signalId: cancelledSignal.id,
@@ -10393,6 +10814,8 @@ class ClientStrategy {
10393
10814
  return await RETURN_IDLE_FN(this, currentPrice);
10394
10815
  }
10395
10816
  this._closedSignal = null; // Clear only after sync confirmed
10817
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10818
+ await PERSIST_STRATEGY_FN(this);
10396
10819
  this.params.logger.info("ClientStrategy tick: pending signal was closed", {
10397
10820
  symbol: this.params.execution.context.symbol,
10398
10821
  signalId: closedSignal.id,
@@ -10448,6 +10871,8 @@ class ClientStrategy {
10448
10871
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
10449
10872
  const activatedSignal = this._activatedSignal;
10450
10873
  this._activatedSignal = null; // Clear after emitting
10874
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10875
+ await PERSIST_STRATEGY_FN(this);
10451
10876
  this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
10452
10877
  symbol: this.params.execution.context.symbol,
10453
10878
  signalId: activatedSignal.id,
@@ -10705,18 +11130,20 @@ class ClientStrategy {
10705
11130
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
10706
11131
  const closedSignal = this._closedSignal;
10707
11132
  const closeTimestamp = this.params.execution.context.when.getTime();
10708
- // Sync close: if external system rejects — restore _pendingSignal, retry on next backtest() call
11133
+ // Sync close: if external system rejects — keep the close pending and re-attempt
11134
+ // it inside the candle loop below (PROCESS_PENDING_SIGNAL_CANDLES_FN handles
11135
+ // _closedSignal per candle). Mirrors live tick, which keeps _closedSignal and
11136
+ // retries on the next tick instead of failing.
10709
11137
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, currentPrice, "closed", closedSignal, this);
10710
11138
  if (!syncCloseAllowed) {
10711
- this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
11139
+ this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry in candle loop", {
10712
11140
  symbol: this.params.execution.context.symbol,
10713
11141
  signalId: closedSignal.id,
10714
11142
  });
10715
- // Restore _pendingSignal so next backtest() call can process it normally
10716
- this._closedSignal = null;
11143
+ // Restore _pendingSignal so the candle loop processes the position normally;
11144
+ // _closedSignal is kept so the loop re-attempts the close on each candle.
10717
11145
  this._pendingSignal = closedSignal;
10718
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
10719
- `Retry backtest() with new candle data.`);
11146
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, this._pendingSignal, candles, frameEndTime);
10720
11147
  }
10721
11148
  this._closedSignal = null; // Clear only after sync confirmed
10722
11149
  // Emit commit with correct timestamp from backtest context
@@ -10898,7 +11325,7 @@ class ClientStrategy {
10898
11325
  if (backtest) {
10899
11326
  return;
10900
11327
  }
10901
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11328
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
10902
11329
  }
10903
11330
  /**
10904
11331
  * Cancels the scheduled signal without stopping the strategy.
@@ -10942,7 +11369,9 @@ class ClientStrategy {
10942
11369
  // Commit will be emitted in backtest() with correct candle timestamp
10943
11370
  return;
10944
11371
  }
10945
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11372
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11373
+ // Persist deferred _cancelledSignal so a crash before the next tick does not lose it
11374
+ await PERSIST_STRATEGY_FN(this);
10946
11375
  // Commit will be emitted in tick() with correct currentTime
10947
11376
  }
10948
11377
  /**
@@ -10993,7 +11422,9 @@ class ClientStrategy {
10993
11422
  // Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
10994
11423
  return;
10995
11424
  }
10996
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11425
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11426
+ // Persist deferred _activatedSignal so a crash before the next tick does not lose it
11427
+ await PERSIST_STRATEGY_FN(this);
10997
11428
  // Commit will be emitted AFTER successful risk check in tick()
10998
11429
  }
10999
11430
  /**
@@ -11038,8 +11469,92 @@ class ClientStrategy {
11038
11469
  return;
11039
11470
  }
11040
11471
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11472
+ // Persist deferred _closedSignal so a crash before the next tick does not lose it
11473
+ await PERSIST_STRATEGY_FN(this);
11041
11474
  // Commit will be emitted in tick() with correct currentTime
11042
11475
  }
11476
+ /**
11477
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of
11478
+ * params.getSignal.
11479
+ *
11480
+ * Works OUT of the async-hooks execution context (uses this.params.symbol directly).
11481
+ * priceOpen decides the outcome in the existing pipeline: when omitted the position opens
11482
+ * immediately at currentPrice; when provided the pipeline opens immediately if priceOpen is
11483
+ * already reached, otherwise registers a scheduled (priceOpen-awaiting) signal. On the next
11484
+ * tick GET_SIGNAL_FN consumes _userSignal and runs the normal pipeline, so onSignalSync
11485
+ * delivery is checked by OPEN_NEW_PENDING_SIGNAL_FN exactly as for a getSignal-produced signal.
11486
+ *
11487
+ * Validation (BEFORE any state mutation — a rejected call stores nothing):
11488
+ * - The DTO must pass the intrinsic shape/price checks.
11489
+ * - There must be NO signal already in flight: no active pending signal, no scheduled signal,
11490
+ * no already-queued createSignal, and no deferred activate/close/cancel awaiting drain.
11491
+ * Creating a new signal on top of an existing one is rejected.
11492
+ *
11493
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11494
+ * @param currentPrice - Current market price (priceOpen fallback for immediate signals)
11495
+ * @param dto - Signal DTO to open (priceOpen optional)
11496
+ * @returns Promise that resolves when the DTO is queued (and persisted in live mode)
11497
+ * @throws {Error} If the DTO is invalid or a signal/deferred action is already in flight
11498
+ */
11499
+ async createSignal(symbol, currentPrice, dto) {
11500
+ this.params.logger.debug("ClientStrategy createSignal", { symbol, currentPrice });
11501
+ // Validate BEFORE mutating state — a bad DTO or a busy strategy must store nothing.
11502
+ // Reuse validateSignal (the canonical getSignal-output validator, branching
11503
+ // pending-vs-scheduled by the same rule as GET_SIGNAL_FN). It forwards to
11504
+ // validateCommonSignal which requires priceOpen and minuteEstimatedTime, so normalize the
11505
+ // DTO to the defaults GET_SIGNAL_FN would apply (priceOpen → currentPrice for an immediate
11506
+ // signal, minuteEstimatedTime → CC_MAX_SIGNAL_LIFETIME_MINUTES). The original dto stored in
11507
+ // _userSignal is left untouched so GET_SIGNAL_FN applies its own defaults on consume.
11508
+ if (!validateSignal({
11509
+ ...dto,
11510
+ priceOpen: dto.priceOpen ?? currentPrice,
11511
+ minuteEstimatedTime: dto.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
11512
+ }, currentPrice)) {
11513
+ throw new Error(`ClientStrategy createSignal: invalid signal DTO for symbol=${symbol}`);
11514
+ }
11515
+ // Reject if any signal is already in flight or any deferred action is awaiting drain.
11516
+ if (this._pendingSignal) {
11517
+ throw new Error(`ClientStrategy createSignal: a pending signal already exists for symbol=${symbol}`);
11518
+ }
11519
+ if (this._scheduledSignal) {
11520
+ throw new Error(`ClientStrategy createSignal: a scheduled signal already exists for symbol=${symbol}`);
11521
+ }
11522
+ if (this._userSignal) {
11523
+ throw new Error(`ClientStrategy createSignal: a signal is already queued for creation for symbol=${symbol}`);
11524
+ }
11525
+ if (this._activatedSignal) {
11526
+ throw new Error(`ClientStrategy createSignal: a scheduled activation is pending for symbol=${symbol}`);
11527
+ }
11528
+ if (this._closedSignal) {
11529
+ throw new Error(`ClientStrategy createSignal: a pending close is awaiting for symbol=${symbol}`);
11530
+ }
11531
+ if (this._cancelledSignal) {
11532
+ throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
11533
+ }
11534
+ this._userSignal = dto;
11535
+ await PERSIST_STRATEGY_FN(this);
11536
+ }
11537
+ /**
11538
+ * Returns the deferred strategy-state snapshot exactly as it would be written to persist on
11539
+ * this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
11540
+ * plus the current pending signal id.
11541
+ *
11542
+ * Synchronous in-memory read (no disk access), so it works OUT of the async-hooks context.
11543
+ *
11544
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11545
+ * @returns The current StrategyData snapshot held in memory
11546
+ */
11547
+ async getStatus(symbol) {
11548
+ this.params.logger.debug("ClientStrategy getStatus", { symbol });
11549
+ return {
11550
+ pendingSignalId: this._pendingSignal?.id ?? null,
11551
+ createSignal: this._userSignal,
11552
+ commitQueue: this._commitQueue,
11553
+ closedSignal: this._closedSignal,
11554
+ cancelledSignal: this._cancelledSignal,
11555
+ activatedSignal: this._activatedSignal,
11556
+ };
11557
+ }
11043
11558
  /**
11044
11559
  * Validates preconditions for partialProfit without mutating state.
11045
11560
  *
@@ -11208,10 +11723,10 @@ class ClientStrategy {
11208
11723
  });
11209
11724
  // Call onWrite callback for testing persist storage
11210
11725
  if (this.params.callbacks?.onWrite) {
11211
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11726
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11212
11727
  }
11213
11728
  if (!backtest) {
11214
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11729
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11215
11730
  }
11216
11731
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11217
11732
  this._commitQueue.push({
@@ -11221,6 +11736,8 @@ class ClientStrategy {
11221
11736
  percentToClose,
11222
11737
  currentPrice,
11223
11738
  });
11739
+ // Persist the queued commit so a crash before the next tick does not lose it
11740
+ await PERSIST_STRATEGY_FN(this);
11224
11741
  return true;
11225
11742
  }
11226
11743
  /**
@@ -11391,10 +11908,10 @@ class ClientStrategy {
11391
11908
  });
11392
11909
  // Call onWrite callback for testing persist storage
11393
11910
  if (this.params.callbacks?.onWrite) {
11394
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11911
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11395
11912
  }
11396
11913
  if (!backtest) {
11397
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11914
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11398
11915
  }
11399
11916
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11400
11917
  this._commitQueue.push({
@@ -11404,6 +11921,8 @@ class ClientStrategy {
11404
11921
  percentToClose,
11405
11922
  currentPrice,
11406
11923
  });
11924
+ // Persist the queued commit so a crash before the next tick does not lose it
11925
+ await PERSIST_STRATEGY_FN(this);
11407
11926
  return true;
11408
11927
  }
11409
11928
  /**
@@ -11590,10 +12109,10 @@ class ClientStrategy {
11590
12109
  // Call onWrite callback for testing persist storage
11591
12110
  if (this.params.callbacks?.onWrite) {
11592
12111
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11593
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12112
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11594
12113
  }
11595
12114
  if (!backtest) {
11596
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12115
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11597
12116
  }
11598
12117
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11599
12118
  this._commitQueue.push({
@@ -11602,6 +12121,8 @@ class ClientStrategy {
11602
12121
  backtest,
11603
12122
  currentPrice,
11604
12123
  });
12124
+ // Persist the queued commit so a crash before the next tick does not lose it
12125
+ await PERSIST_STRATEGY_FN(this);
11605
12126
  return true;
11606
12127
  }
11607
12128
  /**
@@ -11839,10 +12360,10 @@ class ClientStrategy {
11839
12360
  // Call onWrite callback for testing persist storage
11840
12361
  if (this.params.callbacks?.onWrite) {
11841
12362
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11842
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12363
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11843
12364
  }
11844
12365
  if (!backtest) {
11845
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12366
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11846
12367
  }
11847
12368
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11848
12369
  this._commitQueue.push({
@@ -11852,6 +12373,8 @@ class ClientStrategy {
11852
12373
  percentShift,
11853
12374
  currentPrice,
11854
12375
  });
12376
+ // Persist the queued commit so a crash before the next tick does not lose it
12377
+ await PERSIST_STRATEGY_FN(this);
11855
12378
  return true;
11856
12379
  }
11857
12380
  /**
@@ -12075,10 +12598,10 @@ class ClientStrategy {
12075
12598
  // Call onWrite callback for testing persist storage
12076
12599
  if (this.params.callbacks?.onWrite) {
12077
12600
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
12078
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12601
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
12079
12602
  }
12080
12603
  if (!backtest) {
12081
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12604
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12082
12605
  }
12083
12606
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12084
12607
  this._commitQueue.push({
@@ -12088,6 +12611,8 @@ class ClientStrategy {
12088
12611
  percentShift,
12089
12612
  currentPrice,
12090
12613
  });
12614
+ // Persist the queued commit so a crash before the next tick does not lose it
12615
+ await PERSIST_STRATEGY_FN(this);
12091
12616
  return true;
12092
12617
  }
12093
12618
  /**
@@ -12166,10 +12691,10 @@ class ClientStrategy {
12166
12691
  });
12167
12692
  // Call onWrite callback for testing persist storage
12168
12693
  if (this.params.callbacks?.onWrite) {
12169
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12694
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12170
12695
  }
12171
12696
  if (!backtest) {
12172
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12697
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12173
12698
  }
12174
12699
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12175
12700
  this._commitQueue.push({
@@ -12180,6 +12705,8 @@ class ClientStrategy {
12180
12705
  cost,
12181
12706
  totalEntries: this._pendingSignal._entry?.length ?? 1,
12182
12707
  });
12708
+ // Persist the queued commit so a crash before the next tick does not lose it
12709
+ await PERSIST_STRATEGY_FN(this);
12183
12710
  return true;
12184
12711
  }
12185
12712
  }
@@ -12326,6 +12853,8 @@ class MergeRisk {
12326
12853
 
12327
12854
  /** Default interval for strategies that do not specify one */
12328
12855
  const STRATEGY_DEFAULT_INTERVAL = "1m";
12856
+ /** If not specified strategy will not open positions until createSignal is called manually */
12857
+ const STRATEGY_DEFAULT_SIGNAL = () => null;
12329
12858
  /**
12330
12859
  * If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
12331
12860
  * to the exchange (e.g. limit order failed to fill).
@@ -12759,7 +13288,7 @@ class StrategyConnectionService {
12759
13288
  * @returns Configured ClientStrategy instance
12760
13289
  */
12761
13290
  this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12762
- const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
13291
+ const { riskName = "", riskList = [], getSignal = STRATEGY_DEFAULT_SIGNAL, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
12763
13292
  return new ClientStrategy({
12764
13293
  symbol,
12765
13294
  interval,
@@ -13778,6 +14307,47 @@ class StrategyConnectionService {
13778
14307
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
13779
14308
  await strategy.closePending(symbol, backtest, payload);
13780
14309
  };
14310
+ /**
14311
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
14312
+ *
14313
+ * Delegates to ClientStrategy.createSignal(). Validated and rejected if a signal/deferred
14314
+ * action is already in flight. Works out of the async-hooks execution context.
14315
+ *
14316
+ * @param backtest - Whether running in backtest mode
14317
+ * @param symbol - Trading pair symbol
14318
+ * @param dto - Signal DTO to open
14319
+ * @param context - Context with strategyName, exchangeName, frameName
14320
+ * @returns Promise that resolves when the DTO is queued
14321
+ */
14322
+ this.createSignal = async (backtest, symbol, dto, context) => {
14323
+ this.loggerService.log("strategyConnectionService createSignal", {
14324
+ symbol,
14325
+ context,
14326
+ backtest,
14327
+ });
14328
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14329
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
14330
+ await strategy.createSignal(symbol, currentPrice, dto);
14331
+ };
14332
+ /**
14333
+ * Returns the in-memory deferred strategy-state snapshot for this iteration.
14334
+ *
14335
+ * Delegates to ClientStrategy.getStatus(). Synchronous in-memory read; works out of context.
14336
+ *
14337
+ * @param backtest - Whether running in backtest mode
14338
+ * @param symbol - Trading pair symbol
14339
+ * @param context - Context with strategyName, exchangeName, frameName
14340
+ * @returns Promise resolving to the current StrategyData snapshot
14341
+ */
14342
+ this.getStatus = async (backtest, symbol, context) => {
14343
+ this.loggerService.log("strategyConnectionService getStatus", {
14344
+ symbol,
14345
+ context,
14346
+ backtest,
14347
+ });
14348
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14349
+ return await strategy.getStatus(symbol);
14350
+ };
13781
14351
  /**
13782
14352
  * Checks whether `partialProfit` would succeed without executing it.
13783
14353
  * Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
@@ -17381,6 +17951,47 @@ class StrategyCoreService {
17381
17951
  await this.validate(context);
17382
17952
  return await this.strategyConnectionService.closePending(backtest, symbol, context, payload);
17383
17953
  };
17954
+ /**
17955
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
17956
+ *
17957
+ * Validates the context, then delegates to StrategyConnectionService.createSignal().
17958
+ * Rejected if a signal or deferred action is already in flight. Does not require execution context.
17959
+ *
17960
+ * @param backtest - Whether running in backtest mode
17961
+ * @param symbol - Trading pair symbol
17962
+ * @param dto - Signal DTO to open
17963
+ * @param context - Context with strategyName, exchangeName, frameName
17964
+ * @returns Promise that resolves when the DTO is queued
17965
+ */
17966
+ this.createSignal = async (backtest, symbol, dto, context) => {
17967
+ this.loggerService.log("strategyCoreService createSignal", {
17968
+ symbol,
17969
+ context,
17970
+ backtest,
17971
+ });
17972
+ await this.validate(context);
17973
+ return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
17974
+ };
17975
+ /**
17976
+ * Returns the in-memory deferred strategy-state snapshot for this iteration.
17977
+ *
17978
+ * Validates the context, then delegates to StrategyConnectionService.getStatus().
17979
+ * Does not require execution context.
17980
+ *
17981
+ * @param backtest - Whether running in backtest mode
17982
+ * @param symbol - Trading pair symbol
17983
+ * @param context - Context with strategyName, exchangeName, frameName
17984
+ * @returns Promise resolving to the current StrategyStatus snapshot
17985
+ */
17986
+ this.getStatus = async (backtest, symbol, context) => {
17987
+ this.loggerService.log("strategyCoreService getStatus", {
17988
+ symbol,
17989
+ context,
17990
+ backtest,
17991
+ });
17992
+ await this.validate(context);
17993
+ return await this.strategyConnectionService.getStatus(backtest, symbol, context);
17994
+ };
17384
17995
  /**
17385
17996
  * Disposes the ClientStrategy instance for the given context.
17386
17997
  *
@@ -18905,7 +19516,7 @@ class StrategySchemaService {
18905
19516
  if (strategySchema.interval && typeof strategySchema.interval !== "string") {
18906
19517
  throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
18907
19518
  }
18908
- if (typeof strategySchema.getSignal !== "function") {
19519
+ if (strategySchema.getSignal && typeof strategySchema.getSignal !== "function") {
18909
19520
  throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
18910
19521
  }
18911
19522
  };
@@ -22920,32 +23531,32 @@ const heat_columns = [
22920
23531
  {
22921
23532
  key: "totalPnl",
22922
23533
  label: "Total PNL",
22923
- format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
23534
+ format: (data) => data.totalPnl !== null ? `${data.totalPnl.toFixed(2)}%` : "N/A",
22924
23535
  isVisible: () => true,
22925
23536
  },
22926
23537
  {
22927
23538
  key: "sharpeRatio",
22928
23539
  label: "Sharpe",
22929
- format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
23540
+ format: (data) => data.sharpeRatio !== null ? data.sharpeRatio.toFixed(3) : "N/A",
22930
23541
  isVisible: () => true,
22931
23542
  },
22932
23543
  {
22933
23544
  key: "annualizedSharpeRatio",
22934
23545
  label: "Ann Sharpe",
22935
- format: (data) => data.annualizedSharpeRatio !== null ? functoolsKit.str(data.annualizedSharpeRatio) : "N/A",
23546
+ format: (data) => data.annualizedSharpeRatio !== null ? data.annualizedSharpeRatio.toFixed(3) : "N/A",
22936
23547
  isVisible: () => true,
22937
23548
  },
22938
23549
  {
22939
23550
  key: "certaintyRatio",
22940
23551
  label: "Certainty",
22941
- format: (data) => data.certaintyRatio !== null ? functoolsKit.str(data.certaintyRatio) : "N/A",
23552
+ format: (data) => data.certaintyRatio !== null ? data.certaintyRatio.toFixed(3) : "N/A",
22942
23553
  isVisible: () => true,
22943
23554
  },
22944
23555
  {
22945
23556
  key: "expectedYearlyReturns",
22946
23557
  label: "Exp Yearly",
22947
23558
  format: (data) => data.expectedYearlyReturns !== null
22948
- ? functoolsKit.str(data.expectedYearlyReturns, "%")
23559
+ ? `${data.expectedYearlyReturns.toFixed(2)}%`
22949
23560
  : "N/A",
22950
23561
  isVisible: () => true,
22951
23562
  },
@@ -22958,37 +23569,37 @@ const heat_columns = [
22958
23569
  {
22959
23570
  key: "profitFactor",
22960
23571
  label: "PF",
22961
- format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
23572
+ format: (data) => data.profitFactor !== null ? data.profitFactor.toFixed(3) : "N/A",
22962
23573
  isVisible: () => true,
22963
23574
  },
22964
23575
  {
22965
23576
  key: "expectancy",
22966
23577
  label: "Expect",
22967
- format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
23578
+ format: (data) => data.expectancy !== null ? `${data.expectancy.toFixed(2)}%` : "N/A",
22968
23579
  isVisible: () => true,
22969
23580
  },
22970
23581
  {
22971
23582
  key: "winRate",
22972
23583
  label: "WR",
22973
- format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
23584
+ format: (data) => data.winRate !== null ? `${data.winRate.toFixed(2)}%` : "N/A",
22974
23585
  isVisible: () => true,
22975
23586
  },
22976
23587
  {
22977
23588
  key: "avgWin",
22978
23589
  label: "Avg Win",
22979
- format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
23590
+ format: (data) => data.avgWin !== null ? `${data.avgWin.toFixed(2)}%` : "N/A",
22980
23591
  isVisible: () => true,
22981
23592
  },
22982
23593
  {
22983
23594
  key: "avgLoss",
22984
23595
  label: "Avg Loss",
22985
- format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
23596
+ format: (data) => data.avgLoss !== null ? `${data.avgLoss.toFixed(2)}%` : "N/A",
22986
23597
  isVisible: () => true,
22987
23598
  },
22988
23599
  {
22989
23600
  key: "maxDrawdown",
22990
23601
  label: "Max DD",
22991
- format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
23602
+ format: (data) => data.maxDrawdown !== null ? `${(-data.maxDrawdown).toFixed(2)}%` : "N/A",
22992
23603
  isVisible: () => true,
22993
23604
  },
22994
23605
  {
@@ -23012,31 +23623,31 @@ const heat_columns = [
23012
23623
  {
23013
23624
  key: "avgPeakPnl",
23014
23625
  label: "Avg Peak PNL",
23015
- format: (data) => data.avgPeakPnl !== null ? functoolsKit.str(data.avgPeakPnl, "%") : "N/A",
23626
+ format: (data) => data.avgPeakPnl !== null ? `${data.avgPeakPnl.toFixed(2)}%` : "N/A",
23016
23627
  isVisible: () => true,
23017
23628
  },
23018
23629
  {
23019
23630
  key: "avgFallPnl",
23020
23631
  label: "Avg DD PNL",
23021
- format: (data) => data.avgFallPnl !== null ? functoolsKit.str(data.avgFallPnl, "%") : "N/A",
23632
+ format: (data) => data.avgFallPnl !== null ? `${data.avgFallPnl.toFixed(2)}%` : "N/A",
23022
23633
  isVisible: () => true,
23023
23634
  },
23024
23635
  {
23025
23636
  key: "peakProfitPnl",
23026
23637
  label: "Peak Profit PNL",
23027
- format: (data) => data.peakProfitPnl !== null ? functoolsKit.str(data.peakProfitPnl, "%") : "N/A",
23638
+ format: (data) => data.peakProfitPnl !== null ? `${data.peakProfitPnl.toFixed(2)}%` : "N/A",
23028
23639
  isVisible: () => true,
23029
23640
  },
23030
23641
  {
23031
23642
  key: "maxDrawdownPnl",
23032
23643
  label: "Max DD PNL",
23033
- format: (data) => data.maxDrawdownPnl !== null ? functoolsKit.str(data.maxDrawdownPnl, "%") : "N/A",
23644
+ format: (data) => data.maxDrawdownPnl !== null ? `${data.maxDrawdownPnl.toFixed(2)}%` : "N/A",
23034
23645
  isVisible: () => true,
23035
23646
  },
23036
23647
  {
23037
23648
  key: "medianPnl",
23038
23649
  label: "Median PNL",
23039
- format: (data) => data.medianPnl !== null ? functoolsKit.str(data.medianPnl, "%") : "N/A",
23650
+ format: (data) => data.medianPnl !== null ? `${data.medianPnl.toFixed(2)}%` : "N/A",
23040
23651
  isVisible: () => true,
23041
23652
  },
23042
23653
  {
@@ -23061,7 +23672,7 @@ const heat_columns = [
23061
23672
  key: "avgConsecutiveWinPnl",
23062
23673
  label: "Avg Win Streak PNL",
23063
23674
  format: (data) => data.avgConsecutiveWinPnl !== null
23064
- ? functoolsKit.str(data.avgConsecutiveWinPnl, "%")
23675
+ ? `${data.avgConsecutiveWinPnl.toFixed(2)}%`
23065
23676
  : "N/A",
23066
23677
  isVisible: () => true,
23067
23678
  },
@@ -23069,7 +23680,7 @@ const heat_columns = [
23069
23680
  key: "avgConsecutiveLossPnl",
23070
23681
  label: "Avg Loss Streak PNL",
23071
23682
  format: (data) => data.avgConsecutiveLossPnl !== null
23072
- ? functoolsKit.str(data.avgConsecutiveLossPnl, "%")
23683
+ ? `${data.avgConsecutiveLossPnl.toFixed(2)}%`
23073
23684
  : "N/A",
23074
23685
  isVisible: () => true,
23075
23686
  },
@@ -23082,7 +23693,7 @@ const heat_columns = [
23082
23693
  {
23083
23694
  key: "trendStrength",
23084
23695
  label: "Trend %/d",
23085
- format: (data) => data.trendStrength !== null ? functoolsKit.str(data.trendStrength, "%") : "N/A",
23696
+ format: (data) => data.trendStrength !== null ? `${data.trendStrength.toFixed(2)}%` : "N/A",
23086
23697
  isVisible: () => true,
23087
23698
  },
23088
23699
  {
@@ -23135,25 +23746,25 @@ const heat_columns = [
23135
23746
  {
23136
23747
  key: "medianStepSize",
23137
23748
  label: "Median Step",
23138
- format: (data) => data.medianStepSize !== null ? functoolsKit.str(data.medianStepSize, "%") : "N/A",
23749
+ format: (data) => data.medianStepSize !== null ? `${data.medianStepSize.toFixed(2)}%` : "N/A",
23139
23750
  isVisible: () => true,
23140
23751
  },
23141
23752
  {
23142
23753
  key: "sortinoRatio",
23143
23754
  label: "Sortino",
23144
- format: (data) => data.sortinoRatio !== null ? functoolsKit.str(data.sortinoRatio) : "N/A",
23755
+ format: (data) => data.sortinoRatio !== null ? data.sortinoRatio.toFixed(3) : "N/A",
23145
23756
  isVisible: () => true,
23146
23757
  },
23147
23758
  {
23148
23759
  key: "calmarRatio",
23149
23760
  label: "Calmar",
23150
- format: (data) => data.calmarRatio !== null ? functoolsKit.str(data.calmarRatio) : "N/A",
23761
+ format: (data) => data.calmarRatio !== null ? data.calmarRatio.toFixed(3) : "N/A",
23151
23762
  isVisible: () => true,
23152
23763
  },
23153
23764
  {
23154
23765
  key: "recoveryFactor",
23155
23766
  label: "Recovery",
23156
- format: (data) => data.recoveryFactor !== null ? functoolsKit.str(data.recoveryFactor) : "N/A",
23767
+ format: (data) => data.recoveryFactor !== null ? data.recoveryFactor.toFixed(3) : "N/A",
23157
23768
  isVisible: () => true,
23158
23769
  },
23159
23770
  ];
@@ -30329,28 +30940,28 @@ class HeatmapStorage {
30329
30940
  `# Portfolio Heatmap: ${strategyName}`,
30330
30941
  "",
30331
30942
  `**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"}`,
30943
+ `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? `${data.portfolioTotalPnl.toFixed(2)} %` : "N/A"}`,
30944
+ `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? data.portfolioSharpeRatio.toFixed(3) : "N/A"}`,
30945
+ `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? data.portfolioAnnualizedSharpeRatio.toFixed(3) : "N/A"}`,
30946
+ `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? data.portfolioCertaintyRatio.toFixed(3) : "N/A"}`,
30947
+ `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? `${data.portfolioExpectedYearlyReturns.toFixed(2)} %` : "N/A"}`,
30337
30948
  `**Trades Per Year:** ${data.portfolioTradesPerYear !== null ? data.portfolioTradesPerYear.toFixed(1) : "N/A"}`,
30338
30949
  `**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"}`,
30950
+ `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? `${data.portfolioAvgPeakPnl.toFixed(2)} %` : "N/A"}`,
30951
+ `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? `${data.portfolioAvgFallPnl.toFixed(2)} %` : "N/A"}`,
30952
+ `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? `${data.portfolioPeakProfitPnl.toFixed(2)} %` : "N/A"}`,
30953
+ `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? `${data.portfolioMaxDrawdownPnl.toFixed(2)} %` : "N/A"}`,
30954
+ `**Median PNL:** ${data.portfolioMedianPnl !== null ? `${data.portfolioMedianPnl.toFixed(2)} %` : "N/A"}`,
30344
30955
  `**Avg Duration:** ${data.portfolioAvgDuration !== null ? `${data.portfolioAvgDuration.toFixed(1)} min` : "N/A"}`,
30345
30956
  `**Avg Win Duration:** ${data.portfolioAvgWinDuration !== null ? `${data.portfolioAvgWinDuration.toFixed(1)} min` : "N/A"}`,
30346
30957
  `**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"}`,
30958
+ `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? `${data.portfolioAvgConsecutiveWinPnl.toFixed(2)} %` : "N/A"}`,
30959
+ `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? `${data.portfolioAvgConsecutiveLossPnl.toFixed(2)} %` : "N/A"}`,
30960
+ `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? `${data.portfolioStdDev.toFixed(2)} %` : "N/A"}`,
30961
+ `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? data.portfolioSortinoRatio.toFixed(3) : "N/A"}`,
30962
+ `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? data.portfolioCalmarRatio.toFixed(3) : "N/A"}`,
30963
+ `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? data.portfolioRecoveryFactor.toFixed(3) : "N/A"}`,
30964
+ `**Expectancy:** ${data.portfolioExpectancy !== null ? `${data.portfolioExpectancy.toFixed(2)} %` : "N/A"}`,
30354
30965
  "",
30355
30966
  table,
30356
30967
  "",
@@ -42619,6 +43230,8 @@ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOve
42619
43230
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
42620
43231
  const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
42621
43232
  const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
43233
+ const CREATE_SIGNAL_METHOD_NAME = "strategy.createSignal";
43234
+ const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
42622
43235
  /**
42623
43236
  * Cancels the scheduled signal without stopping the strategy.
42624
43237
  *
@@ -44683,6 +45296,69 @@ async function commitSignalNotify(symbol, payload = {}) {
44683
45296
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
44684
45297
  await bt.notificationHelperService.commitSignalNotify(payload, symbol, currentPrice, { strategyName, exchangeName, frameName }, isBacktest);
44685
45298
  }
45299
+ /**
45300
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of params.getSignal.
45301
+ *
45302
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
45303
+ * current price; provided → opens immediately if already reached, otherwise registers a
45304
+ * scheduled (priceOpen-awaiting) signal. The DTO is validated (reusing validateSignal) and the
45305
+ * call is rejected if a signal or deferred action is already in flight.
45306
+ *
45307
+ * Automatically detects backtest/live mode from execution context.
45308
+ *
45309
+ * @param symbol - Trading pair symbol
45310
+ * @param dto - Signal DTO to open (priceOpen optional)
45311
+ * @returns Promise that resolves when the DTO is queued
45312
+ *
45313
+ * @example
45314
+ * ```typescript
45315
+ * import { createSignal } from "backtest-kit";
45316
+ *
45317
+ * // Open immediately at current price
45318
+ * await createSignal("BTCUSDT", { position: "long", priceTakeProfit: 110, priceStopLoss: 90 });
45319
+ * ```
45320
+ */
45321
+ async function createSignal(symbol, dto) {
45322
+ bt.loggerService.info(CREATE_SIGNAL_METHOD_NAME, { symbol });
45323
+ if (!ExecutionContextService.hasContext()) {
45324
+ throw new Error("createSignal requires an execution context");
45325
+ }
45326
+ if (!MethodContextService.hasContext()) {
45327
+ throw new Error("createSignal requires a method context");
45328
+ }
45329
+ const { backtest: isBacktest } = bt.executionContextService.context;
45330
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45331
+ await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
45332
+ }
45333
+ /**
45334
+ * Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
45335
+ * createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
45336
+ *
45337
+ * Automatically detects backtest/live mode from execution context.
45338
+ *
45339
+ * @param symbol - Trading pair symbol
45340
+ * @returns Promise resolving to the current StrategyStatus snapshot
45341
+ *
45342
+ * @example
45343
+ * ```typescript
45344
+ * import { getStrategyStatus } from "backtest-kit";
45345
+ *
45346
+ * const status = await getStrategyStatus("BTCUSDT");
45347
+ * console.log(status.pendingSignalId, status.commitQueue.length);
45348
+ * ```
45349
+ */
45350
+ async function getStrategyStatus(symbol) {
45351
+ bt.loggerService.info(GET_STRATEGY_STATUS_METHOD_NAME, { symbol });
45352
+ if (!ExecutionContextService.hasContext()) {
45353
+ throw new Error("getStrategyStatus requires an execution context");
45354
+ }
45355
+ if (!MethodContextService.hasContext()) {
45356
+ throw new Error("getStrategyStatus requires a method context");
45357
+ }
45358
+ const { backtest: isBacktest } = bt.executionContextService.context;
45359
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45360
+ return await bt.strategyCoreService.getStatus(isBacktest, symbol, { exchangeName, frameName, strategyName });
45361
+ }
44686
45362
 
44687
45363
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
44688
45364
  /**
@@ -46354,6 +47030,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPosi
46354
47030
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
46355
47031
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
46356
47032
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
47033
+ const BACKTEST_METHOD_NAME_CREATE_SIGNAL = "Backtest.createSignal";
47034
+ const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
46357
47035
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
46358
47036
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
46359
47037
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
@@ -48058,6 +48736,53 @@ class BacktestUtils {
48058
48736
  }
48059
48737
  await bt.strategyCoreService.closePending(true, symbol, context, payload);
48060
48738
  };
48739
+ /**
48740
+ * Queues a user-supplied signal DTO to be consumed by the next backtest tick instead of
48741
+ * params.getSignal.
48742
+ *
48743
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
48744
+ * current price; provided → opens immediately if already reached, otherwise registers a
48745
+ * scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
48746
+ *
48747
+ * @param symbol - Trading pair symbol
48748
+ * @param context - Execution context with strategyName, exchangeName, and frameName
48749
+ * @param dto - Signal DTO to open (priceOpen optional)
48750
+ * @returns Promise that resolves when the DTO is queued
48751
+ */
48752
+ this.createSignal = async (symbol, context, dto) => {
48753
+ bt.loggerService.info(BACKTEST_METHOD_NAME_CREATE_SIGNAL, {
48754
+ symbol,
48755
+ context,
48756
+ });
48757
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48758
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48759
+ {
48760
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
48761
+ riskName &&
48762
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48763
+ riskList &&
48764
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
48765
+ actions &&
48766
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
48767
+ }
48768
+ await bt.strategyCoreService.createSignal(true, symbol, dto, context);
48769
+ };
48770
+ /**
48771
+ * Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
48772
+ *
48773
+ * @param symbol - Trading pair symbol
48774
+ * @param context - Execution context with strategyName, exchangeName, and frameName
48775
+ * @returns Promise resolving to the current StrategyStatus snapshot
48776
+ */
48777
+ this.getStrategyStatus = async (symbol, context) => {
48778
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS, {
48779
+ symbol,
48780
+ context,
48781
+ });
48782
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
48783
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
48784
+ return await bt.strategyCoreService.getStatus(true, symbol, context);
48785
+ };
48061
48786
  /**
48062
48787
  * Executes partial close at profit level (moving toward TP).
48063
48788
  *
@@ -49016,6 +49741,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPart
49016
49741
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
49017
49742
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
49018
49743
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
49744
+ const LIVE_METHOD_NAME_CREATE_SIGNAL = "Live.createSignal";
49745
+ const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
49019
49746
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
49020
49747
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
49021
49748
  const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
@@ -50893,6 +51620,61 @@ class LiveUtils {
50893
51620
  frameName: "",
50894
51621
  }, payload);
50895
51622
  };
51623
+ /**
51624
+ * Queues a user-supplied signal DTO to be consumed by the next live tick instead of
51625
+ * params.getSignal.
51626
+ *
51627
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
51628
+ * current price; provided → opens immediately if already reached, otherwise registers a
51629
+ * scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
51630
+ *
51631
+ * @param symbol - Trading pair symbol
51632
+ * @param context - Execution context with strategyName and exchangeName
51633
+ * @param dto - Signal DTO to open (priceOpen optional)
51634
+ * @returns Promise that resolves when the DTO is queued
51635
+ */
51636
+ this.createSignal = async (symbol, context, dto) => {
51637
+ bt.loggerService.info(LIVE_METHOD_NAME_CREATE_SIGNAL, {
51638
+ symbol,
51639
+ context,
51640
+ });
51641
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51642
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51643
+ {
51644
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
51645
+ riskName &&
51646
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51647
+ riskList &&
51648
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL));
51649
+ actions &&
51650
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CREATE_SIGNAL));
51651
+ }
51652
+ await bt.strategyCoreService.createSignal(false, symbol, dto, {
51653
+ strategyName: context.strategyName,
51654
+ exchangeName: context.exchangeName,
51655
+ frameName: "",
51656
+ });
51657
+ };
51658
+ /**
51659
+ * Returns the in-memory deferred strategy-state snapshot for the current live iteration.
51660
+ *
51661
+ * @param symbol - Trading pair symbol
51662
+ * @param context - Execution context with strategyName and exchangeName
51663
+ * @returns Promise resolving to the current StrategyStatus snapshot
51664
+ */
51665
+ this.getStrategyStatus = async (symbol, context) => {
51666
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_STRATEGY_STATUS, {
51667
+ symbol,
51668
+ context,
51669
+ });
51670
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
51671
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
51672
+ return await bt.strategyCoreService.getStatus(false, symbol, {
51673
+ strategyName: context.strategyName,
51674
+ exchangeName: context.exchangeName,
51675
+ frameName: "",
51676
+ });
51677
+ };
50896
51678
  /**
50897
51679
  * Executes partial close at profit level (moving toward TP).
50898
51680
  *
@@ -67539,119 +68321,6 @@ const percentValue = (yesterdayValue, todayValue) => {
67539
68321
  return yesterdayValue / todayValue - 1;
67540
68322
  };
67541
68323
 
67542
- /**
67543
- * Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
67544
- *
67545
- * When priceOpen is provided:
67546
- * - If currentPrice already reached priceOpen (shouldActivateImmediately) →
67547
- * validates as pending: currentPrice must be between SL and TP
67548
- * - Otherwise → validates as scheduled: priceOpen must be between SL and TP
67549
- *
67550
- * When priceOpen is absent:
67551
- * - Validates as pending: currentPrice must be between SL and TP
67552
- *
67553
- * Checks:
67554
- * - currentPrice is a finite positive number
67555
- * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
67556
- * - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
67557
- *
67558
- * @param signal - Signal DTO returned by getSignal
67559
- * @param currentPrice - Current market price at the moment of signal creation
67560
- * @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
67561
- */
67562
- const validateSignal = (signal, currentPrice) => {
67563
- const errors = [];
67564
- // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
67565
- {
67566
- if (typeof currentPrice !== "number") {
67567
- errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
67568
- }
67569
- if (!isFinite(currentPrice)) {
67570
- errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
67571
- }
67572
- if (isFinite(currentPrice) && currentPrice <= 0) {
67573
- errors.push(`currentPrice must be positive, got ${currentPrice}`);
67574
- }
67575
- }
67576
- if (errors.length > 0) {
67577
- console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
67578
- return false;
67579
- }
67580
- try {
67581
- validateCommonSignal(signal);
67582
- }
67583
- catch (error) {
67584
- console.error(functoolsKit.getErrorMessage(error));
67585
- return false;
67586
- }
67587
- // Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
67588
- // - нет priceOpen → pending (открывается по currentPrice)
67589
- // - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
67590
- // - priceOpen задан и ещё не достигнут → scheduled
67591
- const hasPriceOpen = signal.priceOpen !== undefined;
67592
- const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
67593
- (signal.position === "short" && currentPrice >= signal.priceOpen));
67594
- const isScheduled = hasPriceOpen && !shouldActivateImmediately;
67595
- if (isScheduled) {
67596
- // Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
67597
- if (signal.position === "long") {
67598
- if (isFinite(signal.priceOpen)) {
67599
- if (signal.priceOpen <= signal.priceStopLoss) {
67600
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
67601
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
67602
- }
67603
- if (signal.priceOpen >= signal.priceTakeProfit) {
67604
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
67605
- `Signal would close immediately on activation. This is logically impossible for LONG position.`);
67606
- }
67607
- }
67608
- }
67609
- if (signal.position === "short") {
67610
- if (isFinite(signal.priceOpen)) {
67611
- if (signal.priceOpen >= signal.priceStopLoss) {
67612
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
67613
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
67614
- }
67615
- if (signal.priceOpen <= signal.priceTakeProfit) {
67616
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
67617
- `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
67618
- }
67619
- }
67620
- }
67621
- }
67622
- else {
67623
- // Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
67624
- if (signal.position === "long") {
67625
- if (isFinite(currentPrice)) {
67626
- if (currentPrice <= signal.priceStopLoss) {
67627
- errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
67628
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
67629
- }
67630
- if (currentPrice >= signal.priceTakeProfit) {
67631
- errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
67632
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
67633
- }
67634
- }
67635
- }
67636
- if (signal.position === "short") {
67637
- if (isFinite(currentPrice)) {
67638
- if (currentPrice >= signal.priceStopLoss) {
67639
- errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
67640
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
67641
- }
67642
- if (currentPrice <= signal.priceTakeProfit) {
67643
- errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
67644
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
67645
- }
67646
- }
67647
- }
67648
- }
67649
- if (errors.length > 0) {
67650
- console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
67651
- }
67652
- return !errors.length;
67653
- };
67654
-
67655
68324
  exports.ActionBase = ActionBase;
67656
68325
  exports.Backtest = Backtest;
67657
68326
  exports.Breakeven = Breakeven;
@@ -67716,6 +68385,8 @@ exports.PersistStateAdapter = PersistStateAdapter;
67716
68385
  exports.PersistStateInstance = PersistStateInstance;
67717
68386
  exports.PersistStorageAdapter = PersistStorageAdapter;
67718
68387
  exports.PersistStorageInstance = PersistStorageInstance;
68388
+ exports.PersistStrategyAdapter = PersistStrategyAdapter;
68389
+ exports.PersistStrategyInstance = PersistStrategyInstance;
67719
68390
  exports.Position = Position;
67720
68391
  exports.PositionSize = PositionSize;
67721
68392
  exports.Recent = Recent;
@@ -67768,6 +68439,7 @@ exports.commitTrailingStop = commitTrailingStop;
67768
68439
  exports.commitTrailingStopCost = commitTrailingStopCost;
67769
68440
  exports.commitTrailingTake = commitTrailingTake;
67770
68441
  exports.commitTrailingTakeCost = commitTrailingTakeCost;
68442
+ exports.createSignal = createSignal;
67771
68443
  exports.createSignalState = createSignalState;
67772
68444
  exports.dumpAgentAnswer = dumpAgentAnswer;
67773
68445
  exports.dumpError = dumpError;
@@ -67842,6 +68514,7 @@ exports.getSessionData = getSessionData;
67842
68514
  exports.getSignalState = getSignalState;
67843
68515
  exports.getSizingSchema = getSizingSchema;
67844
68516
  exports.getStrategySchema = getStrategySchema;
68517
+ exports.getStrategyStatus = getStrategyStatus;
67845
68518
  exports.getSymbol = getSymbol;
67846
68519
  exports.getTimestamp = getTimestamp;
67847
68520
  exports.getTotalClosed = getTotalClosed;