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.mjs CHANGED
@@ -981,6 +981,12 @@ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writ
981
981
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
982
982
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
983
983
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR = "PersistScheduleUtils.clear";
984
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER = "PersistStrategyUtils.usePersistStrategyAdapter";
985
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA = "PersistStrategyUtils.readStrategyData";
986
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA = "PersistStrategyUtils.writeStrategyData";
987
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON = "PersistStrategyUtils.useJson";
988
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY = "PersistStrategyUtils.useDummy";
989
+ const PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR = "PersistStrategyUtils.clear";
984
990
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
985
991
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
986
992
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
@@ -1844,6 +1850,212 @@ class PersistScheduleUtils {
1844
1850
  * ```
1845
1851
  */
1846
1852
  const PersistScheduleAdapter = new PersistScheduleUtils();
1853
+ /**
1854
+ * Default file-based implementation of IPersistStrategyInstance.
1855
+ *
1856
+ * Features:
1857
+ * - Wraps PersistBase for atomic JSON writes
1858
+ * - Uses fixed entity ID "strategy" within a per-context PersistBase
1859
+ * - Crash-safe via atomic writes
1860
+ *
1861
+ * @example
1862
+ * ```typescript
1863
+ * const instance = new PersistStrategyInstance("BTCUSDT", "my-strategy", "binance");
1864
+ * await instance.waitForInit(true);
1865
+ * await instance.writeStrategyData(snapshot);
1866
+ * const restored = await instance.readStrategyData();
1867
+ * ```
1868
+ */
1869
+ class PersistStrategyInstance {
1870
+ /**
1871
+ * Creates new deferred strategy state persistence instance.
1872
+ *
1873
+ * @param symbol - Trading pair symbol
1874
+ * @param strategyName - Strategy identifier
1875
+ * @param exchangeName - Exchange identifier
1876
+ */
1877
+ constructor(symbol, strategyName, exchangeName) {
1878
+ this.symbol = symbol;
1879
+ this.strategyName = strategyName;
1880
+ this.exchangeName = exchangeName;
1881
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/strategy/`);
1882
+ }
1883
+ /**
1884
+ * Initializes the underlying PersistBase storage.
1885
+ *
1886
+ * @param initial - Whether this is the first initialization
1887
+ * @returns Promise that resolves when initialization is complete
1888
+ */
1889
+ async waitForInit(initial) {
1890
+ await this._storage.waitForInit(initial);
1891
+ }
1892
+ /**
1893
+ * Reads the persisted strategy state snapshot using the fixed STORAGE_KEY.
1894
+ *
1895
+ * @returns Promise resolving to strategy state snapshot or null if not found
1896
+ */
1897
+ async readStrategyData() {
1898
+ if (await this._storage.hasValue(PersistStrategyInstance.STORAGE_KEY)) {
1899
+ const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
1900
+ // JSON serializes Infinity as null, so an eternal-hold signal
1901
+ // (minuteEstimatedTime: Infinity) reads back as null — restore it.
1902
+ for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
1903
+ if (signal && signal.minuteEstimatedTime == null) {
1904
+ signal.minuteEstimatedTime = Infinity;
1905
+ }
1906
+ }
1907
+ return strategyData;
1908
+ }
1909
+ return null;
1910
+ }
1911
+ /**
1912
+ * Writes the strategy state snapshot (or null to clear) using the fixed STORAGE_KEY.
1913
+ *
1914
+ * @param row - Strategy state snapshot to persist, or null to clear
1915
+ * @returns Promise that resolves when write is complete
1916
+ */
1917
+ async writeStrategyData(row) {
1918
+ await this._storage.writeValue(PersistStrategyInstance.STORAGE_KEY, row);
1919
+ }
1920
+ }
1921
+ /** Fixed entity key for storing the strategy state snapshot */
1922
+ PersistStrategyInstance.STORAGE_KEY = "strategy";
1923
+ /**
1924
+ * No-op IPersistStrategyInstance implementation used by PersistStrategyUtils.useDummy().
1925
+ * All reads return null, all writes are discarded.
1926
+ */
1927
+ class PersistStrategyDummyInstance {
1928
+ /**
1929
+ * No-op constructor.
1930
+ * Context arguments are accepted to satisfy TPersistStrategyInstanceCtor.
1931
+ */
1932
+ constructor(_symbol, _strategyName, _exchangeName) { }
1933
+ /**
1934
+ * No-op initialization.
1935
+ * @returns Promise that resolves immediately
1936
+ */
1937
+ async waitForInit(_initial) { }
1938
+ /**
1939
+ * Always returns null (no persisted strategy state).
1940
+ * @returns Promise resolving to null
1941
+ */
1942
+ async readStrategyData() { return null; }
1943
+ /**
1944
+ * No-op write (discards strategy state).
1945
+ * @returns Promise that resolves immediately
1946
+ */
1947
+ async writeStrategyData(_row) { }
1948
+ }
1949
+ /**
1950
+ * Utility class for managing deferred strategy state persistence.
1951
+ *
1952
+ * Features:
1953
+ * - Memoized storage instances per strategy
1954
+ * - Custom adapter support
1955
+ * - Atomic read/write operations for the deferred state snapshot
1956
+ * - Crash-safe in-flight broker operation state management
1957
+ *
1958
+ * Used by ClientStrategy for live mode persistence of the commit queue and deferred
1959
+ * user actions (_commitQueue, _closedSignal, _cancelledSignal, _activatedSignal).
1960
+ */
1961
+ class PersistStrategyUtils {
1962
+ constructor() {
1963
+ /**
1964
+ * Constructor used to create per-context strategy state instances.
1965
+ * Replaceable via usePersistStrategyAdapter() / useJson() / useDummy().
1966
+ */
1967
+ this.PersistStrategyInstanceCtor = PersistStrategyInstance;
1968
+ /**
1969
+ * Memoized factory creating one IPersistStrategyInstance per (symbol, strategy, exchange) triple.
1970
+ */
1971
+ this.getStrategyStorage = memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistStrategyInstanceCtor, [symbol, strategyName, exchangeName]));
1972
+ /**
1973
+ * Reads persisted deferred strategy state for the given context.
1974
+ * Lazily initializes the instance on first access.
1975
+ *
1976
+ * @param symbol - Trading pair symbol
1977
+ * @param strategyName - Strategy identifier
1978
+ * @param exchangeName - Exchange identifier
1979
+ * @returns Promise resolving to strategy state snapshot or null if none persisted
1980
+ */
1981
+ this.readStrategyData = async (symbol, strategyName, exchangeName) => {
1982
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA);
1983
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
1984
+ const isInitial = !this.getStrategyStorage.has(key);
1985
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
1986
+ await instance.waitForInit(isInitial);
1987
+ return instance.readStrategyData();
1988
+ };
1989
+ /**
1990
+ * Writes deferred strategy state (or null to clear) for the given context.
1991
+ * Lazily initializes the instance on first access.
1992
+ *
1993
+ * @param strategyRow - Strategy state snapshot to persist, or null to clear
1994
+ * @param symbol - Trading pair symbol
1995
+ * @param strategyName - Strategy identifier
1996
+ * @param exchangeName - Exchange identifier
1997
+ * @returns Promise that resolves when write is complete
1998
+ */
1999
+ this.writeStrategyData = async (strategyRow, symbol, strategyName, exchangeName) => {
2000
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA);
2001
+ const key = `${symbol}:${strategyName}:${exchangeName}`;
2002
+ const isInitial = !this.getStrategyStorage.has(key);
2003
+ const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
2004
+ await instance.waitForInit(isInitial);
2005
+ return instance.writeStrategyData(strategyRow);
2006
+ };
2007
+ }
2008
+ /**
2009
+ * Registers a custom IPersistStrategyInstance constructor.
2010
+ * Clears the memoization cache so subsequent calls use the new adapter.
2011
+ *
2012
+ * @param Ctor - Custom IPersistStrategyInstance constructor
2013
+ */
2014
+ usePersistStrategyAdapter(Ctor) {
2015
+ LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER);
2016
+ this.PersistStrategyInstanceCtor = Ctor;
2017
+ this.getStrategyStorage.clear();
2018
+ }
2019
+ /**
2020
+ * Clears the memoized instance cache.
2021
+ * Call when process.cwd() changes between strategy iterations.
2022
+ */
2023
+ clear() {
2024
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR);
2025
+ this.getStrategyStorage.clear();
2026
+ }
2027
+ /**
2028
+ * Switches to the default file-based PersistStrategyInstance.
2029
+ */
2030
+ useJson() {
2031
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON);
2032
+ this.usePersistStrategyAdapter(PersistStrategyInstance);
2033
+ }
2034
+ /**
2035
+ * Switches to PersistStrategyDummyInstance (all operations are no-ops).
2036
+ */
2037
+ useDummy() {
2038
+ LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY);
2039
+ this.usePersistStrategyAdapter(PersistStrategyDummyInstance);
2040
+ }
2041
+ }
2042
+ /**
2043
+ * Global singleton instance of PersistStrategyUtils.
2044
+ * Used by ClientStrategy for deferred strategy state persistence.
2045
+ *
2046
+ * @example
2047
+ * ```typescript
2048
+ * // Custom adapter
2049
+ * PersistStrategyAdapter.usePersistStrategyAdapter(RedisPersist);
2050
+ *
2051
+ * // Read strategy state
2052
+ * const state = await PersistStrategyAdapter.readStrategyData("BTCUSDT", "my-strategy", "binance");
2053
+ *
2054
+ * // Write strategy state
2055
+ * await PersistStrategyAdapter.writeStrategyData(state, "BTCUSDT", "my-strategy", "binance");
2056
+ * ```
2057
+ */
2058
+ const PersistStrategyAdapter = new PersistStrategyUtils();
1847
2059
  /**
1848
2060
  * Default file-based implementation of IPersistPartialInstance.
1849
2061
  *
@@ -6605,6 +6817,119 @@ const validateScheduledSignal = (signal, currentPrice) => {
6605
6817
  }
6606
6818
  };
6607
6819
 
6820
+ /**
6821
+ * Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
6822
+ *
6823
+ * When priceOpen is provided:
6824
+ * - If currentPrice already reached priceOpen (shouldActivateImmediately) →
6825
+ * validates as pending: currentPrice must be between SL and TP
6826
+ * - Otherwise → validates as scheduled: priceOpen must be between SL and TP
6827
+ *
6828
+ * When priceOpen is absent:
6829
+ * - Validates as pending: currentPrice must be between SL and TP
6830
+ *
6831
+ * Checks:
6832
+ * - currentPrice is a finite positive number
6833
+ * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
6834
+ * - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
6835
+ *
6836
+ * @param signal - Signal DTO returned by getSignal
6837
+ * @param currentPrice - Current market price at the moment of signal creation
6838
+ * @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
6839
+ */
6840
+ const validateSignal = (signal, currentPrice) => {
6841
+ const errors = [];
6842
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
6843
+ {
6844
+ if (typeof currentPrice !== "number") {
6845
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
6846
+ }
6847
+ if (!isFinite(currentPrice)) {
6848
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
6849
+ }
6850
+ if (isFinite(currentPrice) && currentPrice <= 0) {
6851
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
6852
+ }
6853
+ }
6854
+ if (errors.length > 0) {
6855
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6856
+ return false;
6857
+ }
6858
+ try {
6859
+ validateCommonSignal(signal);
6860
+ }
6861
+ catch (error) {
6862
+ console.error(getErrorMessage(error));
6863
+ return false;
6864
+ }
6865
+ // Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
6866
+ // - нет priceOpen → pending (открывается по currentPrice)
6867
+ // - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
6868
+ // - priceOpen задан и ещё не достигнут → scheduled
6869
+ const hasPriceOpen = signal.priceOpen !== undefined;
6870
+ const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
6871
+ (signal.position === "short" && currentPrice >= signal.priceOpen));
6872
+ const isScheduled = hasPriceOpen && !shouldActivateImmediately;
6873
+ if (isScheduled) {
6874
+ // Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
6875
+ if (signal.position === "long") {
6876
+ if (isFinite(signal.priceOpen)) {
6877
+ if (signal.priceOpen <= signal.priceStopLoss) {
6878
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
6879
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
6880
+ }
6881
+ if (signal.priceOpen >= signal.priceTakeProfit) {
6882
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
6883
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
6884
+ }
6885
+ }
6886
+ }
6887
+ if (signal.position === "short") {
6888
+ if (isFinite(signal.priceOpen)) {
6889
+ if (signal.priceOpen >= signal.priceStopLoss) {
6890
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
6891
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
6892
+ }
6893
+ if (signal.priceOpen <= signal.priceTakeProfit) {
6894
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
6895
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
6896
+ }
6897
+ }
6898
+ }
6899
+ }
6900
+ else {
6901
+ // Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
6902
+ if (signal.position === "long") {
6903
+ if (isFinite(currentPrice)) {
6904
+ if (currentPrice <= signal.priceStopLoss) {
6905
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
6906
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
6907
+ }
6908
+ if (currentPrice >= signal.priceTakeProfit) {
6909
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
6910
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
6911
+ }
6912
+ }
6913
+ }
6914
+ if (signal.position === "short") {
6915
+ if (isFinite(currentPrice)) {
6916
+ if (currentPrice >= signal.priceStopLoss) {
6917
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
6918
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
6919
+ }
6920
+ if (currentPrice <= signal.priceTakeProfit) {
6921
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
6922
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
6923
+ }
6924
+ }
6925
+ }
6926
+ }
6927
+ if (errors.length > 0) {
6928
+ console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
6929
+ }
6930
+ return !errors.length;
6931
+ };
6932
+
6608
6933
  const INTERVAL_MINUTES$8 = {
6609
6934
  "1m": 1,
6610
6935
  "3m": 3,
@@ -6771,6 +7096,9 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
6771
7096
  {
6772
7097
  self._commitQueue = [];
6773
7098
  }
7099
+ // Persist the now-empty queue so a crash after draining does not replay commits
7100
+ // that were already forwarded to the broker on the next restart.
7101
+ await PERSIST_STRATEGY_FN(self);
6774
7102
  if (!self._pendingSignal) {
6775
7103
  return;
6776
7104
  }
@@ -7068,11 +7396,27 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
7068
7396
  self._lastSignalTimestamp = alignedTime;
7069
7397
  }
