backtest-kit 1.8.1 → 1.9.1

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 (4) hide show
  1. package/build/index.cjs +1110 -401
  2. package/build/index.mjs +1111 -403
  3. package/package.json +1 -1
  4. package/types.d.ts +308 -42
package/build/index.cjs CHANGED
@@ -363,6 +363,136 @@ const GLOBAL_CONFIG = {
363
363
  };
364
364
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
365
365
 
366
+ /**
367
+ * Global signal emitter for all trading events (live + backtest).
368
+ * Emits all signal events regardless of execution mode.
369
+ */
370
+ const signalEmitter = new functoolsKit.Subject();
371
+ /**
372
+ * Live trading signal emitter.
373
+ * Emits only signals from live trading execution.
374
+ */
375
+ const signalLiveEmitter = new functoolsKit.Subject();
376
+ /**
377
+ * Backtest signal emitter.
378
+ * Emits only signals from backtest execution.
379
+ */
380
+ const signalBacktestEmitter = new functoolsKit.Subject();
381
+ /**
382
+ * Error emitter for background execution errors.
383
+ * Emits errors caught in background tasks (Live.background, Backtest.background).
384
+ */
385
+ const errorEmitter = new functoolsKit.Subject();
386
+ /**
387
+ * Exit emitter for critical errors that require process termination.
388
+ * Emits errors that should terminate the current execution (Backtest, Live, Walker).
389
+ * Unlike errorEmitter (for recoverable errors), exitEmitter signals fatal errors.
390
+ */
391
+ const exitEmitter = new functoolsKit.Subject();
392
+ /**
393
+ * Done emitter for live background execution completion.
394
+ * Emits when live background tasks complete (Live.background).
395
+ */
396
+ const doneLiveSubject = new functoolsKit.Subject();
397
+ /**
398
+ * Done emitter for backtest background execution completion.
399
+ * Emits when backtest background tasks complete (Backtest.background).
400
+ */
401
+ const doneBacktestSubject = new functoolsKit.Subject();
402
+ /**
403
+ * Done emitter for walker background execution completion.
404
+ * Emits when walker background tasks complete (Walker.background).
405
+ */
406
+ const doneWalkerSubject = new functoolsKit.Subject();
407
+ /**
408
+ * Progress emitter for backtest execution progress.
409
+ * Emits progress updates during backtest execution.
410
+ */
411
+ const progressBacktestEmitter = new functoolsKit.Subject();
412
+ /**
413
+ * Progress emitter for walker execution progress.
414
+ * Emits progress updates during walker execution.
415
+ */
416
+ const progressWalkerEmitter = new functoolsKit.Subject();
417
+ /**
418
+ * Progress emitter for optimizer execution progress.
419
+ * Emits progress updates during optimizer execution.
420
+ */
421
+ const progressOptimizerEmitter = new functoolsKit.Subject();
422
+ /**
423
+ * Performance emitter for execution metrics.
424
+ * Emits performance metrics for profiling and bottleneck detection.
425
+ */
426
+ const performanceEmitter = new functoolsKit.Subject();
427
+ /**
428
+ * Walker emitter for strategy comparison progress.
429
+ * Emits progress updates during walker execution (each strategy completion).
430
+ */
431
+ const walkerEmitter = new functoolsKit.Subject();
432
+ /**
433
+ * Walker complete emitter for strategy comparison completion.
434
+ * Emits when all strategies have been tested and final results are available.
435
+ */
436
+ const walkerCompleteSubject = new functoolsKit.Subject();
437
+ /**
438
+ * Walker stop emitter for walker cancellation events.
439
+ * Emits when a walker comparison is stopped/cancelled.
440
+ *
441
+ * Includes walkerName to support multiple walkers running on the same symbol.
442
+ */
443
+ const walkerStopSubject = new functoolsKit.Subject();
444
+ /**
445
+ * Validation emitter for risk validation errors.
446
+ * Emits when risk validation functions throw errors during signal checking.
447
+ */
448
+ const validationSubject = new functoolsKit.Subject();
449
+ /**
450
+ * Partial profit emitter for profit level milestones.
451
+ * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
452
+ */
453
+ const partialProfitSubject = new functoolsKit.Subject();
454
+ /**
455
+ * Partial loss emitter for loss level milestones.
456
+ * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
457
+ */
458
+ const partialLossSubject = new functoolsKit.Subject();
459
+ /**
460
+ * Risk rejection emitter for risk management violations.
461
+ * Emits ONLY when a signal is rejected due to risk validation failure.
462
+ * Does not emit for allowed signals (prevents spam).
463
+ */
464
+ const riskSubject = new functoolsKit.Subject();
465
+ /**
466
+ * Ping emitter for scheduled signal monitoring events.
467
+ * Emits every minute when a scheduled signal is being monitored (waiting for activation).
468
+ * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
469
+ */
470
+ const pingSubject = new functoolsKit.Subject();
471
+
472
+ var emitters = /*#__PURE__*/Object.freeze({
473
+ __proto__: null,
474
+ doneBacktestSubject: doneBacktestSubject,
475
+ doneLiveSubject: doneLiveSubject,
476
+ doneWalkerSubject: doneWalkerSubject,
477
+ errorEmitter: errorEmitter,
478
+ exitEmitter: exitEmitter,
479
+ partialLossSubject: partialLossSubject,
480
+ partialProfitSubject: partialProfitSubject,
481
+ performanceEmitter: performanceEmitter,
482
+ pingSubject: pingSubject,
483
+ progressBacktestEmitter: progressBacktestEmitter,
484
+ progressOptimizerEmitter: progressOptimizerEmitter,
485
+ progressWalkerEmitter: progressWalkerEmitter,
486
+ riskSubject: riskSubject,
487
+ signalBacktestEmitter: signalBacktestEmitter,
488
+ signalEmitter: signalEmitter,
489
+ signalLiveEmitter: signalLiveEmitter,
490
+ validationSubject: validationSubject,
491
+ walkerCompleteSubject: walkerCompleteSubject,
492
+ walkerEmitter: walkerEmitter,
493
+ walkerStopSubject: walkerStopSubject
494
+ });
495
+
366
496
  const INTERVAL_MINUTES$4 = {
367
497
  "1m": 1,
368
498
  "3m": 3,
@@ -464,6 +594,33 @@ const GET_CANDLES_FN = async (dto, since, self) => {
464
594
  }
465
595
  throw lastError;
466
596
  };
597
+ /**
598
+ * Wrapper to call onCandleData callback with error handling.
599
+ * Catches and logs any errors thrown by the user-provided callback.
600
+ *
601
+ * @param self - ClientExchange instance reference
602
+ * @param symbol - Trading pair symbol
603
+ * @param interval - Candle interval
604
+ * @param since - Start date for candle data
605
+ * @param limit - Number of candles
606
+ * @param data - Array of candle data
607
+ */
608
+ const CALL_CANDLE_DATA_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, interval, since, limit, data) => {
609
+ if (self.params.callbacks?.onCandleData) {
610
+ await self.params.callbacks.onCandleData(symbol, interval, since, limit, data);
611
+ }
612
+ }, {
613
+ fallback: (error) => {
614
+ const message = "ClientExchange CALL_CANDLE_DATA_CALLBACKS_FN thrown";
615
+ const payload = {
616
+ error: functoolsKit.errorData(error),
617
+ message: functoolsKit.getErrorMessage(error),
618
+ };
619
+ backtest$1.loggerService.warn(message, payload);
620
+ console.warn(message, payload);
621
+ errorEmitter.next(error);
622
+ },
623
+ });
467
624
  /**
468
625
  * Client implementation for exchange data access.
469
626
  *
@@ -522,9 +679,7 @@ class ClientExchange {
522
679
  if (filteredData.length < limit) {
523
680
  this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
524
681
  }
525
- if (this.params.callbacks?.onCandleData) {
526
- this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
527
- }
682
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, filteredData);
528
683
  return filteredData;
529
684
  }
530
685
  /**
@@ -559,9 +714,7 @@ class ClientExchange {
559
714
  if (filteredData.length < limit) {
560
715
  this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
561
716
  }
562
- if (this.params.callbacks?.onCandleData) {
563
- this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
564
- }
717
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, filteredData);
565
718
  return filteredData;
566
719
  }
567
720
  /**
@@ -1738,136 +1891,6 @@ class PersistPartialUtils {
1738
1891
  */
1739
1892
  const PersistPartialAdapter = new PersistPartialUtils();
1740
1893
 
