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