7070
7398
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
7071
- const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
7072
- const signal = await Promise.race([
7073
- self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
7074
- sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
7075
- ]);
7399
+ // PRIORITY: a user-queued createPending/createScheduled DTO (set out of async-hooks
7400
+ // context) takes precedence over params.getSignal. When present it is consumed once
7401
+ // here — the slot is cleared and the snapshot rewritten — and then flows through the
7402
+ // exact same pipeline (risk check, priceOpen branching, onSignalSync on open) as a
7403
+ // signal returned by getSignal would.
7404
+ let signal;
7405
+ {
7406
+ if (!self._userSignal) {
7407
+ const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
7408
+ signal = await Promise.race([
7409
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
7410
+ sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
7411
+ ]);
7412
+ }
7413
+ if (self._userSignal) {
7414
+ signal = self._userSignal;
7415
+ self._userSignal = null;
7416
+ await PERSIST_STRATEGY_FN(self);
7417
+ }
7418
+ self._userSignal = null;
7419
+ }
7076
7420
  if (typeof signal === "symbol") {
7077
7421
  throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
7078
7422
  }
@@ -7227,6 +7571,21 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7227
7571
  self._lastPendingId = recentSignal.id;
7228
7572
  }
7229
7573
  }
7574
+ // Read deferred strategy state (commit queue + deferred user actions) so any
7575
+ // confirmed-but-not-yet-forwarded broker operation survives a live crash and is
7576
+ // re-drained on the next tick. Read here, before the pending restore which may
7577
+ // early-return on exchange/strategy mismatch.
7578
+ const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7579
+ if (strategyData) {
7580
+ // Deferred user actions are restored unconditionally: a deferred close belongs to
7581
+ // an already-cleared _pendingSignal, and cancel/activate belong to the scheduled
7582
+ // signal — none of them are tied to the currently-pending signal's id. A queued
7583
+ // createSignal is a not-yet-consumed signal source and likewise stands on its own.
7584
+ self._userSignal = strategyData.createSignal;
7585
+ self._closedSignal = strategyData.closedSignal;
7586
+ self._cancelledSignal = strategyData.cancelledSignal;
7587
+ self._activatedSignal = strategyData.activatedSignal;
7588
+ }
7230
7589
  // Restore pending signal
7231
7590
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
7232
7591
  if (pendingSignal) {
@@ -7243,6 +7602,14 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7243
7602
  pendingSignal.minuteEstimatedTime = Infinity;
7244
7603
  }
7245
7604
  self._pendingSignal = pendingSignal;
7605
+ // Restore the commit queue only if the snapshot belongs to this exact pending
7606
+ // signal (pendingSignalId === restored id). The queue holds confirmed-but-not-yet
7607
+ // forwarded broker ops (average-buy / partial-* / trailing-* / breakeven) that are
7608
+ // always tied to the active position; a mismatch means the snapshot is stale and
7609
+ // the queue is dropped to avoid replaying ops against the wrong position.
7610
+ if (strategyData && strategyData.pendingSignalId === pendingSignal.id) {
7611
+ self._commitQueue = strategyData.commitQueue ?? [];
7612
+ }
7246
7613
  // Call onActive callback for restored signal