1741
- /**
1742
- * Global signal emitter for all trading events (live + backtest).
1743
- * Emits all signal events regardless of execution mode.
1744
- */
1745
- const signalEmitter = new functoolsKit.Subject();
1746
- /**
1747
- * Live trading signal emitter.
1748
- * Emits only signals from live trading execution.
1749
- */
1750
- const signalLiveEmitter = new functoolsKit.Subject();
1751
- /**
1752
- * Backtest signal emitter.
1753
- * Emits only signals from backtest execution.
1754
- */
1755
- const signalBacktestEmitter = new functoolsKit.Subject();
1756
- /**
1757
- * Error emitter for background execution errors.
1758
- * Emits errors caught in background tasks (Live.background, Backtest.background).
1759
- */
1760
- const errorEmitter = new functoolsKit.Subject();
1761
- /**
1762
- * Exit emitter for critical errors that require process termination.
1763
- * Emits errors that should terminate the current execution (Backtest, Live, Walker).
1764
- * Unlike errorEmitter (for recoverable errors), exitEmitter signals fatal errors.
1765
- */
1766
- const exitEmitter = new functoolsKit.Subject();
1767
- /**
1768
- * Done emitter for live background execution completion.
1769
- * Emits when live background tasks complete (Live.background).
1770
- */
1771
- const doneLiveSubject = new functoolsKit.Subject();
1772
- /**
1773
- * Done emitter for backtest background execution completion.
1774
- * Emits when backtest background tasks complete (Backtest.background).
1775
- */
1776
- const doneBacktestSubject = new functoolsKit.Subject();
1777
- /**
1778
- * Done emitter for walker background execution completion.
1779
- * Emits when walker background tasks complete (Walker.background).
1780
- */
1781
- const doneWalkerSubject = new functoolsKit.Subject();
1782
- /**
1783
- * Progress emitter for backtest execution progress.
1784
- * Emits progress updates during backtest execution.
1785
- */
1786
- const progressBacktestEmitter = new functoolsKit.Subject();
1787
- /**
1788
- * Progress emitter for walker execution progress.
1789
- * Emits progress updates during walker execution.
1790
- */
1791
- const progressWalkerEmitter = new functoolsKit.Subject();
1792
- /**
1793
- * Progress emitter for optimizer execution progress.
1794
- * Emits progress updates during optimizer execution.
1795
- */
1796
- const progressOptimizerEmitter = new functoolsKit.Subject();
1797
- /**
1798
- * Performance emitter for execution metrics.
1799
- * Emits performance metrics for profiling and bottleneck detection.
1800
- */
1801
- const performanceEmitter = new functoolsKit.Subject();
1802
- /**
1803
- * Walker emitter for strategy comparison progress.
1804
- * Emits progress updates during walker execution (each strategy completion).
1805
- */
1806
- const walkerEmitter = new functoolsKit.Subject();
1807
- /**
1808
- * Walker complete emitter for strategy comparison completion.
1809
- * Emits when all strategies have been tested and final results are available.
1810
- */
1811
- const walkerCompleteSubject = new functoolsKit.Subject();
1812
- /**
1813
- * Walker stop emitter for walker cancellation events.
1814
- * Emits when a walker comparison is stopped/cancelled.
1815
- *
1816
- * Includes walkerName to support multiple walkers running on the same symbol.
1817
- */
1818
- const walkerStopSubject = new functoolsKit.Subject();
1819
- /**
1820
- * Validation emitter for risk validation errors.
1821
- * Emits when risk validation functions throw errors during signal checking.
1822
- */
1823
- const validationSubject = new functoolsKit.Subject();
1824
- /**
1825
- * Partial profit emitter for profit level milestones.
1826
- * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
1827
- */
1828
- const partialProfitSubject = new functoolsKit.Subject();
1829
- /**
1830
- * Partial loss emitter for loss level milestones.
1831
- * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1832
- */
1833
- const partialLossSubject = new functoolsKit.Subject();
1834
- /**
1835
- * Risk rejection emitter for risk management violations.
1836
- * Emits ONLY when a signal is rejected due to risk validation failure.
1837
- * Does not emit for allowed signals (prevents spam).
1838
- */
1839
- const riskSubject = new functoolsKit.Subject();
1840
- /**
1841
- * Ping emitter for scheduled signal monitoring events.
1842
- * Emits every minute when a scheduled signal is being monitored (waiting for activation).
1843
- * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
1844
- */
1845
- const pingSubject = new functoolsKit.Subject();
1846
-
1847
- var emitters = /*#__PURE__*/Object.freeze({
1848
- __proto__: null,
1849
- doneBacktestSubject: doneBacktestSubject,
1850
- doneLiveSubject: doneLiveSubject,
1851
- doneWalkerSubject: doneWalkerSubject,
1852
- errorEmitter: errorEmitter,
1853
- exitEmitter: exitEmitter,
1854
- partialLossSubject: partialLossSubject,
1855
- partialProfitSubject: partialProfitSubject,
1856
- performanceEmitter: performanceEmitter,
1857
- pingSubject: pingSubject,
1858
- progressBacktestEmitter: progressBacktestEmitter,
1859
- progressOptimizerEmitter: progressOptimizerEmitter,
1860
- progressWalkerEmitter: progressWalkerEmitter,
1861
- riskSubject: riskSubject,
1862
- signalBacktestEmitter: signalBacktestEmitter,
1863
- signalEmitter: signalEmitter,
1864
- signalLiveEmitter: signalLiveEmitter,
1865
- validationSubject: validationSubject,
1866
- walkerCompleteSubject: walkerCompleteSubject,
1867
- walkerEmitter: walkerEmitter,
1868
- walkerStopSubject: walkerStopSubject
1869
- });
1870
-
1871
1894
  /**
1872
1895
  * Converts markdown content to plain text with minimal formatting
1873
1896
  * @param content - Markdown string to convert
@@ -1924,227 +1947,321 @@ const INTERVAL_MINUTES$3 = {
1924
1947
  "1h": 60,
1925
1948
  };
1926
1949
  const TIMEOUT_SYMBOL = Symbol('timeout');
1950
+ /**
1951
+ * Converts internal signal to public API format.
1952
+ *
1953
+ * This function is used AFTER position opens for external callbacks and API.
1954
+ * It hides internal implementation details while exposing effective values:
1955
+ *
1956
+ * - Replaces internal _trailingPriceStopLoss with effective priceStopLoss
1957
+ * - Preserves original stop-loss in originalPriceStopLoss for reference
1958
+ * - Ensures external code never sees private _trailingPriceStopLoss field
1959
+ * - Maintains backward compatibility with non-trailing positions
1960
+ *
1961
+ * Key differences from TO_RISK_SIGNAL (in ClientRisk.ts):
1962
+ * - Used AFTER position opens (vs BEFORE for risk validation)
1963
+ * - Works only with ISignalRow/IScheduledSignalRow (vs ISignalDto)
1964
+ * - No currentPrice fallback needed (priceOpen always present in opened signals)
1965
+ * - Returns IPublicSignalRow (vs IRiskSignalRow for risk checks)
1966
+ *
1967
+ * Use cases:
1968
+ * - All strategy callbacks (onOpen, onClose, onActive, etc.)
1969
+ * - External API responses (getPendingSignal, getScheduledSignal)
1970
+ * - Event emissions and logging
1971
+ * - Integration with ClientPartial and ClientRisk
1972
+ *
1973
+ * @param signal - Internal signal row with optional trailing stop-loss
1974
+ * @returns Signal in IPublicSignalRow format with effective stop-loss and hidden internals
1975
+ *
1976
+ * @example
1977
+ * ```typescript
1978
+ * // Signal without trailing SL
1979
+ * const publicSignal = TO_PUBLIC_SIGNAL(signal);
1980
+ * // publicSignal.priceStopLoss = signal.priceStopLoss
1981
+ * // publicSignal.originalPriceStopLoss = signal.priceStopLoss
1982
+ *
1983
+ * // Signal with trailing SL
1984
+ * const publicSignal = TO_PUBLIC_SIGNAL(signalWithTrailing);
1985
+ * // publicSignal.priceStopLoss = signal._trailingPriceStopLoss (effective)
1986
+ * // publicSignal.originalPriceStopLoss = signal.priceStopLoss (original)
1987
+ * // publicSignal._trailingPriceStopLoss = undefined (hidden from external API)
1988
+ * ```
1989
+ */
1990
+ const TO_PUBLIC_SIGNAL = (signal) => {
1991
+ if (signal._trailingPriceStopLoss !== undefined) {
1992
+ return {
1993
+ ...signal,
1994
+ priceStopLoss: signal._trailingPriceStopLoss,
1995
+ originalPriceStopLoss: signal.priceStopLoss,
1996
+ };
1997
+ }
1998
+ return {
1999
+ ...signal,
2000
+ originalPriceStopLoss: signal.priceStopLoss,
2001
+ };
2002
+ };
1927
2003
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1928
2004
  const errors = [];
1929
2005
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
1930
- if (signal.id === undefined || signal.id === null || signal.id === '') {
1931
- errors.push('id is required and must be a non-empty string');
1932
- }
1933
- if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
1934
- errors.push('exchangeName is required');
1935
- }
1936
- if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
1937
- errors.push('strategyName is required');
1938
- }
1939
- if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
1940
- errors.push('symbol is required and must be a non-empty string');
1941
- }
1942
- if (signal._isScheduled === undefined || signal._isScheduled === null) {
1943
- errors.push('_isScheduled is required');
1944
- }
1945
- if (signal.position === undefined || signal.position === null) {
1946
- errors.push('position is required and must be "long" or "short"');
1947
- }
1948
- if (signal.position !== "long" && signal.position !== "short") {
1949
- errors.push(`position must be "long" or "short", got "${signal.position}"`);
2006
+ {
2007
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
2008
+ errors.push('id is required and must be a non-empty string');
2009
+ }
2010
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
2011
+ errors.push('exchangeName is required');
2012
+ }
2013
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
2014
+ errors.push('strategyName is required');
2015
+ }
2016
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
2017
+ errors.push('symbol is required and must be a non-empty string');
2018
+ }
2019
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
2020
+ errors.push('_isScheduled is required');
2021
+ }
2022
+ if (signal.position === undefined || signal.position === null) {
2023
+ errors.push('position is required and must be "long" or "short"');
2024
+ }
2025
+ if (signal.position !== "long" && signal.position !== "short") {
2026
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
2027
+ }
1950
2028
  }
1951
2029
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1952
- if (typeof currentPrice !== "number") {
1953
- errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
1954
- }
1955
- if (!isFinite(currentPrice)) {
1956
- errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
1957
- }
1958
- if (isFinite(currentPrice) && currentPrice <= 0) {
1959
- errors.push(`currentPrice must be positive, got ${currentPrice}`);
2030
+ {
2031
+ if (typeof currentPrice !== "number") {
2032
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
2033
+ }
2034
+ if (!isFinite(currentPrice)) {
2035
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
2036
+ }
2037
+ if (isFinite(currentPrice) && currentPrice <= 0) {
2038
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
2039
+ }
1960
2040
  }
1961
2041
  // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
1962
- if (typeof signal.priceOpen !== "number") {
1963
- errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
1964
- }
1965
- if (!isFinite(signal.priceOpen)) {
1966
- errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
1967
- }
1968
- if (typeof signal.priceTakeProfit !== "number") {
1969
- errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
1970
- }
1971
- if (!isFinite(signal.priceTakeProfit)) {
1972
- errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
1973
- }
1974
- if (typeof signal.priceStopLoss !== "number") {
1975
- errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
1976
- }
1977
- if (!isFinite(signal.priceStopLoss)) {
1978
- errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2042
+ {
2043
+ if (typeof signal.priceOpen !== "number") {
2044
+ errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
2045
+ }
2046
+ if (!isFinite(signal.priceOpen)) {
2047
+ errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
2048
+ }
2049
+ if (typeof signal.priceTakeProfit !== "number") {
2050
+ errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
2051
+ }
2052
+ if (!isFinite(signal.priceTakeProfit)) {
2053
+ errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
2054
+ }
2055
+ if (typeof signal.priceStopLoss !== "number") {
2056
+ errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2057
+ }
2058
+ if (!isFinite(signal.priceStopLoss)) {
2059
+ errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2060
+ }
1979
2061
  }
1980
2062
  // Валидация цен (только если они конечные)
1981
- if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
1982
- errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
1983
- }
1984
- if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
1985
- errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
1986
- }
1987
- if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
1988
- errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
2063
+ {
2064
+ if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
2065
+ errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
2066
+ }
2067
+ if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
2068
+ errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
2069
+ }
2070
+ if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
2071
+ errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
2072
+ }
1989
2073
  }
1990
2074
  // Валидация для long позиции
1991
2075
  if (signal.position === "long") {
1992
- if (signal.priceTakeProfit <= signal.priceOpen) {
1993
- errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
1994
- }
1995
- if (signal.priceStopLoss >= signal.priceOpen) {
1996
- errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
2076
+ // Проверка соотношения цен для long
2077
+ {
2078
+ if (signal.priceTakeProfit <= signal.priceOpen) {
2079
+ errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
2080
+ }
2081
+ if (signal.priceStopLoss >= signal.priceOpen) {
2082
+ errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
2083
+ }
1997
2084
  }
1998
2085
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
1999
- if (!isScheduled && isFinite(currentPrice)) {
2000
- // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
2001
- // SL < currentPrice < TP
2002
- if (currentPrice <= signal.priceStopLoss) {
2003
- errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2004
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2005
- }
2006
- if (currentPrice >= signal.priceTakeProfit) {
2007
- errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2008
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2086
+ {
2087
+ if (!isScheduled && isFinite(currentPrice)) {
2088
+ // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
2089
+ // SL < currentPrice < TP
2090
+ if (currentPrice <= signal.priceStopLoss) {
2091
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2092
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2093
+ }
2094
+ if (currentPrice >= signal.priceTakeProfit) {
2095
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2096
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2097
+ }
2009
2098
  }
2010
2099
  }
2011
2100
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
2012
- if (isScheduled && isFinite(signal.priceOpen)) {
2013
- // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
2014
- // SL < priceOpen < TP
2015
- if (signal.priceOpen <= signal.priceStopLoss) {
2016
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2017
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2018
- }
2019
- if (signal.priceOpen >= signal.priceTakeProfit) {
2020
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2021
- `Signal would close immediately on activation. This is logically impossible for LONG position.`);
2101
+ {
2102
+ if (isScheduled && isFinite(signal.priceOpen)) {
2103
+ // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
2104
+ // SL < priceOpen < TP
2105
+ if (signal.priceOpen <= signal.priceStopLoss) {
2106
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2107
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2108
+ }
2109
+ if (signal.priceOpen >= signal.priceTakeProfit) {
2110
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2111
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
2112
+ }
2022
2113
  }
2023
2114
  }
2024
2115
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
2025
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2026
- const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
2027
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2028
- errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2029
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2030
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2116
+ {
2117
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2118
+ const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
2119
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2120
+ errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2121
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2122
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2123
+ }
2031
2124
  }
2032
2125
  }
2033
2126
  // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
2034
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2035
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2036
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2037
- errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2038
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2039
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2127
+ {
2128
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2129
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2130
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2131
+ errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2132
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2133
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2134
+ }
2040
2135
  }
2041
2136
  }
2042
2137
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
2043
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2044
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2045
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2046
- errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2047
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2048
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2138
+ {
2139
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2140
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2141
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2142
+ errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2143
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2144
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2145
+ }
2049
2146
  }
2050
2147
  }
2051
2148
  }
2052
2149
  // Валидация для short позиции