7247
7614
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
7248
7615
  const currentTime = self.params.execution.context.when.getTime();
@@ -7276,6 +7643,30 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
7276
7643
  self.params.logger.debug("ClientStrategy dispose");
7277
7644
  await self.params.onDispose(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, self.params.execution.context.backtest);
7278
7645
  };
7646
+ /**
7647
+ * Persists the deferred strategy state snapshot (commit queue + deferred user actions)
7648
+ * to disk in live mode.
7649
+ *
7650
+ * These fields carry confirmed-but-not-yet-forwarded broker operations and survive the
7651
+ * gap between ticks (drained at the start of the next tick). Without persistence a live
7652
+ * crash in that window silently loses the pending broker operation while _pendingSignal
7653
+ * (already mutated and saved) claims it happened. Skipped in backtest mode.
7654
+ *
7655
+ * @param self - ClientStrategy instance
7656
+ */
7657
+ const PERSIST_STRATEGY_FN = async (self) => {
7658
+ if (self.params.backtest) {
7659
+ return;
7660
+ }
7661
+ await PersistStrategyAdapter.writeStrategyData({
7662
+ pendingSignalId: self._pendingSignal?.id ?? null,
7663
+ createSignal: self._userSignal,
7664
+ commitQueue: self._commitQueue,
7665
+ closedSignal: self._closedSignal,
7666
+ cancelledSignal: self._cancelledSignal,
7667
+ activatedSignal: self._activatedSignal,
7668
+ }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7669
+ };
7279
7670
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
7280
7671
  // Initialize partial array if not present
7281
7672
  if (!signal._partial)
@@ -8900,14 +9291,15 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
8900
9291
  const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
8901
9292
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
8902
9293
  if (!syncCloseAllowed) {
8903
- self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
9294
+ // Sync close rejected (e.g. broker rejected the order) keep _closedSignal intact
9295
+ // and return null so the candle loop re-attempts on the next candle. Mirrors live
9296
+ // tick, which keeps _closedSignal and returns idle on a rejected user close,
9297
+ // re-trying on the following tick.
9298
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry on next candle", {
8904
9299
  symbol: self.params.execution.context.symbol,
8905
9300
  signalId: closedSignal.id,
8906
9301
  });
8907
- self._closedSignal = null;
8908
- self._pendingSignal = closedSignal;
8909
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
8910
- `Retry backtest() with new candle data.`);
9302
+ return null;
8911
9303
  }
8912
9304
  self._closedSignal = null;
8913
9305
  const publicSignal = TO_PUBLIC_SIGNAL("pending", closedSignal, averagePrice);
@@ -9159,7 +9551,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9159
9551
  }
9160
9552
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
9161
9553
  if (self._closedSignal) {
9162
- return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9554
+ const userCloseResult = await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
9555
+ // Sync close accepted — position closed, return. If rejected, _closedSignal is
9556
+ // kept and we skip this candle: live tick returns idle on a rejected user close
9557
+ // (it does NOT fall into the TP/SL or active-monitoring path), so the candle is
9558
+ // skipped here and the close is re-attempted on the next candle.
9559
+ if (userCloseResult) {
9560
+ return userCloseResult;
9561
+ }
9562
+ continue;
9163
9563
  }
9164
9564
  let shouldClose = false;
9165
9565
  let closeReason;
@@ -9210,7 +9610,19 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9210
9610
  else {
9211
9611
  closePrice = averagePrice; // time_expired uses VWAP
9212
9612
  }
9213
- return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9613
+ const closeResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
9614
+ // Sync close accepted — position closed, return.
9615
+ if (closeResult) {
9616
+ return closeResult;
9617
+ }
9618
+ // Sync close rejected (e.g. broker rejected the order) — _pendingSignal is left
9619
+ // intact (not cleared on rejection). Do NOT return/continue: fall through to the
9620
+ // active-monitoring block below so this candle is processed exactly like live
9621
+ // tick, which runs RETURN_PENDING_SIGNAL_ACTIVE_FN when CLOSE_PENDING_SIGNAL_FN
9622
+ // returns null (updates _peak/_fall, fires active ping / breakeven / partial
9623
+ // callbacks, drains the commit queue). The close is re-attempted on the next
9624
+ // candle; for time_expired this eventually reaches the loop-exhausted close
9625
+ // (which throws if still rejected).
9214
9626
  }
9215
9627
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
9216
9628
  // Calculate percentage of path to TP/SL