2053
2150
  if (signal.position === "short") {
2054
- if (signal.priceTakeProfit >= signal.priceOpen) {
2055
- errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
2056
- }
2057
- if (signal.priceStopLoss <= signal.priceOpen) {
2058
- errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
2151
+ // Проверка соотношения цен для short
2152
+ {
2153
+ if (signal.priceTakeProfit >= signal.priceOpen) {
2154
+ errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
2155
+ }
2156
+ if (signal.priceStopLoss <= signal.priceOpen) {
2157
+ errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
2158
+ }
2059
2159
  }
2060
2160
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
2061
- if (!isScheduled && isFinite(currentPrice)) {
2062
- // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
2063
- // TP < currentPrice < SL
2064
- if (currentPrice >= signal.priceStopLoss) {
2065
- errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2066
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2067
- }
2068
- if (currentPrice <= signal.priceTakeProfit) {
2069
- errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2070
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2161
+ {
2162
+ if (!isScheduled && isFinite(currentPrice)) {
2163
+ // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
2164
+ // TP < currentPrice < SL
2165
+ if (currentPrice >= signal.priceStopLoss) {
2166
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2167
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2168
+ }
2169
+ if (currentPrice <= signal.priceTakeProfit) {
2170
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2171
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2172
+ }
2071
2173
  }
2072
2174
  }
2073
2175
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
2074
- if (isScheduled && isFinite(signal.priceOpen)) {
2075
- // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
2076
- // TP < priceOpen < SL
2077
- if (signal.priceOpen >= signal.priceStopLoss) {
2078
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2079
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2080
- }
2081
- if (signal.priceOpen <= signal.priceTakeProfit) {
2082
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2083
- `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
2176
+ {
2177
+ if (isScheduled && isFinite(signal.priceOpen)) {
2178
+ // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
2179
+ // TP < priceOpen < SL
2180
+ if (signal.priceOpen >= signal.priceStopLoss) {
2181
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2182
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2183
+ }
2184
+ if (signal.priceOpen <= signal.priceTakeProfit) {
2185
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2186
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
2187
+ }
2084
2188
  }
2085
2189
  }
2086
2190
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
2087
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2088
- const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
2089
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2090
- errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2091
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2092
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2191
+ {
2192
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2193
+ const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
2194
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2195
+ errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2196
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2197
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2198
+ }
2093
2199
  }
2094
2200
  }
2095
2201
  // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
2096
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2097
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2098
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2099
- errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2100
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2101
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2202
+ {
2203
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2204
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2205
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2206
+ errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2207
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2208
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2209
+ }
2102
2210
  }
2103
2211
  }
2104
2212
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
2105
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2106
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2107
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2108
- errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2109
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2110
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2213
+ {
2214
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2215
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2216
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2217
+ errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2218
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2219
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2220
+ }
2111
2221
  }
2112
2222
  }
2113
2223
  }
2114
2224
  // Валидация временных параметров
2115
- if (typeof signal.minuteEstimatedTime !== "number") {
2116
- errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
2117
- }
2118
- if (signal.minuteEstimatedTime <= 0) {
2119
- errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
2120
- }
2121
- if (!Number.isInteger(signal.minuteEstimatedTime)) {
2122
- errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
2123
- }
2124
- if (!isFinite(signal.minuteEstimatedTime)) {
2125
- errors.push(`minuteEstimatedTime must be a finite number, got ${signal.minuteEstimatedTime}`);
2225
+ {
2226
+ if (typeof signal.minuteEstimatedTime !== "number") {
2227
+ errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
2228
+ }
2229
+ if (signal.minuteEstimatedTime <= 0) {
2230
+ errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
2231
+ }
2232
+ if (!Number.isInteger(signal.minuteEstimatedTime)) {
2233
+ errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
2234
+ }
2235
+ if (!isFinite(signal.minuteEstimatedTime)) {
2236
+ errors.push(`minuteEstimatedTime must be a finite number, got ${signal.minuteEstimatedTime}`);
2237
+ }
2126
2238
  }
2127
2239
  // ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
2128
- if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2129
- if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2130
- const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
2131
- const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
2132
- errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
2133
- `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
2134
- `Eternal signals block risk limits and prevent new trades.`);
2240
+ {
2241
+ if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2242
+ if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2243
+ const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
2244
+ const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
2245
+ errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
2246
+ `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
2247
+ `Eternal signals block risk limits and prevent new trades.`);
2248
+ }
2135
2249
  }
2136
2250
  }
2137
- if (typeof signal.scheduledAt !== "number") {
2138
- errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
2139
- }
2140
- if (signal.scheduledAt <= 0) {
2141
- errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
2142
- }
2143
- if (typeof signal.pendingAt !== "number") {
2144
- errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
2145
- }
2146
- if (signal.pendingAt <= 0) {
2147
- errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
2251
+ // Валидация временных меток
2252
+ {
2253
+ if (typeof signal.scheduledAt !== "number") {
2254
+ errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
2255
+ }
2256
+ if (signal.scheduledAt <= 0) {
2257
+ errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
2258
+ }
2259
+ if (typeof signal.pendingAt !== "number") {
2260
+ errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
2261
+ }
2262
+ if (signal.pendingAt <= 0) {
2263
+ errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
2264
+ }
2148
2265
  }
2149
2266
  // Кидаем ошибку если есть проблемы
2150
2267
  if (errors.length > 0) {
@@ -2384,6 +2501,97 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2384
2501
  slClosed: slClosed + percentToClose,
2385
2502
  });
2386
2503
  };
2504
+ const TRAILING_STOP_FN = (self, signal, percentShift) => {
2505
+ // Calculate distance between entry and original stop-loss AS PERCENTAGE of entry price
2506
+ const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
2507
+ // Calculate new stop-loss distance percentage by adding shift
2508
+ // Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
2509
+ // Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
2510
+ const newSlDistancePercent = slDistancePercent + percentShift;
2511
+ // Calculate new stop-loss price based on new distance percentage
2512
+ // Negative newSlDistancePercent means SL crosses entry into profit zone
2513
+ let newStopLoss;
2514
+ if (signal.position === "long") {
2515
+ // LONG: SL is below entry (or above entry if in profit zone)
2516
+ // Formula: entry * (1 - newDistance%)
2517
+ // Example: entry=100, originalSL=90 (10%), shift=-15% → newDistance=-5% → 100 * 1.05 = 105 (profit zone)
2518
+ // Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
2519
+ // Example: entry=100, originalSL=90 (10%), shift=+5% → newDistance=15% → 100 * 0.85 = 85 (looser)
2520
+ newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
2521
+ }
2522
+ else {
2523
+ // SHORT: SL is above entry (or below entry if in profit zone)
2524
+ // Formula: entry * (1 + newDistance%)
2525
+ // Example: entry=100, originalSL=110 (10%), shift=-15% → newDistance=-5% → 100 * 0.95 = 95 (profit zone)
2526
+ // Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
2527
+ // Example: entry=100, originalSL=110 (10%), shift=+5% → newDistance=15% → 100 * 1.15 = 115 (looser)
2528
+ newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
2529
+ }
2530
+ // Get current effective stop-loss (trailing or original)
2531
+ const currentStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
2532
+ // Determine if this is the first trailing stop call (direction not set yet)
2533
+ const isFirstCall = signal._trailingPriceStopLoss === undefined;
2534
+ if (isFirstCall) {
2535
+ // First call: set the direction and update SL unconditionally
2536
+ signal._trailingPriceStopLoss = newStopLoss;
2537
+ self.params.logger.info("TRAILING_STOP_FN executed (first call - direction set)", {
2538
+ signalId: signal.id,
2539
+ position: signal.position,
2540
+ priceOpen: signal.priceOpen,
2541
+ originalStopLoss: signal.priceStopLoss,
2542
+ originalDistancePercent: slDistancePercent,
2543
+ previousStopLoss: currentStopLoss,
2544
+ newStopLoss,
2545
+ newDistancePercent: newSlDistancePercent,
2546
+ percentShift,
2547
+ inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
2548
+ direction: newStopLoss > currentStopLoss ? "up" : "down",
2549
+ });
2550
+ }
2551
+ else {
2552
+ // Subsequent calls: only update if new SL continues in the same direction
2553
+ const movingUp = newStopLoss > currentStopLoss;
2554
+ const movingDown = newStopLoss < currentStopLoss;
2555
+ // Determine initial direction based on first trailing SL vs original SL
2556
+ const initialDirection = signal._trailingPriceStopLoss > signal.priceStopLoss ? "up" : "down";
2557
+ let shouldUpdate = false;
2558
+ if (initialDirection === "up" && movingUp) {
2559
+ // Direction is UP, and new SL continues moving up
2560
+ shouldUpdate = true;
2561
+ }
2562
+ else if (initialDirection === "down" && movingDown) {
2563
+ // Direction is DOWN, and new SL continues moving down
2564
+ shouldUpdate = true;
2565
+ }
2566
+ if (!shouldUpdate) {
2567
+ self.params.logger.debug("TRAILING_STOP_FN: new SL not in same direction, skipping", {
2568
+ signalId: signal.id,
2569
+ position: signal.position,
2570
+ currentStopLoss,
2571
+ newStopLoss,
2572
+ percentShift,
2573
+ initialDirection,
2574
+ attemptedDirection: movingUp ? "up" : movingDown ? "down" : "same",
2575
+ });
2576
+ return;
2577
+ }
2578
+ // Update trailing stop-loss
2579
+ signal._trailingPriceStopLoss = newStopLoss;
2580
+ self.params.logger.info("TRAILING_STOP_FN executed", {
2581
+ signalId: signal.id,
2582
+ position: signal.position,
2583
+ priceOpen: signal.priceOpen,
2584
+ originalStopLoss: signal.priceStopLoss,
2585
+ originalDistancePercent: slDistancePercent,
2586
+ previousStopLoss: currentStopLoss,
2587
+ newStopLoss,
2588
+ newDistancePercent: newSlDistancePercent,
2589
+ percentShift,
2590
+ inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
2591
+ direction: initialDirection,
2592
+ });
2593
+ }
2594
+ };
2387
2595
  const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
2388
2596
  const currentTime = self.params.execution.context.when.getTime();
2389
2597
  const signalTime = scheduled.scheduledAt; // Таймаут для scheduled signal считается от scheduledAt
@@ -2402,7 +2610,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
2402
2610
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2403
2611
  const result = {
2404
2612
  action: "cancelled",
2405
- signal: scheduled,
2613
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2406
2614
  currentPrice: currentPrice,
2407
2615
  closeTimestamp: currentTime,
2408
2616
  strategyName: self.params.method.context.strategyName,
@@ -2457,7 +2665,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
2457
2665
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2458
2666
  const result = {
2459
2667
  action: "cancelled",
2460
- signal: scheduled,
2668
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2461
2669
  currentPrice: currentPrice,
2462
2670
  closeTimestamp: currentTime,
2463
2671
  strategyName: self.params.method.context.strategyName,
@@ -2514,7 +2722,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2514
2722
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
2515
2723
  const result = {
2516
2724
  action: "opened",
2517
- signal: self._pendingSignal,
2725
+ signal: TO_PUBLIC_SIGNAL(self._pendingSignal),
2518
2726
  strategyName: self.params.method.context.strategyName,
2519
2727
  exchangeName: self.params.method.context.exchangeName,
2520
2728
  frameName: self.params.method.context.frameName,
@@ -2527,11 +2735,12 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2527
2735
  };
2528
2736
  const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, scheduled, timestamp, backtest) => {
2529
2737
  await ExecutionContextService.runInContext(async () => {
2738
+ const publicSignal = TO_PUBLIC_SIGNAL(scheduled);
2530
2739
  // Call system onPing callback first (emits to pingSubject)
2531
- await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, scheduled, self.params.execution.context.backtest, timestamp);
2740
+ await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
2532
2741
  // Call user onPing callback only if signal is still active (not cancelled, not activated)
2533
2742
  if (self.params.callbacks?.onPing) {
2534
- await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
2743
+ await self.params.callbacks.onPing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
2535
2744
  }
2536
2745
  }, {
2537
2746
  when: new Date(timestamp),
@@ -2553,7 +2762,8 @@ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, schedu
2553
2762
  const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2554
2763
  await ExecutionContextService.runInContext(async () => {
2555
2764
  if (self.params.callbacks?.onActive) {
2556
- self.params.callbacks.onActive(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2765
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2766
+ await self.params.callbacks.onActive(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2557
2767
  }
2558
2768
  }, {
2559
2769
  when: new Date(timestamp),
@@ -2575,7 +2785,8 @@ const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, sign
2575
2785
  const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2576
2786
  await ExecutionContextService.runInContext(async () => {
2577
2787
  if (self.params.callbacks?.onSchedule) {
2578
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2788
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2789
+ await self.params.callbacks.onSchedule(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2579
2790
  }
2580
2791
  }, {
2581
2792
  when: new Date(timestamp),
@@ -2597,7 +2808,8 @@ const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, si
2597
2808
  const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2598
2809
  await ExecutionContextService.runInContext(async () => {
2599
2810
  if (self.params.callbacks?.onCancel) {
2600
- self.params.callbacks.onCancel(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2811
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2812
+ await self.params.callbacks.onCancel(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2601
2813
  }
2602
2814
  }, {
2603
2815
  when: new Date(timestamp),
@@ -2619,7 +2831,8 @@ const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, sign
2619
2831
  const CALL_OPEN_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, priceOpen, timestamp, backtest) => {
2620
2832
  await ExecutionContextService.runInContext(async () => {
2621
2833
  if (self.params.callbacks?.onOpen) {
2622
- self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, priceOpen, self.params.execution.context.backtest);
2834
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2835
+ await self.params.callbacks.onOpen(self.params.execution.context.symbol, publicSignal, priceOpen, self.params.execution.context.backtest);
2623
2836
  }
2624
2837
  }, {
2625
2838
  when: new Date(timestamp),
@@ -2641,7 +2854,8 @@ const CALL_OPEN_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal
2641
2854
  const CALL_CLOSE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2642
2855
  await ExecutionContextService.runInContext(async () => {
2643
2856
  if (self.params.callbacks?.onClose) {
2644
- self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2857
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2858
+ await self.params.callbacks.onClose(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2645
2859
  }
2646
2860
  }, {
2647
2861
  when: new Date(timestamp),
@@ -2663,7 +2877,7 @@ const CALL_CLOSE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signa
2663
2877
  const CALL_TICK_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, result, timestamp, backtest) => {
2664
2878
  await ExecutionContextService.runInContext(async () => {
2665
2879
  if (self.params.callbacks?.onTick) {
2666
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2880
+ await self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2667
2881
  }
2668
2882
  }, {
2669
2883
  when: new Date(timestamp),
@@ -2685,7 +2899,7 @@ const CALL_TICK_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, result
2685
2899
  const CALL_IDLE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, currentPrice, timestamp, backtest) => {
2686
2900
  await ExecutionContextService.runInContext(async () => {
2687
2901
  if (self.params.callbacks?.onIdle) {
2688
- self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2902
+ await self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2689
2903
  }
2690
2904
  }, {
2691
2905
  when: new Date(timestamp),
@@ -2756,7 +2970,8 @@ const CALL_RISK_REMOVE_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, ti
2756
2970
  });
2757
2971
  const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2758
2972
  await ExecutionContextService.runInContext(async () => {
2759
- await self.params.partial.clear(symbol, signal, currentPrice, backtest);
2973
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2974
+ await self.params.partial.clear(symbol, publicSignal, currentPrice, backtest);
2760
2975
  }, {
2761
2976
  when: new Date(timestamp),
2762
2977
  symbol: symbol,
@@ -2805,9 +3020,10 @@ const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pen
2805
3020
  });
2806
3021
  const CALL_PARTIAL_PROFIT_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentTp, timestamp, backtest) => {
2807
3022
  await ExecutionContextService.runInContext(async () => {
2808
- await self.params.partial.profit(symbol, signal, currentPrice, percentTp, backtest, new Date(timestamp));
3023
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
3024
+ await self.params.partial.profit(symbol, publicSignal, currentPrice, percentTp, backtest, new Date(timestamp));
2809
3025
  if (self.params.callbacks?.onPartialProfit) {
2810
- self.params.callbacks.onPartialProfit(symbol, signal, currentPrice, percentTp, backtest);
3026
+ await self.params.callbacks.onPartialProfit(symbol, publicSignal, currentPrice, percentTp, backtest);
2811
3027
  }
2812
3028
  }, {
2813
3029
  when: new Date(timestamp),
@@ -2828,9 +3044,10 @@ const CALL_PARTIAL_PROFIT_CALLBACKS_FN = functoolsKit.trycatch(async (self, symb
2828
3044
  });
2829
3045
  const CALL_PARTIAL_LOSS_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentSl, timestamp, backtest) => {
2830
3046
  await ExecutionContextService.runInContext(async () => {
2831
- await self.params.partial.loss(symbol, signal, currentPrice, percentSl, backtest, new Date(timestamp));
3047
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
3048
+ await self.params.partial.loss(symbol, publicSignal, currentPrice, percentSl, backtest, new Date(timestamp));
2832
3049
  if (self.params.callbacks?.onPartialLoss) {
2833
- self.params.callbacks.onPartialLoss(symbol, signal, currentPrice, percentSl, backtest);
3050
+ await self.params.callbacks.onPartialLoss(symbol, publicSignal, currentPrice, percentSl, backtest);
2834
3051
  }
2835
3052
  }, {
2836
3053
  when: new Date(timestamp),
@@ -2854,7 +3071,7 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2854
3071
  await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
2855
3072
  const result = {
2856
3073
  action: "active",
2857
- signal: scheduled,
3074
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2858
3075
  currentPrice: currentPrice,
2859
3076
  strategyName: self.params.method.context.strategyName,
2860
3077
  exchangeName: self.params.method.context.exchangeName,
@@ -2880,7 +3097,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2880
3097
  await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2881
3098
  const result = {
2882
3099
  action: "scheduled",
2883
- signal: signal,
3100
+ signal: TO_PUBLIC_SIGNAL(signal),
2884
3101
  strategyName: self.params.method.context.strategyName,
2885
3102
  exchangeName: self.params.method.context.exchangeName,
2886
3103
  frameName: self.params.method.context.frameName,
@@ -2900,7 +3117,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2900
3117
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
2901
3118
  const result = {
2902
3119
  action: "opened",
2903
- signal: signal,
3120
+ signal: TO_PUBLIC_SIGNAL(signal),
2904
3121
  strategyName: self.params.method.context.strategyName,
2905
3122
  exchangeName: self.params.method.context.exchangeName,
2906
3123
  frameName: self.params.method.context.frameName,
@@ -2929,13 +3146,14 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
2929
3146
  return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
2930
3147
  "take_profit");
2931
3148
  }
2932
- // Check stop loss
2933
- if (signal.position === "long" && averagePrice <= signal.priceStopLoss) {
2934
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
3149
+ // Check stop loss (use trailing SL if set, otherwise original SL)
3150
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3151
+ if (signal.position === "long" && averagePrice <= effectiveStopLoss) {
3152
+ return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveStopLoss, // КРИТИЧНО: используем точную цену SL (trailing or original)
2935
3153
  "stop_loss");
2936
3154
  }
2937
- if (signal.position === "short" && averagePrice >= signal.priceStopLoss) {
2938
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
3155
+ if (signal.position === "short" && averagePrice >= effectiveStopLoss) {
3156
+ return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveStopLoss, // КРИТИЧНО: используем точную цену SL (trailing or original)
2939
3157
  "stop_loss");
2940
3158
  }
2941
3159
  return null;
@@ -2957,7 +3175,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2957
3175
  await self.setPendingSignal(null);
2958
3176
  const result = {
2959
3177
  action: "closed",
2960
- signal: signal,
3178
+ signal: TO_PUBLIC_SIGNAL(signal),
2961
3179
  currentPrice: currentPrice,
2962
3180
  closeReason: closeReason,
2963
3181
  closeTimestamp: currentTime,
@@ -2988,8 +3206,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2988
3206
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
2989
3207
  }
2990
3208
  else if (currentDistance < 0) {
2991
- // Moving towards SL
2992
- const slDistance = signal.priceOpen - signal.priceStopLoss;
3209
+ // Moving towards SL (use trailing SL if set)
3210
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3211
+ const slDistance = signal.priceOpen - effectiveStopLoss;
2993
3212
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2994
3213
  percentSl = Math.min(progressPercent, 100);
2995
3214
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -3006,8 +3225,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3006
3225
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
3007
3226
  }
3008
3227
  if (currentDistance < 0) {
3009
- // Moving towards SL
3010
- const slDistance = signal.priceStopLoss - signal.priceOpen;
3228
+ // Moving towards SL (use trailing SL if set)
3229
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3230
+ const slDistance = effectiveStopLoss - signal.priceOpen;
3011
3231
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
3012
3232
  percentSl = Math.min(progressPercent, 100);
3013
3233
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -3016,7 +3236,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3016
3236
  }
3017
3237
  const result = {
3018
3238
  action: "active",
3019
- signal: signal,
3239
+ signal: TO_PUBLIC_SIGNAL(signal),
3020
3240
  currentPrice: currentPrice,
3021
3241
  strategyName: self.params.method.context.strategyName,
3022
3242
  exchangeName: self.params.method.context.exchangeName,
@@ -3058,7 +3278,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
3058
3278
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
3059
3279
  const result = {
3060
3280
  action: "cancelled",
3061
- signal: scheduled,
3281
+ signal: TO_PUBLIC_SIGNAL(scheduled),
3062
3282
  currentPrice: averagePrice,
3063
3283
  closeTimestamp: closeTimestamp,
3064
3284
  strategyName: self.params.method.context.strategyName,
@@ -3135,7 +3355,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
3135
3355
  await self.setPendingSignal(null);
3136
3356
  const result = {
3137
3357
  action: "closed",
3138
- signal: signal,
3358
+ signal: TO_PUBLIC_SIGNAL(signal),
3139
3359
  currentPrice: averagePrice,
3140
3360
  closeReason: closeReason,
3141
3361
  closeTimestamp: closeTimestamp,
@@ -3259,13 +3479,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3259
3479
  }
3260
3480
  // Check TP/SL only if not expired
3261
3481
  // КРИТИЧНО: используем candle.high/low для точной проверки достижения TP/SL
3482
+ // КРИТИЧНО: используем trailing SL если установлен
3483
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3262
3484
  if (!shouldClose && signal.position === "long") {
3263
3485
  // Для LONG: TP срабатывает если high >= TP, SL если low <= SL
3264
3486
  if (currentCandle.high >= signal.priceTakeProfit) {
3265
3487
  shouldClose = true;
3266
3488
  closeReason = "take_profit";
3267
3489
  }
3268
- else if (currentCandle.low <= signal.priceStopLoss) {
3490
+ else if (currentCandle.low <= effectiveStopLoss) {
3269
3491
  shouldClose = true;
3270
3492
  closeReason = "stop_loss";
3271
3493
  }
@@ -3276,7 +3498,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3276
3498
  shouldClose = true;
3277
3499
  closeReason = "take_profit";
3278
3500
  }
3279
- else if (currentCandle.high >= signal.priceStopLoss) {
3501
+ else if (currentCandle.high >= effectiveStopLoss) {
3280
3502
  shouldClose = true;
3281
3503
  closeReason = "stop_loss";
3282
3504
  }
@@ -3288,7 +3510,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3288
3510
  closePrice = signal.priceTakeProfit;
3289
3511
  }
3290
3512
  else if (closeReason === "stop_loss") {
3291
- closePrice = signal.priceStopLoss;
3513
+ closePrice = effectiveStopLoss; // Используем trailing SL если установлен
3292
3514
  }
3293
3515
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
3294
3516
  }
@@ -3305,8 +3527,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3305
3527
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
3306
3528
  }
3307
3529
  else if (currentDistance < 0) {
3308
- // Moving towards SL
3309
- const slDistance = signal.priceOpen - signal.priceStopLoss;
3530
+ // Moving towards SL (use trailing SL if set)
3531
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3532
+ const slDistance = signal.priceOpen - effectiveStopLoss;
3310
3533
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
3311
3534
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
3312
3535
  }
@@ -3321,8 +3544,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3321
3544
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
3322
3545
  }
3323
3546
  if (currentDistance < 0) {
3324
- // Moving towards SL
3325
- const slDistance = signal.priceStopLoss - signal.priceOpen;
3547
+ // Moving towards SL (use trailing SL if set)
3548
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3549
+ const slDistance = effectiveStopLoss - signal.priceOpen;
3326
3550
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
3327
3551
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
3328
3552
  }
@@ -3394,7 +3618,8 @@ class ClientStrategy {
3394
3618
  // КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
3395
3619
  // даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
3396
3620
  if (this.params.callbacks?.onWrite) {
3397
- this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, this.params.execution.context.backtest);
3621
+ const publicSignal = this._pendingSignal ? TO_PUBLIC_SIGNAL(this._pendingSignal) : null;
3622
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, this.params.execution.context.backtest);
3398
3623
  }
3399
3624
  if (this.params.execution.context.backtest) {
3400
3625
  return;
@@ -3429,7 +3654,7 @@ class ClientStrategy {
3429
3654
  this.params.logger.debug("ClientStrategy getPendingSignal", {
3430
3655
  symbol,
3431
3656
  });
3432
- return this._pendingSignal;
3657
+ return this._pendingSignal ? TO_PUBLIC_SIGNAL(this._pendingSignal) : null;
3433
3658
  }
3434
3659
  /**
3435
3660
  * Retrieves the current scheduled signal.
@@ -3440,7 +3665,7 @@ class ClientStrategy {
3440
3665
  this.params.logger.debug("ClientStrategy getScheduledSignal", {
3441
3666
  symbol,
3442
3667
  });
3443
- return this._scheduledSignal;
3668
+ return this._scheduledSignal ? TO_PUBLIC_SIGNAL(this._scheduledSignal) : null;
3444
3669
  }
3445
3670
  /**
3446
3671
  * Returns the stopped state of the strategy.
@@ -3513,7 +3738,7 @@ class ClientStrategy {
3513
3738
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
3514
3739
  const result = {
3515
3740
  action: "cancelled",
3516
- signal: cancelledSignal,
3741
+ signal: TO_PUBLIC_SIGNAL(cancelledSignal),
3517
3742
  currentPrice,
3518
3743
  closeTimestamp: currentTime,
3519
3744
  strategyName: this.params.method.context.strategyName,
@@ -3626,7 +3851,7 @@ class ClientStrategy {
3626
3851
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
3627
3852
  const cancelledResult = {
3628
3853
  action: "cancelled",
3629
- signal: cancelledSignal,
3854
+ signal: TO_PUBLIC_SIGNAL(cancelledSignal),
3630
3855
  currentPrice,
3631
3856
  closeTimestamp: closeTimestamp,
3632
3857
  strategyName: this.params.method.context.strategyName,
@@ -3688,7 +3913,7 @@ class ClientStrategy {
3688
3913
  // but this is correct behavior if someone calls backtest() with partial data
3689
3914
  const result = {
3690
3915
  action: "active",
3691
- signal: scheduled,
3916
+ signal: TO_PUBLIC_SIGNAL(scheduled),
3692
3917
  currentPrice: lastPrice,
3693
3918
  percentSl: 0,
3694
3919
  percentTp: 0,
@@ -3893,7 +4118,7 @@ class ClientStrategy {
3893
4118
  });
3894
4119
  // Call onWrite callback for testing persist storage
3895
4120
  if (this.params.callbacks?.onWrite) {
3896
- this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
4121
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
3897
4122
  }
3898
4123
  if (!backtest) {
3899
4124
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
@@ -3985,7 +4210,90 @@ class ClientStrategy {
3985
4210
  });
3986
4211
  // Call onWrite callback for testing persist storage
3987
4212
  if (this.params.callbacks?.onWrite) {
3988
- this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, backtest);
4213
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
4214
+ }
4215
+ if (!backtest) {
4216
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
4217
+ }
4218
+ }
4219
+ /**
4220
+ * Adjusts trailing stop-loss by shifting distance between entry and original SL.
4221
+ *
4222
+ * Calculates new SL based on percentage shift of the distance (entry - originalSL):
4223
+ * - Negative %: tightens stop (moves SL closer to entry, reduces risk)
4224
+ * - Positive %: loosens stop (moves SL away from entry, allows more drawdown)
4225
+ *
4226
+ * For LONG position (entry=100, originalSL=90, distance=10):
4227
+ * - percentShift = -50: newSL = 100 - 10*(1-0.5) = 95 (tighter, closer to entry)
4228
+ * - percentShift = +20: newSL = 100 - 10*(1+0.2) = 88 (looser, away from entry)
4229
+ *
4230
+ * For SHORT position (entry=100, originalSL=110, distance=10):
4231
+ * - percentShift = -50: newSL = 100 + 10*(1-0.5) = 105 (tighter, closer to entry)
4232
+ * - percentShift = +20: newSL = 100 + 10*(1+0.2) = 112 (looser, away from entry)
4233
+ *
4234
+ * Trailing behavior:
4235
+ * - Only updates if new SL is BETTER (protects more profit)
4236
+ * - For LONG: only accepts higher SL (never moves down)
4237
+ * - For SHORT: only accepts lower SL (never moves up)
4238
+ * - Validates that SL never crosses entry price
4239
+ * - Stores in _trailingPriceStopLoss, original priceStopLoss preserved
4240
+ *
4241
+ * Validation:
4242
+ * - Throws if no pending signal exists
4243
+ * - Throws if percentShift is not a finite number
4244
+ * - Throws if percentShift < -100 or > 100
4245
+ * - Throws if percentShift === 0
4246
+ * - Skips if new SL would cross entry price
4247
+ *
4248
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4249
+ * @param percentShift - Percentage shift of SL distance [-100, 100], excluding 0
4250
+ * @param backtest - Whether running in backtest mode (controls persistence)
4251
+ * @returns Promise that resolves when trailing SL is updated and persisted
4252
+ *
4253
+ * @example
4254
+ * ```typescript
4255
+ * // LONG position: entry=100, originalSL=90, distance=10
4256
+ *
4257
+ * // Move SL 50% closer to entry (tighten)
4258
+ * await strategy.trailingStop("BTCUSDT", -50, false);
4259
+ * // newSL = 100 - 10*(1-0.5) = 95
4260
+ *
4261
+ * // Move SL 30% away from entry (loosen, allow more drawdown)
4262
+ * await strategy.trailingStop("BTCUSDT", 30, false);
4263
+ * // newSL = 100 - 10*(1+0.3) = 87 (SKIPPED: worse than current 95)
4264
+ * ```
4265
+ */
4266
+ async trailingStop(symbol, percentShift, backtest) {
4267
+ this.params.logger.debug("ClientStrategy trailingStop", {
4268
+ symbol,
4269
+ percentShift,
4270
+ hasPendingSignal: this._pendingSignal !== null,
4271
+ });
4272
+ // Validation: must have pending signal
4273
+ if (!this._pendingSignal) {
4274
+ throw new Error(`ClientStrategy trailingStop: No pending signal exists for symbol=${symbol}`);
4275
+ }
4276
+ // Validation: percentShift must be valid
4277
+ if (typeof percentShift !== "number" || !isFinite(percentShift)) {
4278
+ throw new Error(`ClientStrategy trailingStop: percentShift must be a finite number, got ${percentShift} (${typeof percentShift})`);
4279
+ }
4280
+ if (percentShift < -100 || percentShift > 100) {
4281
+ throw new Error(`ClientStrategy trailingStop: percentShift must be in range [-100, 100], got ${percentShift}`);
4282
+ }
4283
+ if (percentShift === 0) {
4284
+ throw new Error(`ClientStrategy trailingStop: percentShift cannot be 0`);
4285
+ }
4286
+ // Execute trailing logic
4287
+ TRAILING_STOP_FN(this, this._pendingSignal, percentShift);
4288
+ // Persist updated signal state (inline setPendingSignal content)
4289
+ // Note: this._pendingSignal already mutated by TRAILING_STOP_FN, no reassignment needed
4290
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
4291
+ pendingSignal: this._pendingSignal,
4292
+ });
4293
+ // Call onWrite callback for testing persist storage
4294
+ if (this.params.callbacks?.onWrite) {
4295
+ const publicSignal = TO_PUBLIC_SIGNAL(this._pendingSignal);
4296
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
3989
4297
  }
3990
4298
  if (!backtest) {
3991
4299
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
@@ -4659,40 +4967,76 @@ class StrategyConnectionService {
4659
4967
  /**
4660
4968
  * Executes partial close at loss level (moving toward SL).
4661
4969
  *
4662
- * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
4663
- * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4970
+ * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
4971
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4972
+ *
4973
+ * Delegates to ClientStrategy.partialLoss() with current execution context.
4974
+ *
4975
+ * @param backtest - Whether running in backtest mode
4976
+ * @param symbol - Trading pair symbol
4977
+ * @param context - Execution context with strategyName, exchangeName, frameName
4978
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4979
+ * @param currentPrice - Current market price for this partial close
4980
+ * @returns Promise that resolves when state is updated and persisted
4981
+ *
4982
+ * @example
4983
+ * ```typescript
4984
+ * // Close 40% of position at loss
4985
+ * await strategyConnectionService.partialLoss(
4986
+ * false,
4987
+ * "BTCUSDT",
4988
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4989
+ * 40,
4990
+ * 38000
4991
+ * );
4992
+ * ```
4993
+ */
4994
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
4995
+ this.loggerService.log("strategyConnectionService partialLoss", {
4996
+ symbol,
4997
+ context,
4998
+ percentToClose,
4999
+ currentPrice,
5000
+ backtest,
5001
+ });
5002
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5003
+ await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
5004
+ };
5005
+ /**
5006
+ * Adjusts the trailing stop-loss distance for an active pending signal.
5007
+ *
5008
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
5009
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
4664
5010
  *
4665
- * Delegates to ClientStrategy.partialLoss() with current execution context.
5011
+ * Delegates to ClientStrategy.trailingStop() with current execution context.
4666
5012
  *
4667
5013
  * @param backtest - Whether running in backtest mode
4668
5014
  * @param symbol - Trading pair symbol
5015
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
4669
5016
  * @param context - Execution context with strategyName, exchangeName, frameName
4670
- * @param percentToClose - Percentage of position to close (0-100, absolute value)
4671
- * @param currentPrice - Current market price for this partial close
4672
- * @returns Promise that resolves when state is updated and persisted
5017
+ * @returns Promise that resolves when trailing SL is updated
4673
5018
  *
4674
5019
  * @example
4675
5020
  * ```typescript
4676
- * // Close 40% of position at loss
4677
- * await strategyConnectionService.partialLoss(
5021
+ * // LONG: entry=100, originalSL=90, distance=10
5022
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
5023
+ * await strategyConnectionService.trailingStop(
4678
5024
  * false,
4679
5025
  * "BTCUSDT",
4680
- * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4681
- * 40,
4682
- * 38000
5026
+ * -50,
5027
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
4683
5028
  * );
4684
5029
  * ```
4685
5030
  */
4686
- this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
4687
- this.loggerService.log("strategyConnectionService partialLoss", {
5031
+ this.trailingStop = async (backtest, symbol, percentShift, context) => {
5032
+ this.loggerService.log("strategyConnectionService trailingStop", {
4688
5033
  symbol,
4689
5034
  context,
4690
- percentToClose,
4691
- currentPrice,
5035
+ percentShift,
4692
5036
  backtest,
4693
5037
  });
4694
5038
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4695
- await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
5039
+ await strategy.trailingStop(symbol, percentShift, backtest);
4696
5040
  };
4697
5041
  }
4698
5042
  }
@@ -4716,6 +5060,32 @@ const INTERVAL_MINUTES$2 = {
4716
5060
  "1d": 1440,
4717
5061
  "3d": 4320,
4718
5062
  };
5063
+ /**
5064
+ * Wrapper to call onTimeframe callback with error handling.
5065
+ * Catches and logs any errors thrown by the user-provided callback.
5066
+ *
5067
+ * @param self - ClientFrame instance reference
5068
+ * @param timeframe - Array of generated timestamp dates
5069
+ * @param startDate - Start date of the backtest period
5070
+ * @param endDate - Effective end date of the backtest period
5071
+ * @param interval - Frame interval used for generation
5072
+ */
5073
+ const CALL_TIMEFRAME_CALLBACKS_FN = functoolsKit.trycatch(async (self, timeframe, startDate, endDate, interval) => {
5074
+ if (self.params.callbacks?.onTimeframe) {
5075
+ await self.params.callbacks.onTimeframe(timeframe, startDate, endDate, interval);
5076
+ }
5077
+ }, {
5078
+ fallback: (error) => {
5079
+ const message = "ClientFrame CALL_TIMEFRAME_CALLBACKS_FN thrown";
5080
+ const payload = {
5081
+ error: functoolsKit.errorData(error),
5082
+ message: functoolsKit.getErrorMessage(error),
5083
+ };
5084
+ backtest$1.loggerService.warn(message, payload);
5085
+ console.warn(message, payload);
5086
+ errorEmitter.next(error);
5087
+ },
5088
+ });
4719
5089
  /**
4720
5090
  * Generates timeframe array from startDate to endDate with specified interval.
4721
5091
  * Uses prototype function pattern for memory efficiency.
@@ -4745,9 +5115,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
4745
5115
  timeframes.push(new Date(currentDate));
4746
5116
  currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
4747
5117
  }
4748
- if (self.params.callbacks?.onTimeframe) {
4749
- self.params.callbacks.onTimeframe(timeframes, startDate, effectiveEndDate, interval);
4750
- }
5118
+ await CALL_TIMEFRAME_CALLBACKS_FN(self, timeframes, startDate, effectiveEndDate, interval);
4751
5119
  return timeframes;
4752
5120
  };
4753
5121
  /**
@@ -4906,6 +5274,30 @@ const calculateATRBased = (params, schema) => {
4906
5274
  const stopDistance = atr * atrMultiplier;
4907
5275
  return riskAmount / stopDistance;
4908
5276
  };
5277
+ /**
5278
+ * Wrapper to call onCalculate callback with error handling.
5279
+ * Catches and logs any errors thrown by the user-provided callback.
5280
+ *
5281
+ * @param self - ClientSizing instance reference
5282
+ * @param quantity - Calculated position size
5283
+ * @param params - Parameters used for size calculation
5284
+ */
5285
+ const CALL_CALCULATE_CALLBACKS_FN = functoolsKit.trycatch(async (self, quantity, params) => {
5286
+ if (self.params.callbacks?.onCalculate) {
5287
+ await self.params.callbacks.onCalculate(quantity, params);
5288
+ }
5289
+ }, {
5290
+ fallback: (error) => {
5291
+ const message = "ClientSizing CALL_CALCULATE_CALLBACKS_FN thrown";
5292
+ const payload = {
5293
+ error: functoolsKit.errorData(error),
5294
+ message: functoolsKit.getErrorMessage(error),
5295
+ };
5296
+ backtest$1.loggerService.warn(message, payload);
5297
+ console.warn(message, payload);
5298
+ errorEmitter.next(error);
5299
+ },
5300
+ });
4909
5301
  /**
4910
5302
  * Main calculation function routing to specific sizing method.
4911
5303
  * Applies min/max constraints after calculation.
@@ -4959,9 +5351,7 @@ const CALCULATE_FN = async (params, self) => {
4959
5351
  quantity = Math.min(quantity, schema.maxPositionSize);
4960
5352
  }
4961
5353
  // Trigger callback if defined
4962
- if (schema.callbacks?.onCalculate) {
4963
- schema.callbacks.onCalculate(quantity, params);
4964
- }
5354
+ await CALL_CALCULATE_CALLBACKS_FN(self, quantity, params);
4965
5355
  return quantity;
4966
5356
  };
4967
5357
  /**
@@ -5077,6 +5467,50 @@ const get = (object, path) => {
5077
5467
 
5078
5468
  /** Symbol indicating that positions need to be fetched from persistence */
5079
5469
  const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
5470
+ /**
5471
+ * Converts signal to risk validation format.
5472
+ *
5473
+ * This function is used BEFORE position opens during risk checks.
5474
+ * It ensures all required fields are present for risk validation:
5475
+ *
5476
+ * - Falls back to currentPrice if priceOpen is not set (for ISignalDto/scheduled signals)
5477
+ * - Replaces priceStopLoss with trailing SL if active (for positions with trailing stops)
5478
+ * - Preserves original stop-loss in originalPriceStopLoss for reference
5479
+ *
5480
+ * Use cases:
5481
+ * - Risk validation before opening a position (checkSignal)
5482
+ * - Pre-flight validation of scheduled signals
5483
+ * - Calculating position size based on stop-loss distance
5484
+ *
5485
+ * @param signal - Signal DTO or row (may not have priceOpen for scheduled signals)
5486
+ * @param currentPrice - Current market price, used as fallback for priceOpen if not set
5487
+ * @returns Signal in IRiskSignalRow format with guaranteed priceOpen and effective stop-loss
5488
+ *
5489
+ * @example
5490
+ * ```typescript
5491
+ * // For scheduled signal without priceOpen
5492
+ * const riskSignal = TO_RISK_SIGNAL(scheduledSignal, 45000);
5493
+ * // riskSignal.priceOpen = 45000 (fallback to currentPrice)
5494
+ *
5495
+ * // For signal with trailing SL
5496
+ * const riskSignal = TO_RISK_SIGNAL(activeSignal, 46000);
5497
+ * // riskSignal.priceStopLoss = activeSignal._trailingPriceStopLoss
5498
+ * ```
5499
+ */
5500
+ const TO_RISK_SIGNAL = (signal, currentPrice) => {
5501
+ if ("_trailingPriceStopLoss" in signal) {
5502
+ return {
5503
+ ...signal,
5504
+ priceStopLoss: signal._trailingPriceStopLoss,
5505
+ originalPriceStopLoss: signal.priceStopLoss,
5506
+ };
5507
+ }
5508
+ return {
5509
+ ...signal,
5510
+ priceOpen: signal.priceOpen ?? currentPrice,
5511
+ originalPriceStopLoss: signal.priceStopLoss,
5512
+ };
5513
+ };
5080
5514
  /** Key generator for active position map */
5081
5515
  const CREATE_NAME_FN = (strategyName, exchangeName, symbol) => `${strategyName}_${exchangeName}_${symbol}`;
5082
5516
  /** Wrapper to execute risk validation function with error handling */
@@ -5096,6 +5530,40 @@ const DO_VALIDATION_FN = async (validation, params) => {
5096
5530
  return payload.message;
5097
5531
  }
5098
5532
  };
5533
+ /** Wrapper to call onRejected callback with error handling */
5534
+ const CALL_REJECTED_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, params) => {
5535
+ if (self.params.callbacks?.onRejected) {
5536
+ await self.params.callbacks.onRejected(symbol, params);
5537
+ }
5538
+ }, {
5539
+ fallback: (error) => {
5540
+ const message = "ClientRisk CALL_REJECTED_CALLBACKS_FN thrown";
5541
+ const payload = {
5542
+ error: functoolsKit.errorData(error),
5543
+ message: functoolsKit.getErrorMessage(error),
5544
+ };
5545
+ backtest$1.loggerService.warn(message, payload);
5546
+ console.warn(message, payload);
5547
+ errorEmitter.next(error);
5548
+ },
5549
+ });
5550
+ /** Wrapper to call onAllowed callback with error handling */
5551
+ const CALL_ALLOWED_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, params) => {
5552
+ if (self.params.callbacks?.onAllowed) {
5553
+ await self.params.callbacks.onAllowed(symbol, params);
5554
+ }
5555
+ }, {
5556
+ fallback: (error) => {
5557
+ const message = "ClientRisk CALL_ALLOWED_CALLBACKS_FN thrown";
5558
+ const payload = {
5559
+ error: functoolsKit.errorData(error),
5560
+ message: functoolsKit.getErrorMessage(error),
5561
+ };
5562
+ backtest$1.loggerService.warn(message, payload);
5563
+ console.warn(message, payload);
5564
+ errorEmitter.next(error);
5565
+ },
5566
+ });
5099
5567
  /**
5100
5568
  * Initializes active positions by reading from persistence.
5101
5569
  * Uses singleshot pattern to ensure it only runs once.
@@ -5166,6 +5634,7 @@ class ClientRisk {
5166
5634
  const riskMap = this._activePositions;
5167
5635
  const payload = {
5168
5636
  ...params,
5637
+ pendingSignal: TO_RISK_SIGNAL(params.pendingSignal, params.currentPrice),
5169
5638
  activePositionCount: riskMap.size,
5170
5639
  activePositions: Array.from(riskMap.values()),
5171
5640
  };
@@ -5200,15 +5669,11 @@ class ClientRisk {
5200
5669
  // Call params.onRejected for riskSubject emission
5201
5670
  await this.params.onRejected(params.symbol, params, riskMap.size, rejectionResult, params.timestamp, this.params.backtest);
5202
5671
  // Call schema callbacks.onRejected if defined
5203
- if (this.params.callbacks?.onRejected) {
5204
- this.params.callbacks.onRejected(params.symbol, params);
5205
- }
5672
+ await CALL_REJECTED_CALLBACKS_FN(this, params.symbol, params);
5206
5673
  return false;
5207
5674
  }
5208
5675
  // All checks passed
5209
- if (this.params.callbacks?.onAllowed) {
5210
- this.params.callbacks.onAllowed(params.symbol, params);
5211
- }
5676
+ await CALL_ALLOWED_CALLBACKS_FN(this, params.symbol, params);
5212
5677
  return true;
5213
5678
  };
5214
5679
  }
@@ -5896,6 +6361,42 @@ class StrategyCoreService {
5896
6361
  await this.validate(symbol, context);
5897
6362
  return await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
5898
6363
  };
6364
+ /**
6365
+ * Adjusts the trailing stop-loss distance for an active pending signal.
6366
+ *
6367
+ * Validates strategy existence and delegates to connection service
6368
+ * to update the stop-loss distance by a percentage adjustment.
6369
+ *
6370
+ * Does not require execution context as this is a direct state mutation.
6371
+ *
6372
+ * @param backtest - Whether running in backtest mode
6373
+ * @param symbol - Trading pair symbol
6374
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
6375
+ * @param context - Execution context with strategyName, exchangeName, frameName
6376
+ * @returns Promise that resolves when trailing SL is updated
6377
+ *
6378
+ * @example
6379
+ * ```typescript
6380
+ * // LONG: entry=100, originalSL=90, distance=10
6381
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
6382
+ * await strategyCoreService.trailingStop(
6383
+ * false,
6384
+ * "BTCUSDT",
6385
+ * -50,
6386
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
6387
+ * );
6388
+ * ```
6389
+ */
6390
+ this.trailingStop = async (backtest, symbol, percentShift, context) => {
6391
+ this.loggerService.log("strategyCoreService trailingStop", {
6392
+ symbol,
6393
+ percentShift,
6394
+ context,
6395
+ backtest,
6396
+ });
6397
+ await this.validate(symbol, context);
6398
+ return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, context);
6399
+ };
5899
6400
  }
5900
6401
  }
5901
6402
 
@@ -7036,6 +7537,104 @@ class LiveLogicPrivateService {
7036
7537
  }
7037
7538
  }
7038
7539
 
7540
+ /**
7541
+ * Wrapper to call onStrategyStart callback with error handling.
7542
+ * Catches and logs any errors thrown by the user-provided callback.
7543
+ *
7544
+ * @param walkerSchema - Walker schema containing callbacks
7545
+ * @param strategyName - Name of the strategy being tested
7546
+ * @param symbol - Trading pair symbol
7547
+ */
7548
+ const CALL_STRATEGY_START_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol) => {
7549
+ if (walkerSchema.callbacks?.onStrategyStart) {
7550
+ await walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
7551
+ }
7552
+ }, {
7553
+ fallback: (error) => {
7554
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_START_CALLBACKS_FN thrown";
7555
+ const payload = {
7556
+ error: functoolsKit.errorData(error),
7557
+ message: functoolsKit.getErrorMessage(error),
7558
+ };
7559
+ backtest$1.loggerService.warn(message, payload);
7560
+ console.warn(message, payload);
7561
+ errorEmitter.next(error);
7562
+ },
7563
+ });
7564
+ /**
7565
+ * Wrapper to call onStrategyError callback with error handling.
7566
+ * Catches and logs any errors thrown by the user-provided callback.
7567
+ *
7568
+ * @param walkerSchema - Walker schema containing callbacks
7569
+ * @param strategyName - Name of the strategy that failed
7570
+ * @param symbol - Trading pair symbol
7571
+ * @param error - Error that occurred during strategy execution
7572
+ */
7573
+ const CALL_STRATEGY_ERROR_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol, error) => {
7574
+ if (walkerSchema.callbacks?.onStrategyError) {
7575
+ await walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
7576
+ }
7577
+ }, {
7578
+ fallback: (error) => {
7579
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_ERROR_CALLBACKS_FN thrown";
7580
+ const payload = {
7581
+ error: functoolsKit.errorData(error),
7582
+ message: functoolsKit.getErrorMessage(error),
7583
+ };
7584
+ backtest$1.loggerService.warn(message, payload);
7585
+ console.warn(message, payload);
7586
+ errorEmitter.next(error);
7587
+ },
7588
+ });
7589
+ /**
7590
+ * Wrapper to call onStrategyComplete callback with error handling.
7591
+ * Catches and logs any errors thrown by the user-provided callback.
7592
+ *
7593
+ * @param walkerSchema - Walker schema containing callbacks
7594
+ * @param strategyName - Name of the strategy that completed
7595
+ * @param symbol - Trading pair symbol
7596
+ * @param stats - Backtest statistics for the strategy
7597
+ * @param metricValue - Calculated metric value for comparison
7598
+ */
7599
+ const CALL_STRATEGY_COMPLETE_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol, stats, metricValue) => {
7600
+ if (walkerSchema.callbacks?.onStrategyComplete) {
7601
+ await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
7602
+ }
7603
+ }, {
7604
+ fallback: (error) => {
7605
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_COMPLETE_CALLBACKS_FN thrown";
7606
+ const payload = {
7607
+ error: functoolsKit.errorData(error),
7608
+ message: functoolsKit.getErrorMessage(error),
7609
+ };
7610
+ backtest$1.loggerService.warn(message, payload);
7611
+ console.warn(message, payload);
7612
+ errorEmitter.next(error);
7613
+ },
7614
+ });
7615
+ /**
7616
+ * Wrapper to call onComplete callback with error handling.
7617
+ * Catches and logs any errors thrown by the user-provided callback.
7618
+ *
7619
+ * @param walkerSchema - Walker schema containing callbacks
7620
+ * @param finalResults - Final results with all strategies ranked
7621
+ */
7622
+ const CALL_COMPLETE_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, finalResults) => {
7623
+ if (walkerSchema.callbacks?.onComplete) {
7624
+ await walkerSchema.callbacks.onComplete(finalResults);
7625
+ }
7626
+ }, {
7627
+ fallback: (error) => {
7628
+ const message = "WalkerLogicPrivateService CALL_COMPLETE_CALLBACKS_FN thrown";
7629
+ const payload = {
7630
+ error: functoolsKit.errorData(error),
7631
+ message: functoolsKit.getErrorMessage(error),
7632
+ };
7633
+ backtest$1.loggerService.warn(message, payload);
7634
+ console.warn(message, payload);
7635
+ errorEmitter.next(error);
7636
+ },
7637
+ });
7039
7638
  /**
7040
7639
  * Private service for walker orchestration (strategy comparison).
7041
7640
  *
@@ -7121,9 +7720,7 @@ class WalkerLogicPrivateService {
7121
7720
  break;
7122
7721
  }
7123
7722
  // Call onStrategyStart callback if provided
7124
- if (walkerSchema.callbacks?.onStrategyStart) {
7125
- walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
7126
- }
7723
+ await CALL_STRATEGY_START_CALLBACKS_FN(walkerSchema, strategyName, symbol);
7127
7724
  this.loggerService.info("walkerLogicPrivateService testing strategy", {
7128
7725
  strategyName,
7129
7726
  symbol,
@@ -7145,9 +7742,7 @@ class WalkerLogicPrivateService {
7145
7742
  });
7146
7743
  await errorEmitter.next(error);
7147
7744
  // Call onStrategyError callback if provided
7148
- if (walkerSchema.callbacks?.onStrategyError) {
7149
- walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
7150
- }
7745
+ await CALL_STRATEGY_ERROR_CALLBACKS_FN(walkerSchema, strategyName, symbol, error);
7151
7746
  continue;
7152
7747
  }
7153
7748
  this.loggerService.info("walkerLogicPrivateService backtest complete", {
@@ -7198,9 +7793,7 @@ class WalkerLogicPrivateService {
7198
7793
  progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
7199
7794
  });
7200
7795
  // Call onStrategyComplete callback if provided
7201
- if (walkerSchema.callbacks?.onStrategyComplete) {
7202
- await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
7203
- }
7796
+ await CALL_STRATEGY_COMPLETE_CALLBACKS_FN(walkerSchema, strategyName, symbol, stats, metricValue);
7204
7797
  await walkerEmitter.next(walkerContract);
7205
7798
  yield walkerContract;
7206
7799
  }
@@ -7223,9 +7816,7 @@ class WalkerLogicPrivateService {
7223
7816
  : null,
7224
7817
  };
7225
7818
  // Call onComplete callback if provided with final best results
7226
- if (walkerSchema.callbacks?.onComplete) {
7227
- walkerSchema.callbacks.onComplete(finalResults);
7228
- }
7819
+ await CALL_COMPLETE_CALLBACKS_FN(walkerSchema, finalResults);
7229
7820
  await walkerCompleteSubject.next(finalResults);
7230
7821
  }
7231
7822
  }
@@ -8988,7 +9579,7 @@ class BacktestMarkdownService {
8988
9579
  */
8989
9580
  this.init = functoolsKit.singleshot(async () => {
8990
9581
  this.loggerService.log("backtestMarkdownService init");
8991
- signalBacktestEmitter.subscribe(this.tick);
9582
+ this.unsubscribe = signalBacktestEmitter.subscribe(this.tick);
8992
9583
  });
8993
9584
  }
8994
9585
  }
@@ -9517,7 +10108,7 @@ class LiveMarkdownService {
9517
10108
  */
9518
10109
  this.init = functoolsKit.singleshot(async () => {
9519
10110
  this.loggerService.log("liveMarkdownService init");
9520
- signalLiveEmitter.subscribe(this.tick);
10111
+ this.unsubscribe = signalLiveEmitter.subscribe(this.tick);
9521
10112
  });
9522
10113
  }
9523
10114
  }
@@ -9946,7 +10537,7 @@ class ScheduleMarkdownService {
9946
10537
  */
9947
10538
  this.init = functoolsKit.singleshot(async () => {
9948
10539
  this.loggerService.log("scheduleMarkdownService init");
9949
- signalEmitter.subscribe(this.tick);
10540
+ this.unsubscribe = signalEmitter.subscribe(this.tick);
9950
10541
  });
9951
10542
  }
9952
10543
  }
@@ -10314,7 +10905,7 @@ class PerformanceMarkdownService {
10314
10905
  */
10315
10906
  this.init = functoolsKit.singleshot(async () => {
10316
10907
  this.loggerService.log("performanceMarkdownService init");
10317
- performanceEmitter.subscribe(this.track);
10908
+ this.unsubscribe = performanceEmitter.subscribe(this.track);
10318
10909
  });
10319
10910
  }
10320
10911
  }
@@ -10723,7 +11314,7 @@ class WalkerMarkdownService {
10723
11314
  */
10724
11315
  this.init = functoolsKit.singleshot(async () => {
10725
11316
  this.loggerService.log("walkerMarkdownService init");
10726
- walkerEmitter.subscribe(this.tick);
11317
+ this.unsubscribe = walkerEmitter.subscribe(this.tick);
10727
11318
  });
10728
11319
  }
10729
11320
  }
@@ -11250,7 +11841,7 @@ class HeatMarkdownService {
11250
11841
  */
11251
11842
  this.init = functoolsKit.singleshot(async () => {
11252
11843
  this.loggerService.log("heatMarkdownService init");
11253
- signalEmitter.subscribe(this.tick);
11844
+ this.unsubscribe = signalEmitter.subscribe(this.tick);
11254
11845
  });
11255
11846
  }
11256
11847
  }
@@ -14014,8 +14605,9 @@ class PartialMarkdownService {
14014
14605
  */
14015
14606
  this.init = functoolsKit.singleshot(async () => {
14016
14607
  this.loggerService.log("partialMarkdownService init");
14017
- partialProfitSubject.subscribe(this.tickProfit);
14018
- partialLossSubject.subscribe(this.tickLoss);
14608
+ const unProfit = partialProfitSubject.subscribe(this.tickProfit);
14609
+ const unLoss = partialLossSubject.subscribe(this.tickLoss);
14610
+ this.unsubscribe = functoolsKit.compose(() => unProfit(), () => unLoss());
14019
14611
  });
14020
14612
  }
14021
14613
  }
@@ -14760,7 +15352,7 @@ class RiskMarkdownService {
14760
15352
  */
14761
15353
  this.init = functoolsKit.singleshot(async () => {
14762
15354
  this.loggerService.log("riskMarkdownService init");
14763
- riskSubject.subscribe(this.tickRejection);
15355
+ this.unsubscribe = riskSubject.subscribe(this.tickRejection);
14764
15356
  });
14765
15357
  }
14766
15358
  }
@@ -15419,6 +16011,7 @@ const STOP_METHOD_NAME = "strategy.stop";
15419
16011
  const CANCEL_METHOD_NAME = "strategy.cancel";
15420
16012
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
15421
16013
  const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
16014
+ const TRAILING_STOP_METHOD_NAME = "strategy.trailingStop";
15422
16015
  /**
15423
16016
  * Stops the strategy from generating new signals.
15424
16017
  *
@@ -15575,6 +16168,42 @@ async function partialLoss(symbol, percentToClose) {
15575
16168
  const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
15576
16169
  await backtest$1.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
15577
16170
  }
16171
+ /**
16172
+ * Adjusts the trailing stop-loss distance for an active pending signal.
16173
+ *
16174
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
16175
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
16176
+ *
16177
+ * Automatically detects backtest/live mode from execution context.
16178
+ *
16179
+ * @param symbol - Trading pair symbol
16180
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
16181
+ * @returns Promise that resolves when trailing SL is updated
16182
+ *
16183
+ * @example
16184
+ * ```typescript
16185
+ * import { trailingStop } from "backtest-kit";
16186
+ *
16187
+ * // LONG: entry=100, originalSL=90, distance=10
16188
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
16189
+ * await trailingStop("BTCUSDT", -50);
16190
+ * ```
16191
+ */
16192
+ async function trailingStop(symbol, percentShift) {
16193
+ backtest$1.loggerService.info(TRAILING_STOP_METHOD_NAME, {
16194
+ symbol,
16195
+ percentShift,
16196
+ });
16197
+ if (!ExecutionContextService.hasContext()) {
16198
+ throw new Error("trailingStop requires an execution context");
16199
+ }
16200
+ if (!MethodContextService.hasContext()) {
16201
+ throw new Error("trailingStop requires a method context");
16202
+ }
16203
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
16204
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
16205
+ await backtest$1.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, { exchangeName, frameName, strategyName });
16206
+ }
15578
16207
 
15579
16208
  /**
15580
16209
  * Sets custom logger implementation for the framework.
@@ -17412,6 +18041,7 @@ const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSig
17412
18041
  const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
17413
18042
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
17414
18043
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
18044
+ const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.trailingStop";
17415
18045
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
17416
18046
  /**
17417
18047
  * Internal task function that runs backtest and handles completion.
@@ -17980,6 +18610,44 @@ class BacktestUtils {
17980
18610
  }
17981
18611
  await backtest$1.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
17982
18612
  };
18613
+ /**
18614
+ * Adjusts the trailing stop-loss distance for an active pending signal.
18615
+ *
18616
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
18617
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
18618
+ *
18619
+ * @param symbol - Trading pair symbol
18620
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
18621
+ * @param context - Execution context with strategyName, exchangeName, and frameName
18622
+ * @returns Promise that resolves when trailing SL is updated
18623
+ *
18624
+ * @example
18625
+ * ```typescript
18626
+ * // LONG: entry=100, originalSL=90, distance=10
18627
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
18628
+ * await Backtest.trailingStop("BTCUSDT", -50, {
18629
+ * exchangeName: "binance",
18630
+ * frameName: "frame1",
18631
+ * strategyName: "my-strategy"
18632
+ * });
18633
+ * ```
18634
+ */
18635
+ this.trailingStop = async (symbol, percentShift, context) => {
18636
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_STOP, {
18637
+ symbol,
18638
+ percentShift,
18639
+ context,
18640
+ });
18641
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_STOP);
18642
+ {
18643
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18644
+ riskName &&
18645
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP);
18646
+ riskList &&
18647
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP));
18648
+ }
18649
+ await backtest$1.strategyCoreService.trailingStop(true, symbol, percentShift, context);
18650
+ };
17983
18651
  /**
17984
18652
  * Gets statistical data from all closed signals for a symbol-strategy pair.
17985
18653
  *
@@ -18141,6 +18809,7 @@ const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
18141
18809
  const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
18142
18810
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
18143
18811
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
18812
+ const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.trailingStop";
18144
18813
  /**
18145
18814
  * Internal task function that runs live trading and handles completion.
18146
18815
  * Consumes live trading results and updates instance state flags.
@@ -18685,6 +19354,45 @@ class LiveUtils {
18685
19354
  frameName: "",
18686
19355
  });
18687
19356
  };
19357
+ /**
19358
+ * Adjusts the trailing stop-loss distance for an active pending signal.
19359
+ *
19360
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
19361
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
19362
+ *
19363
+ * @param symbol - Trading pair symbol
19364
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
19365
+ * @param context - Execution context with strategyName and exchangeName
19366
+ * @returns Promise that resolves when trailing SL is updated
19367
+ *
19368
+ * @example
19369
+ * ```typescript
19370
+ * // LONG: entry=100, originalSL=90, distance=10
19371
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
19372
+ * await Live.trailingStop("BTCUSDT", -50, {
19373
+ * exchangeName: "binance",
19374
+ * strategyName: "my-strategy"
19375
+ * });
19376
+ * ```
19377
+ */
19378
+ this.trailingStop = async (symbol, percentShift, context) => {
19379
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_TRAILING_STOP, {
19380
+ symbol,
19381
+ percentShift,
19382
+ context,
19383
+ });
19384
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_STOP);
19385
+ {
19386
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
19387
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
19388
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
19389
+ }
19390
+ await backtest$1.strategyCoreService.trailingStop(false, symbol, percentShift, {
19391
+ strategyName: context.strategyName,
19392
+ exchangeName: context.exchangeName,
19393
+ frameName: "",
19394
+ });
19395
+ };
18688
19396
  /**
18689
19397
  * Gets statistical data from all live trading events for a symbol-strategy pair.
18690
19398
  *
@@ -21387,4 +22095,5 @@ exports.setColumns = setColumns;
21387
22095
  exports.setConfig = setConfig;
21388
22096
  exports.setLogger = setLogger;
21389
22097
  exports.stop = stop;
22098
+ exports.trailingStop = trailingStop;
21390
22099
  exports.validate = validate;