@@ -9384,6 +9796,13 @@ class ClientStrategy {
9384
9796
  this._cancelledSignal = null;
9385
9797
  this._closedSignal = null;
9386
9798
  this._activatedSignal = null;
9799
+ /**
9800
+ * User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
9801
+ * params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
9802
+ * and the existing pipeline (priceOpen decides pending vs scheduled, onSignalSync on open)
9803
+ * is reused.
9804
+ */
9805
+ this._userSignal = null;
9387
9806
  /** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
9388
9807
  this._commitQueue = [];
9389
9808
  /**
@@ -9632,7 +10051,7 @@ class ClientStrategy {
9632
10051
  async getStopped(symbol) {
9633
10052
  this.params.logger.debug("ClientStrategy getStopped", {
9634
10053
  symbol,
9635
- strategyName: this.params.method.context.strategyName,
10054
+ strategyName: this.params.strategyName,
9636
10055
  });
9637
10056
  return this._isStopped;
9638
10057
  }
@@ -10315,6 +10734,8 @@ class ClientStrategy {
10315
10734
  if (this._cancelledSignal) {
10316
10735
  const cancelledSignal = this._cancelledSignal;
10317
10736
  this._cancelledSignal = null; // Clear after emitting
10737
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10738
+ await PERSIST_STRATEGY_FN(this);
10318
10739
  this.params.logger.info("ClientStrategy tick: scheduled signal was cancelled", {
10319
10740
  symbol: this.params.execution.context.symbol,
10320
10741
  signalId: cancelledSignal.id,
@@ -10373,6 +10794,8 @@ class ClientStrategy {
10373
10794
  return await RETURN_IDLE_FN(this, currentPrice);
10374
10795
  }
10375
10796
  this._closedSignal = null; // Clear only after sync confirmed
10797
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10798
+ await PERSIST_STRATEGY_FN(this);
10376
10799
  this.params.logger.info("ClientStrategy tick: pending signal was closed", {
10377
10800
  symbol: this.params.execution.context.symbol,
10378
10801
  signalId: closedSignal.id,
@@ -10428,6 +10851,8 @@ class ClientStrategy {
10428
10851
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
10429
10852
  const activatedSignal = this._activatedSignal;
10430
10853
  this._activatedSignal = null; // Clear after emitting
10854
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
10855
+ await PERSIST_STRATEGY_FN(this);
10431
10856
  this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
10432
10857
  symbol: this.params.execution.context.symbol,
10433
10858
  signalId: activatedSignal.id,
@@ -10685,18 +11110,20 @@ class ClientStrategy {
10685
11110
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
10686
11111
  const closedSignal = this._closedSignal;
10687
11112
  const closeTimestamp = this.params.execution.context.when.getTime();
10688
- // Sync close: if external system rejects — restore _pendingSignal, retry on next backtest() call
11113
+ // Sync close: if external system rejects — keep the close pending and re-attempt
11114
+ // it inside the candle loop below (PROCESS_PENDING_SIGNAL_CANDLES_FN handles
11115
+ // _closedSignal per candle). Mirrors live tick, which keeps _closedSignal and
11116
+ // retries on the next tick instead of failing.
10689
11117
  const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, currentPrice, "closed", closedSignal, this);
10690
11118
  if (!syncCloseAllowed) {
10691
- this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
11119
+ this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry in candle loop", {
10692
11120
  symbol: this.params.execution.context.symbol,
10693
11121
  signalId: closedSignal.id,
10694
11122
  });
10695
- // Restore _pendingSignal so next backtest() call can process it normally
10696
- this._closedSignal = null;
11123
+ // Restore _pendingSignal so the candle loop processes the position normally;
11124
+ // _closedSignal is kept so the loop re-attempts the close on each candle.
10697
11125
  this._pendingSignal = closedSignal;
10698
- throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
10699
- `Retry backtest() with new candle data.`);
11126
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, this._pendingSignal, candles, frameEndTime);
10700
11127
  }
10701
11128
  this._closedSignal = null; // Clear only after sync confirmed
10702
11129
  // Emit commit with correct timestamp from backtest context
@@ -10878,7 +11305,7 @@ class ClientStrategy {
10878
11305
  if (backtest) {
10879
11306
  return;
10880
11307
  }
10881
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11308
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
10882
11309
  }
10883
11310
  /**
10884
11311
  * Cancels the scheduled signal without stopping the strategy.
@@ -10922,7 +11349,9 @@ class ClientStrategy {
10922
11349
  // Commit will be emitted in backtest() with correct candle timestamp
10923
11350
  return;
10924
11351
  }
10925
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11352
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11353
+ // Persist deferred _cancelledSignal so a crash before the next tick does not lose it
11354
+ await PERSIST_STRATEGY_FN(this);
10926
11355
  // Commit will be emitted in tick() with correct currentTime
10927
11356
  }
10928
11357
  /**
@@ -10973,7 +11402,9 @@ class ClientStrategy {
10973
11402
  // Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
10974
11403
  return;
10975
11404
  }
10976
- await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
11405
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
11406
+ // Persist deferred _activatedSignal so a crash before the next tick does not lose it
11407
+ await PERSIST_STRATEGY_FN(this);
10977
11408
  // Commit will be emitted AFTER successful risk check in tick()
10978
11409
  }
10979
11410
  /**
@@ -11018,8 +11449,92 @@ class ClientStrategy {
11018
11449
  return;
11019
11450
  }
11020
11451
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11452
+ // Persist deferred _closedSignal so a crash before the next tick does not lose it
11453
+ await PERSIST_STRATEGY_FN(this);
11021
11454
  // Commit will be emitted in tick() with correct currentTime
11022
11455
  }
11456
+ /**
11457
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of
11458
+ * params.getSignal.
11459
+ *
11460
+ * Works OUT of the async-hooks execution context (uses this.params.symbol directly).
11461
+ * priceOpen decides the outcome in the existing pipeline: when omitted the position opens
11462
+ * immediately at currentPrice; when provided the pipeline opens immediately if priceOpen is
11463
+ * already reached, otherwise registers a scheduled (priceOpen-awaiting) signal. On the next
11464
+ * tick GET_SIGNAL_FN consumes _userSignal and runs the normal pipeline, so onSignalSync
11465
+ * delivery is checked by OPEN_NEW_PENDING_SIGNAL_FN exactly as for a getSignal-produced signal.
11466
+ *
11467
+ * Validation (BEFORE any state mutation — a rejected call stores nothing):
11468
+ * - The DTO must pass the intrinsic shape/price checks.
11469
+ * - There must be NO signal already in flight: no active pending signal, no scheduled signal,
11470
+ * no already-queued createSignal, and no deferred activate/close/cancel awaiting drain.
11471
+ * Creating a new signal on top of an existing one is rejected.
11472
+ *
11473
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11474
+ * @param currentPrice - Current market price (priceOpen fallback for immediate signals)
11475
+ * @param dto - Signal DTO to open (priceOpen optional)
11476
+ * @returns Promise that resolves when the DTO is queued (and persisted in live mode)
11477
+ * @throws {Error} If the DTO is invalid or a signal/deferred action is already in flight
11478
+ */
11479
+ async createSignal(symbol, currentPrice, dto) {
11480
+ this.params.logger.debug("ClientStrategy createSignal", { symbol, currentPrice });
11481
+ // Validate BEFORE mutating state — a bad DTO or a busy strategy must store nothing.
11482
+ // Reuse validateSignal (the canonical getSignal-output validator, branching
11483
+ // pending-vs-scheduled by the same rule as GET_SIGNAL_FN). It forwards to
11484
+ // validateCommonSignal which requires priceOpen and minuteEstimatedTime, so normalize the
11485
+ // DTO to the defaults GET_SIGNAL_FN would apply (priceOpen → currentPrice for an immediate
11486
+ // signal, minuteEstimatedTime → CC_MAX_SIGNAL_LIFETIME_MINUTES). The original dto stored in
11487
+ // _userSignal is left untouched so GET_SIGNAL_FN applies its own defaults on consume.
11488
+ if (!validateSignal({
11489
+ ...dto,
11490
+ priceOpen: dto.priceOpen ?? currentPrice,
11491
+ minuteEstimatedTime: dto.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
11492
+ }, currentPrice)) {
11493
+ throw new Error(`ClientStrategy createSignal: invalid signal DTO for symbol=${symbol}`);
11494
+ }
11495
+ // Reject if any signal is already in flight or any deferred action is awaiting drain.
11496
+ if (this._pendingSignal) {
11497
+ throw new Error(`ClientStrategy createSignal: a pending signal already exists for symbol=${symbol}`);
11498
+ }
11499
+ if (this._scheduledSignal) {
11500
+ throw new Error(`ClientStrategy createSignal: a scheduled signal already exists for symbol=${symbol}`);
11501
+ }
11502
+ if (this._userSignal) {
11503
+ throw new Error(`ClientStrategy createSignal: a signal is already queued for creation for symbol=${symbol}`);
11504
+ }
11505
+ if (this._activatedSignal) {
11506
+ throw new Error(`ClientStrategy createSignal: a scheduled activation is pending for symbol=${symbol}`);
11507
+ }
11508
+ if (this._closedSignal) {
11509
+ throw new Error(`ClientStrategy createSignal: a pending close is awaiting for symbol=${symbol}`);
11510
+ }
11511
+ if (this._cancelledSignal) {
11512
+ throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
11513
+ }
11514
+ this._userSignal = dto;
11515
+ await PERSIST_STRATEGY_FN(this);
11516
+ }
11517
+ /**
11518
+ * Returns the deferred strategy-state snapshot exactly as it would be written to persist on
11519
+ * this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
11520
+ * plus the current pending signal id.
11521
+ *
11522
+ * Synchronous in-memory read (no disk access), so it works OUT of the async-hooks context.
11523
+ *
11524
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11525
+ * @returns The current StrategyData snapshot held in memory
11526
+ */
11527
+ async getStatus(symbol) {
11528
+ this.params.logger.debug("ClientStrategy getStatus", { symbol });
11529
+ return {
11530
+ pendingSignalId: this._pendingSignal?.id ?? null,
11531
+ createSignal: this._userSignal,
11532
+ commitQueue: this._commitQueue,
11533
+ closedSignal: this._closedSignal,
11534
+ cancelledSignal: this._cancelledSignal,
11535
+ activatedSignal: this._activatedSignal,
11536
+ };
11537
+ }
11023
11538
  /**
11024
11539
  * Validates preconditions for partialProfit without mutating state.
11025
11540
  *
@@ -11188,10 +11703,10 @@ class ClientStrategy {
11188
11703
  });
11189
11704
  // Call onWrite callback for testing persist storage
11190
11705
  if (this.params.callbacks?.onWrite) {
11191
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11706
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11192
11707
  }
11193
11708
  if (!backtest) {
11194
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11709
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11195
11710
  }
11196
11711
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11197
11712
  this._commitQueue.push({
@@ -11201,6 +11716,8 @@ class ClientStrategy {
11201
11716
  percentToClose,
11202
11717
  currentPrice,
11203
11718
  });
11719
+ // Persist the queued commit so a crash before the next tick does not lose it
11720
+ await PERSIST_STRATEGY_FN(this);
11204
11721
  return true;
11205
11722
  }
11206
11723
  /**
@@ -11371,10 +11888,10 @@ class ClientStrategy {
11371
11888
  });
11372
11889
  // Call onWrite callback for testing persist storage
11373
11890
  if (this.params.callbacks?.onWrite) {
11374
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11891
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
11375
11892
  }
11376
11893
  if (!backtest) {
11377
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
11894
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11378
11895
  }
11379
11896
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11380
11897
  this._commitQueue.push({
@@ -11384,6 +11901,8 @@ class ClientStrategy {
11384
11901
  percentToClose,
11385
11902
  currentPrice,
11386
11903
  });
11904
+ // Persist the queued commit so a crash before the next tick does not lose it
11905
+ await PERSIST_STRATEGY_FN(this);
11387
11906
  return true;
11388
11907
  }
11389
11908
  /**
@@ -11570,10 +12089,10 @@ class ClientStrategy {
11570
12089
  // Call onWrite callback for testing persist storage
11571
12090
  if (this.params.callbacks?.onWrite) {
11572
12091
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11573
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12092
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11574
12093
  }
11575
12094
  if (!backtest) {
11576
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12095
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11577
12096
  }
11578
12097
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11579
12098
  this._commitQueue.push({
@@ -11582,6 +12101,8 @@ class ClientStrategy {
11582
12101
  backtest,
11583
12102
  currentPrice,
11584
12103
  });
12104
+ // Persist the queued commit so a crash before the next tick does not lose it
12105
+ await PERSIST_STRATEGY_FN(this);
11585
12106
  return true;
11586
12107
  }
11587
12108
  /**
@@ -11819,10 +12340,10 @@ class ClientStrategy {
11819
12340
  // Call onWrite callback for testing persist storage
11820
12341
  if (this.params.callbacks?.onWrite) {
11821
12342
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
11822
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12343
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
11823
12344
  }
11824
12345
  if (!backtest) {
11825
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12346
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
11826
12347
  }
11827
12348
  // Queue commit event for processing in tick()/backtest() with proper timestamp
11828
12349
  this._commitQueue.push({
@@ -11832,6 +12353,8 @@ class ClientStrategy {
11832
12353
  percentShift,
11833
12354
  currentPrice,
11834
12355
  });
12356
+ // Persist the queued commit so a crash before the next tick does not lose it
12357
+ await PERSIST_STRATEGY_FN(this);
11835
12358
  return true;
11836
12359
  }
11837
12360
  /**
@@ -12055,10 +12578,10 @@ class ClientStrategy {
12055
12578
  // Call onWrite callback for testing persist storage
12056
12579
  if (this.params.callbacks?.onWrite) {
12057
12580
  const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
12058
- this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
12581
+ this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
12059
12582
  }
12060
12583
  if (!backtest) {
12061
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12584
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12062
12585
  }
12063
12586
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12064
12587
  this._commitQueue.push({
@@ -12068,6 +12591,8 @@ class ClientStrategy {
12068
12591
  percentShift,
12069
12592
  currentPrice,
12070
12593
  });
12594
+ // Persist the queued commit so a crash before the next tick does not lose it
12595
+ await PERSIST_STRATEGY_FN(this);
12071
12596
  return true;
12072
12597
  }
12073
12598
  /**
@@ -12146,10 +12671,10 @@ class ClientStrategy {
12146
12671
  });
12147
12672
  // Call onWrite callback for testing persist storage
12148
12673
  if (this.params.callbacks?.onWrite) {
12149
- this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12674
+ this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
12150
12675
  }
12151
12676
  if (!backtest) {
12152
- await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
12677
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
12153
12678
  }
12154
12679
  // Queue commit event for processing in tick()/backtest() with proper timestamp
12155
12680
  this._commitQueue.push({
@@ -12160,6 +12685,8 @@ class ClientStrategy {
12160
12685
  cost,
12161
12686
  totalEntries: this._pendingSignal._entry?.length ?? 1,
12162
12687
  });
12688
+ // Persist the queued commit so a crash before the next tick does not lose it
12689
+ await PERSIST_STRATEGY_FN(this);
12163
12690
  return true;
12164
12691
  }
12165
12692
  }
@@ -12306,6 +12833,8 @@ class MergeRisk {
12306
12833
 
12307
12834
  /** Default interval for strategies that do not specify one */
12308
12835
  const STRATEGY_DEFAULT_INTERVAL = "1m";
12836
+ /** If not specified strategy will not open positions until createSignal is called manually */
12837
+ const STRATEGY_DEFAULT_SIGNAL = () => null;
12309
12838
  /**
12310
12839
  * If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
12311
12840
  * to the exchange (e.g. limit order failed to fill).
@@ -12739,7 +13268,7 @@ class StrategyConnectionService {
12739
13268
  * @returns Configured ClientStrategy instance
12740
13269
  */
12741
13270
  this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12742
- const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
13271
+ const { riskName = "", riskList = [], getSignal = STRATEGY_DEFAULT_SIGNAL, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
12743
13272
  return new ClientStrategy({
12744
13273
  symbol,
12745
13274
  interval,
@@ -13758,6 +14287,47 @@ class StrategyConnectionService {
13758
14287
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
13759
14288
  await strategy.closePending(symbol, backtest, payload);
13760
14289
  };
14290
+ /**
14291
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
14292
+ *
14293
+ * Delegates to ClientStrategy.createSignal(). Validated and rejected if a signal/deferred
14294
+ * action is already in flight. Works out of the async-hooks execution context.
14295
+ *
14296
+ * @param backtest - Whether running in backtest mode
14297
+ * @param symbol - Trading pair symbol
14298
+ * @param dto - Signal DTO to open
14299
+ * @param context - Context with strategyName, exchangeName, frameName
14300
+ * @returns Promise that resolves when the DTO is queued
14301
+ */
14302
+ this.createSignal = async (backtest, symbol, dto, context) => {
14303
+ this.loggerService.log("strategyConnectionService createSignal", {
14304
+ symbol,
14305
+ context,
14306
+ backtest,
14307
+ });
14308
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14309
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
14310
+ await strategy.createSignal(symbol, currentPrice, dto);
14311
+ };
14312
+ /**
14313
+ * Returns the in-memory deferred strategy-state snapshot for this iteration.
14314
+ *
14315
+ * Delegates to ClientStrategy.getStatus(). Synchronous in-memory read; works out of context.
14316
+ *
14317
+ * @param backtest - Whether running in backtest mode
14318
+ * @param symbol - Trading pair symbol
14319
+ * @param context - Context with strategyName, exchangeName, frameName
14320
+ * @returns Promise resolving to the current StrategyData snapshot
14321
+ */
14322
+ this.getStatus = async (backtest, symbol, context) => {
14323
+ this.loggerService.log("strategyConnectionService getStatus", {
14324
+ symbol,
14325
+ context,
14326
+ backtest,
14327
+ });
14328
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14329
+ return await strategy.getStatus(symbol);
14330
+ };
13761
14331
  /**
13762
14332
  * Checks whether `partialProfit` would succeed without executing it.
13763
14333
  * Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
@@ -17361,6 +17931,47 @@ class StrategyCoreService {
17361
17931
  await this.validate(context);
17362
17932
  return await this.strategyConnectionService.closePending(backtest, symbol, context, payload);
17363
17933
  };
17934
+ /**
17935
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
17936
+ *
17937
+ * Validates the context, then delegates to StrategyConnectionService.createSignal().
17938
+ * Rejected if a signal or deferred action is already in flight. Does not require execution context.
17939
+ *
17940
+ * @param backtest - Whether running in backtest mode
17941
+ * @param symbol - Trading pair symbol
17942
+ * @param dto - Signal DTO to open
17943
+ * @param context - Context with strategyName, exchangeName, frameName
17944
+ * @returns Promise that resolves when the DTO is queued
17945
+ */
17946
+ this.createSignal = async (backtest, symbol, dto, context) => {
17947
+ this.loggerService.log("strategyCoreService createSignal", {
17948
+ symbol,
17949
+ context,
17950
+ backtest,
17951
+ });
17952
+ await this.validate(context);
17953
+ return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
17954
+ };
17955
+ /**
17956
+ * Returns the in-memory deferred strategy-state snapshot for this iteration.
17957
+ *
17958
+ * Validates the context, then delegates to StrategyConnectionService.getStatus().
17959
+ * Does not require execution context.
17960
+ *
17961
+ * @param backtest - Whether running in backtest mode
17962
+ * @param symbol - Trading pair symbol
17963
+ * @param context - Context with strategyName, exchangeName, frameName
17964
+ * @returns Promise resolving to the current StrategyStatus snapshot
17965
+ */
17966
+ this.getStatus = async (backtest, symbol, context) => {
17967
+ this.loggerService.log("strategyCoreService getStatus", {
17968
+ symbol,
17969
+ context,
17970
+ backtest,
17971
+ });
17972
+ await this.validate(context);
17973
+ return await this.strategyConnectionService.getStatus(backtest, symbol, context);
17974
+ };
17364
17975
  /**
17365
17976
  * Disposes the ClientStrategy instance for the given context.
17366
17977
  *
@@ -18885,7 +19496,7 @@ class StrategySchemaService {
18885
19496
  if (strategySchema.interval && typeof strategySchema.interval !== "string") {
18886
19497
  throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
18887
19498
  }
18888
- if (typeof strategySchema.getSignal !== "function") {
19499
+ if (strategySchema.getSignal && typeof strategySchema.getSignal !== "function") {
18889
19500
  throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
18890
19501
  }
18891
19502
  };
@@ -22900,32 +23511,32 @@ const heat_columns = [
22900
23511
  {
22901
23512
  key: "totalPnl",
22902
23513
  label: "Total PNL",
22903
- format: (data) => data.totalPnl !== null ? str(data.totalPnl, "%") : "N/A",
23514
+ format: (data) => data.totalPnl !== null ? `${data.totalPnl.toFixed(2)}%` : "N/A",
22904
23515
  isVisible: () => true,
22905
23516
  },
22906
23517
  {
22907
23518
  key: "sharpeRatio",
22908
23519
  label: "Sharpe",
22909
- format: (data) => data.sharpeRatio !== null ? str(data.sharpeRatio) : "N/A",
23520
+ format: (data) => data.sharpeRatio !== null ? data.sharpeRatio.toFixed(3) : "N/A",
22910
23521
  isVisible: () => true,
22911
23522
  },
22912
23523
  {
22913
23524
  key: "annualizedSharpeRatio",
22914
23525
  label: "Ann Sharpe",
22915
- format: (data) => data.annualizedSharpeRatio !== null ? str(data.annualizedSharpeRatio) : "N/A",
23526
+ format: (data) => data.annualizedSharpeRatio !== null ? data.annualizedSharpeRatio.toFixed(3) : "N/A",
22916
23527
  isVisible: () => true,
22917
23528
  },
22918
23529
  {
22919
23530
  key: "certaintyRatio",
22920
23531
  label: "Certainty",
22921
- format: (data) => data.certaintyRatio !== null ? str(data.certaintyRatio) : "N/A",
23532
+ format: (data) => data.certaintyRatio !== null ? data.certaintyRatio.toFixed(3) : "N/A",
22922
23533
  isVisible: () => true,
22923
23534
  },
22924
23535
  {
22925
23536
  key: "expectedYearlyReturns",
22926
23537
  label: "Exp Yearly",
22927
23538
  format: (data) => data.expectedYearlyReturns !== null
22928
- ? str(data.expectedYearlyReturns, "%")
23539
+ ? `${data.expectedYearlyReturns.toFixed(2)}%`
22929
23540
  : "N/A",
22930
23541
  isVisible: () => true,
22931
23542
  },
@@ -22938,37 +23549,37 @@ const heat_columns = [
22938
23549
  {
22939
23550
  key: "profitFactor",
22940
23551
  label: "PF",
22941
- format: (data) => data.profitFactor !== null ? str(data.profitFactor) : "N/A",
23552
+ format: (data) => data.profitFactor !== null ? data.profitFactor.toFixed(3) : "N/A",
22942
23553
  isVisible: () => true,
22943
23554
  },
22944
23555
  {
22945
23556
  key: "expectancy",
22946
23557
  label: "Expect",
22947
- format: (data) => data.expectancy !== null ? str(data.expectancy, "%") : "N/A",
23558
+ format: (data) => data.expectancy !== null ? `${data.expectancy.toFixed(2)}%` : "N/A",
22948
23559
  isVisible: () => true,
22949
23560
  },
22950
23561
  {
22951
23562
  key: "winRate",
22952
23563
  label: "WR",
22953
- format: (data) => data.winRate !== null ? str(data.winRate, "%") : "N/A",
23564
+ format: (data) => data.winRate !== null ? `${data.winRate.toFixed(2)}%` : "N/A",
22954
23565
  isVisible: () => true,
22955
23566
  },
22956
23567
  {
22957
23568
  key: "avgWin",
22958
23569
  label: "Avg Win",
22959
- format: (data) => data.avgWin !== null ? str(data.avgWin, "%") : "N/A",
23570
+ format: (data) => data.avgWin !== null ? `${data.avgWin.toFixed(2)}%` : "N/A",
22960
23571
  isVisible: () => true,
22961
23572
  },
22962
23573
  {
22963
23574
  key: "avgLoss",
22964
23575
  label: "Avg Loss",
22965
- format: (data) => data.avgLoss !== null ? str(data.avgLoss, "%") : "N/A",
23576
+ format: (data) => data.avgLoss !== null ? `${data.avgLoss.toFixed(2)}%` : "N/A",
22966
23577
  isVisible: () => true,
22967
23578
  },
22968
23579
  {
22969
23580
  key: "maxDrawdown",
22970
23581
  label: "Max DD",
22971
- format: (data) => data.maxDrawdown !== null ? str(-data.maxDrawdown, "%") : "N/A",
23582
+ format: (data) => data.maxDrawdown !== null ? `${(-data.maxDrawdown).toFixed(2)}%` : "N/A",
22972
23583
  isVisible: () => true,
22973
23584
  },
22974
23585
  {
@@ -22992,31 +23603,31 @@ const heat_columns = [
22992
23603
  {
22993
23604
  key: "avgPeakPnl",
22994
23605
  label: "Avg Peak PNL",
22995
- format: (data) => data.avgPeakPnl !== null ? str(data.avgPeakPnl, "%") : "N/A",
23606
+ format: (data) => data.avgPeakPnl !== null ? `${data.avgPeakPnl.toFixed(2)}%` : "N/A",
22996
23607
  isVisible: () => true,
22997
23608
  },
22998
23609
  {
22999
23610
  key: "avgFallPnl",
23000
23611
  label: "Avg DD PNL",
23001
- format: (data) => data.avgFallPnl !== null ? str(data.avgFallPnl, "%") : "N/A",
23612
+ format: (data) => data.avgFallPnl !== null ? `${data.avgFallPnl.toFixed(2)}%` : "N/A",
23002
23613
  isVisible: () => true,
23003
23614
  },
23004
23615
  {
23005
23616
  key: "peakProfitPnl",
23006
23617
  label: "Peak Profit PNL",
23007
- format: (data) => data.peakProfitPnl !== null ? str(data.peakProfitPnl, "%") : "N/A",
23618
+ format: (data) => data.peakProfitPnl !== null ? `${data.peakProfitPnl.toFixed(2)}%` : "N/A",
23008
23619
  isVisible: () => true,
23009
23620
  },
23010
23621
  {
23011
23622
  key: "maxDrawdownPnl",
23012
23623
  label: "Max DD PNL",
23013
- format: (data) => data.maxDrawdownPnl !== null ? str(data.maxDrawdownPnl, "%") : "N/A",
23624
+ format: (data) => data.maxDrawdownPnl !== null ? `${data.maxDrawdownPnl.toFixed(2)}%` : "N/A",
23014
23625
  isVisible: () => true,
23015
23626
  },
23016
23627
  {
23017
23628
  key: "medianPnl",
23018
23629
  label: "Median PNL",
23019
- format: (data) => data.medianPnl !== null ? str(data.medianPnl, "%") : "N/A",
23630
+ format: (data) => data.medianPnl !== null ? `${data.medianPnl.toFixed(2)}%` : "N/A",
23020
23631
  isVisible: () => true,
23021
23632
  },
23022
23633
  {
@@ -23041,7 +23652,7 @@ const heat_columns = [
23041
23652
  key: "avgConsecutiveWinPnl",
23042
23653
  label: "Avg Win Streak PNL",
23043
23654
  format: (data) => data.avgConsecutiveWinPnl !== null
23044
- ? str(data.avgConsecutiveWinPnl, "%")
23655
+ ? `${data.avgConsecutiveWinPnl.toFixed(2)}%`
23045
23656
  : "N/A",
23046
23657
  isVisible: () => true,
23047
23658
  },
@@ -23049,7 +23660,7 @@ const heat_columns = [
23049
23660
  key: "avgConsecutiveLossPnl",
23050
23661
  label: "Avg Loss Streak PNL",
23051
23662
  format: (data) => data.avgConsecutiveLossPnl !== null
23052
- ? str(data.avgConsecutiveLossPnl, "%")
23663
+ ? `${data.avgConsecutiveLossPnl.toFixed(2)}%`
23053
23664
  : "N/A",
23054
23665
  isVisible: () => true,
23055
23666
  },
@@ -23062,7 +23673,7 @@ const heat_columns = [
23062
23673
  {
23063
23674
  key: "trendStrength",
23064
23675
  label: "Trend %/d",
23065
- format: (data) => data.trendStrength !== null ? str(data.trendStrength, "%") : "N/A",
23676
+ format: (data) => data.trendStrength !== null ? `${data.trendStrength.toFixed(2)}%` : "N/A",
23066
23677
  isVisible: () => true,
23067
23678
  },
23068
23679
  {
@@ -23115,25 +23726,25 @@ const heat_columns = [
23115
23726
  {
23116
23727
  key: "medianStepSize",
23117
23728
  label: "Median Step",
23118
- format: (data) => data.medianStepSize !== null ? str(data.medianStepSize, "%") : "N/A",
23729
+ format: (data) => data.medianStepSize !== null ? `${data.medianStepSize.toFixed(2)}%` : "N/A",
23119
23730
  isVisible: () => true,
23120
23731
  },
23121
23732
  {
23122
23733
  key: "sortinoRatio",
23123
23734
  label: "Sortino",
23124
- format: (data) => data.sortinoRatio !== null ? str(data.sortinoRatio) : "N/A",
23735
+ format: (data) => data.sortinoRatio !== null ? data.sortinoRatio.toFixed(3) : "N/A",
23125
23736
  isVisible: () => true,
23126
23737
  },
23127
23738
  {
23128
23739
  key: "calmarRatio",
23129
23740
  label: "Calmar",
23130
- format: (data) => data.calmarRatio !== null ? str(data.calmarRatio) : "N/A",
23741
+ format: (data) => data.calmarRatio !== null ? data.calmarRatio.toFixed(3) : "N/A",
23131
23742
  isVisible: () => true,
23132
23743
  },
23133
23744
  {
23134
23745
  key: "recoveryFactor",
23135
23746
  label: "Recovery",
23136
- format: (data) => data.recoveryFactor !== null ? str(data.recoveryFactor) : "N/A",
23747
+ format: (data) => data.recoveryFactor !== null ? data.recoveryFactor.toFixed(3) : "N/A",
23137
23748
  isVisible: () => true,
23138
23749
  },
23139
23750
  ];
@@ -30309,28 +30920,28 @@ class HeatmapStorage {
30309
30920
  `# Portfolio Heatmap: ${strategyName}`,
30310
30921
  "",
30311
30922
  `**Total Symbols:** ${data.totalSymbols}`,
30312
- `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"}`,
30313
- `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"}`,
30314
- `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? str(data.portfolioAnnualizedSharpeRatio) : "N/A"}`,
30315
- `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? str(data.portfolioCertaintyRatio) : "N/A"}`,
30316
- `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? str(data.portfolioExpectedYearlyReturns, "%") : "N/A"}`,
30923
+ `**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? `${data.portfolioTotalPnl.toFixed(2)} %` : "N/A"}`,
30924
+ `**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? data.portfolioSharpeRatio.toFixed(3) : "N/A"}`,
30925
+ `**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? data.portfolioAnnualizedSharpeRatio.toFixed(3) : "N/A"}`,
30926
+ `**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? data.portfolioCertaintyRatio.toFixed(3) : "N/A"}`,
30927
+ `**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? `${data.portfolioExpectedYearlyReturns.toFixed(2)} %` : "N/A"}`,
30317
30928
  `**Trades Per Year:** ${data.portfolioTradesPerYear !== null ? data.portfolioTradesPerYear.toFixed(1) : "N/A"}`,
30318
30929
  `**Total Trades:** ${data.portfolioTotalTrades}`,
30319
- `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"}`,
30320
- `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
30321
- `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? str(data.portfolioPeakProfitPnl, "%") : "N/A"}`,
30322
- `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? str(data.portfolioMaxDrawdownPnl, "%") : "N/A"}`,
30323
- `**Median PNL:** ${data.portfolioMedianPnl !== null ? str(data.portfolioMedianPnl, "%") : "N/A"}`,
30930
+ `**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? `${data.portfolioAvgPeakPnl.toFixed(2)} %` : "N/A"}`,
30931
+ `**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? `${data.portfolioAvgFallPnl.toFixed(2)} %` : "N/A"}`,
30932
+ `**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? `${data.portfolioPeakProfitPnl.toFixed(2)} %` : "N/A"}`,
30933
+ `**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? `${data.portfolioMaxDrawdownPnl.toFixed(2)} %` : "N/A"}`,
30934
+ `**Median PNL:** ${data.portfolioMedianPnl !== null ? `${data.portfolioMedianPnl.toFixed(2)} %` : "N/A"}`,
30324
30935
  `**Avg Duration:** ${data.portfolioAvgDuration !== null ? `${data.portfolioAvgDuration.toFixed(1)} min` : "N/A"}`,
30325
30936
  `**Avg Win Duration:** ${data.portfolioAvgWinDuration !== null ? `${data.portfolioAvgWinDuration.toFixed(1)} min` : "N/A"}`,
30326
30937
  `**Avg Loss Duration:** ${data.portfolioAvgLossDuration !== null ? `${data.portfolioAvgLossDuration.toFixed(1)} min` : "N/A"}`,
30327
- `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? str(data.portfolioAvgConsecutiveWinPnl, "%") : "N/A"}`,
30328
- `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? str(data.portfolioAvgConsecutiveLossPnl, "%") : "N/A"}`,
30329
- `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? str(data.portfolioStdDev, "%") : "N/A"}`,
30330
- `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? str(data.portfolioSortinoRatio) : "N/A"}`,
30331
- `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? str(data.portfolioCalmarRatio) : "N/A"}`,
30332
- `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? str(data.portfolioRecoveryFactor) : "N/A"}`,
30333
- `**Expectancy:** ${data.portfolioExpectancy !== null ? str(data.portfolioExpectancy, "%") : "N/A"}`,
30938
+ `**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? `${data.portfolioAvgConsecutiveWinPnl.toFixed(2)} %` : "N/A"}`,
30939
+ `**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? `${data.portfolioAvgConsecutiveLossPnl.toFixed(2)} %` : "N/A"}`,
30940
+ `**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? `${data.portfolioStdDev.toFixed(2)} %` : "N/A"}`,
30941
+ `**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? data.portfolioSortinoRatio.toFixed(3) : "N/A"}`,
30942
+ `**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? data.portfolioCalmarRatio.toFixed(3) : "N/A"}`,
30943
+ `**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? data.portfolioRecoveryFactor.toFixed(3) : "N/A"}`,
30944
+ `**Expectancy:** ${data.portfolioExpectancy !== null ? `${data.portfolioExpectancy.toFixed(2)} %` : "N/A"}`,
30334
30945
  "",
30335
30946
  table,
30336
30947
  "",
@@ -42599,6 +43210,8 @@ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOve
42599
43210
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
42600
43211
  const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
42601
43212
  const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
43213
+ const CREATE_SIGNAL_METHOD_NAME = "strategy.createSignal";
43214
+ const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
42602
43215
  /**
42603
43216
  * Cancels the scheduled signal without stopping the strategy.
42604
43217
  *
@@ -44663,6 +45276,69 @@ async function commitSignalNotify(symbol, payload = {}) {
44663
45276
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
44664
45277
  await bt.notificationHelperService.commitSignalNotify(payload, symbol, currentPrice, { strategyName, exchangeName, frameName }, isBacktest);
44665
45278
  }
45279
+ /**
45280
+ * Queues a user-supplied signal DTO to be consumed by the next tick instead of params.getSignal.
45281
+ *
45282
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
45283
+ * current price; provided → opens immediately if already reached, otherwise registers a
45284
+ * scheduled (priceOpen-awaiting) signal. The DTO is validated (reusing validateSignal) and the
45285
+ * call is rejected if a signal or deferred action is already in flight.
45286
+ *
45287
+ * Automatically detects backtest/live mode from execution context.
45288
+ *
45289
+ * @param symbol - Trading pair symbol
45290
+ * @param dto - Signal DTO to open (priceOpen optional)
45291
+ * @returns Promise that resolves when the DTO is queued
45292
+ *
45293
+ * @example
45294
+ * ```typescript
45295
+ * import { createSignal } from "backtest-kit";
45296
+ *
45297
+ * // Open immediately at current price
45298
+ * await createSignal("BTCUSDT", { position: "long", priceTakeProfit: 110, priceStopLoss: 90 });
45299
+ * ```
45300
+ */
45301
+ async function createSignal(symbol, dto) {
45302
+ bt.loggerService.info(CREATE_SIGNAL_METHOD_NAME, { symbol });
45303
+ if (!ExecutionContextService.hasContext()) {
45304
+ throw new Error("createSignal requires an execution context");
45305
+ }
45306
+ if (!MethodContextService.hasContext()) {
45307
+ throw new Error("createSignal requires a method context");
45308
+ }
45309
+ const { backtest: isBacktest } = bt.executionContextService.context;
45310
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45311
+ await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
45312
+ }
45313
+ /**
45314
+ * Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
45315
+ * createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
45316
+ *
45317
+ * Automatically detects backtest/live mode from execution context.
45318
+ *
45319
+ * @param symbol - Trading pair symbol
45320
+ * @returns Promise resolving to the current StrategyStatus snapshot
45321
+ *
45322
+ * @example
45323
+ * ```typescript
45324
+ * import { getStrategyStatus } from "backtest-kit";
45325
+ *
45326
+ * const status = await getStrategyStatus("BTCUSDT");
45327
+ * console.log(status.pendingSignalId, status.commitQueue.length);
45328
+ * ```
45329
+ */
45330
+ async function getStrategyStatus(symbol) {
45331
+ bt.loggerService.info(GET_STRATEGY_STATUS_METHOD_NAME, { symbol });
45332
+ if (!ExecutionContextService.hasContext()) {
45333
+ throw new Error("getStrategyStatus requires an execution context");
45334
+ }
45335
+ if (!MethodContextService.hasContext()) {
45336
+ throw new Error("getStrategyStatus requires a method context");
45337
+ }
45338
+ const { backtest: isBacktest } = bt.executionContextService.context;
45339
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45340
+ return await bt.strategyCoreService.getStatus(isBacktest, symbol, { exchangeName, frameName, strategyName });
45341
+ }
44666
45342
 
44667
45343
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
44668
45344
  /**
@@ -46334,6 +47010,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPosi
46334
47010
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
46335
47011
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
46336
47012
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
47013
+ const BACKTEST_METHOD_NAME_CREATE_SIGNAL = "Backtest.createSignal";
47014
+ const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
46337
47015
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
46338
47016
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
46339
47017
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
@@ -48038,6 +48716,53 @@ class BacktestUtils {
48038
48716
  }
48039
48717
  await bt.strategyCoreService.closePending(true, symbol, context, payload);
48040
48718
  };
48719
+ /**
48720
+ * Queues a user-supplied signal DTO to be consumed by the next backtest tick instead of
48721
+ * params.getSignal.
48722
+ *
48723
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
48724
+ * current price; provided → opens immediately if already reached, otherwise registers a
48725
+ * scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
48726
+ *
48727
+ * @param symbol - Trading pair symbol
48728
+ * @param context - Execution context with strategyName, exchangeName, and frameName
48729
+ * @param dto - Signal DTO to open (priceOpen optional)
48730
+ * @returns Promise that resolves when the DTO is queued
48731
+ */
48732
+ this.createSignal = async (symbol, context, dto) => {
48733
+ bt.loggerService.info(BACKTEST_METHOD_NAME_CREATE_SIGNAL, {
48734
+ symbol,
48735
+ context,
48736
+ });
48737
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48738
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48739
+ {
48740
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
48741
+ riskName &&
48742
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
48743
+ riskList &&
48744
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
48745
+ actions &&
48746
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
48747
+ }
48748
+ await bt.strategyCoreService.createSignal(true, symbol, dto, context);
48749
+ };
48750
+ /**
48751
+ * Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
48752
+ *
48753
+ * @param symbol - Trading pair symbol
48754
+ * @param context - Execution context with strategyName, exchangeName, and frameName
48755
+ * @returns Promise resolving to the current StrategyStatus snapshot
48756
+ */
48757
+ this.getStrategyStatus = async (symbol, context) => {
48758
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS, {
48759
+ symbol,
48760
+ context,
48761
+ });
48762
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
48763
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
48764
+ return await bt.strategyCoreService.getStatus(true, symbol, context);
48765
+ };
48041
48766
  /**
48042
48767
  * Executes partial close at profit level (moving toward TP).
48043
48768
  *
@@ -48996,6 +49721,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPart
48996
49721
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
48997
49722
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
48998
49723
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
49724
+ const LIVE_METHOD_NAME_CREATE_SIGNAL = "Live.createSignal";
49725
+ const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
48999
49726
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
49000
49727
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
49001
49728
  const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
@@ -50873,6 +51600,61 @@ class LiveUtils {
50873
51600
  frameName: "",
50874
51601
  }, payload);
50875
51602
  };
51603
+ /**
51604
+ * Queues a user-supplied signal DTO to be consumed by the next live tick instead of
51605
+ * params.getSignal.
51606
+ *
51607
+ * priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
51608
+ * current price; provided → opens immediately if already reached, otherwise registers a
51609
+ * scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
51610
+ *
51611
+ * @param symbol - Trading pair symbol
51612
+ * @param context - Execution context with strategyName and exchangeName
51613
+ * @param dto - Signal DTO to open (priceOpen optional)
51614
+ * @returns Promise that resolves when the DTO is queued
51615
+ */
51616
+ this.createSignal = async (symbol, context, dto) => {
51617
+ bt.loggerService.info(LIVE_METHOD_NAME_CREATE_SIGNAL, {
51618
+ symbol,
51619
+ context,
51620
+ });
51621
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51622
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51623
+ {
51624
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
51625
+ riskName &&
51626
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL);
51627
+ riskList &&
51628
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL));
51629
+ actions &&
51630
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CREATE_SIGNAL));
51631
+ }
51632
+ await bt.strategyCoreService.createSignal(false, symbol, dto, {
51633
+ strategyName: context.strategyName,
51634
+ exchangeName: context.exchangeName,
51635
+ frameName: "",
51636
+ });
51637
+ };
51638
+ /**
51639
+ * Returns the in-memory deferred strategy-state snapshot for the current live iteration.
51640
+ *
51641
+ * @param symbol - Trading pair symbol
51642
+ * @param context - Execution context with strategyName and exchangeName
51643
+ * @returns Promise resolving to the current StrategyStatus snapshot
51644
+ */
51645
+ this.getStrategyStatus = async (symbol, context) => {
51646
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_STRATEGY_STATUS, {
51647
+ symbol,
51648
+ context,
51649
+ });
51650
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
51651
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
51652
+ return await bt.strategyCoreService.getStatus(false, symbol, {
51653
+ strategyName: context.strategyName,
51654
+ exchangeName: context.exchangeName,
51655
+ frameName: "",
51656
+ });
51657
+ };
50876
51658
  /**
50877
51659
  * Executes partial close at profit level (moving toward TP).
50878
51660
  *
@@ -67519,117 +68301,4 @@ const percentValue = (yesterdayValue, todayValue) => {
67519
68301
  return yesterdayValue / todayValue - 1;
67520
68302
  };
67521
68303
 
67522
- /**
67523
- * Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
67524
- *
67525
- * When priceOpen is provided:
67526
- * - If currentPrice already reached priceOpen (shouldActivateImmediately) →
67527
- * validates as pending: currentPrice must be between SL and TP
67528
- * - Otherwise → validates as scheduled: priceOpen must be between SL and TP
67529
- *
67530
- * When priceOpen is absent:
67531
- * - Validates as pending: currentPrice must be between SL and TP
67532
- *
67533
- * Checks:
67534
- * - currentPrice is a finite positive number
67535
- * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
67536
- * - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
67537
- *
67538
- * @param signal - Signal DTO returned by getSignal
67539
- * @param currentPrice - Current market price at the moment of signal creation
67540
- * @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
67541
- */
67542
- const validateSignal = (signal, currentPrice) => {
67543
- const errors = [];
67544
- // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
67545
- {
67546
- if (typeof currentPrice !== "number") {
67547
- errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
67548
- }
67549
- if (!isFinite(currentPrice)) {
67550
- errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
67551
- }
67552
- if (isFinite(currentPrice) && currentPrice <= 0) {
67553
- errors.push(`currentPrice must be positive, got ${currentPrice}`);
67554
- }
67555
- }
67556
- if (errors.length > 0) {
67557
- console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
67558
- return false;
67559
- }
67560
- try {
67561
- validateCommonSignal(signal);
67562
- }
67563
- catch (error) {
67564
- console.error(getErrorMessage(error));
67565
- return false;
67566
- }
67567
- // Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
67568
- // - нет priceOpen → pending (открывается по currentPrice)
67569
- // - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
67570
- // - priceOpen задан и ещё не достигнут → scheduled
67571
- const hasPriceOpen = signal.priceOpen !== undefined;
67572
- const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
67573
- (signal.position === "short" && currentPrice >= signal.priceOpen));
67574
- const isScheduled = hasPriceOpen && !shouldActivateImmediately;
67575
- if (isScheduled) {
67576
- // Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
67577
- if (signal.position === "long") {
67578
- if (isFinite(signal.priceOpen)) {
67579
- if (signal.priceOpen <= signal.priceStopLoss) {
67580
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
67581
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
67582
- }
67583
- if (signal.priceOpen >= signal.priceTakeProfit) {
67584
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
67585
- `Signal would close immediately on activation. This is logically impossible for LONG position.`);
67586
- }
67587
- }
67588
- }
67589
- if (signal.position === "short") {
67590
- if (isFinite(signal.priceOpen)) {
67591
- if (signal.priceOpen >= signal.priceStopLoss) {
67592
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
67593
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
67594
- }
67595
- if (signal.priceOpen <= signal.priceTakeProfit) {
67596
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
67597
- `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
67598
- }
67599
- }
67600
- }
67601
- }
67602
- else {
67603
- // Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
67604
- if (signal.position === "long") {
67605
- if (isFinite(currentPrice)) {
67606
- if (currentPrice <= signal.priceStopLoss) {
67607
- errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
67608
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
67609
- }
67610
- if (currentPrice >= signal.priceTakeProfit) {
67611
- errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
67612
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
67613
- }
67614
- }
67615
- }
67616
- if (signal.position === "short") {
67617
- if (isFinite(currentPrice)) {
67618
- if (currentPrice >= signal.priceStopLoss) {
67619
- errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
67620
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
67621
- }
67622
- if (currentPrice <= signal.priceTakeProfit) {
67623
- errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
67624
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
67625
- }
67626
- }
67627
- }
67628
- }
67629
- if (errors.length > 0) {
67630
- console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
67631
- }
67632
- return !errors.length;
67633
- };
67634
-
67635
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
68304
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, PersistStrategyAdapter, PersistStrategyInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignal, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getStrategyStatus, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };