backtest-kit 1.7.2 → 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 +2673 -916
  2. package/build/index.mjs +2672 -918
  3. package/package.json +2 -1
  4. package/types.d.ts +1328 -417
package/build/index.cjs CHANGED
@@ -363,6 +363,136 @@ const GLOBAL_CONFIG = {
363
363
  };
364
364
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
365
365
 
366
+ /**
367
+ * Global signal emitter for all trading events (live + backtest).
368
+ * Emits all signal events regardless of execution mode.
369
+ */
370
+ const signalEmitter = new functoolsKit.Subject();
371
+ /**
372
+ * Live trading signal emitter.
373
+ * Emits only signals from live trading execution.
374
+ */
375
+ const signalLiveEmitter = new functoolsKit.Subject();
376
+ /**
377
+ * Backtest signal emitter.
378
+ * Emits only signals from backtest execution.
379
+ */
380
+ const signalBacktestEmitter = new functoolsKit.Subject();
381
+ /**
382
+ * Error emitter for background execution errors.
383
+ * Emits errors caught in background tasks (Live.background, Backtest.background).
384
+ */
385
+ const errorEmitter = new functoolsKit.Subject();
386
+ /**
387
+ * Exit emitter for critical errors that require process termination.
388
+ * Emits errors that should terminate the current execution (Backtest, Live, Walker).
389
+ * Unlike errorEmitter (for recoverable errors), exitEmitter signals fatal errors.
390
+ */
391
+ const exitEmitter = new functoolsKit.Subject();
392
+ /**
393
+ * Done emitter for live background execution completion.
394
+ * Emits when live background tasks complete (Live.background).
395
+ */
396
+ const doneLiveSubject = new functoolsKit.Subject();
397
+ /**
398
+ * Done emitter for backtest background execution completion.
399
+ * Emits when backtest background tasks complete (Backtest.background).
400
+ */
401
+ const doneBacktestSubject = new functoolsKit.Subject();
402
+ /**
403
+ * Done emitter for walker background execution completion.
404
+ * Emits when walker background tasks complete (Walker.background).
405
+ */
406
+ const doneWalkerSubject = new functoolsKit.Subject();
407
+ /**
408
+ * Progress emitter for backtest execution progress.
409
+ * Emits progress updates during backtest execution.
410
+ */
411
+ const progressBacktestEmitter = new functoolsKit.Subject();
412
+ /**
413
+ * Progress emitter for walker execution progress.
414
+ * Emits progress updates during walker execution.
415
+ */
416
+ const progressWalkerEmitter = new functoolsKit.Subject();
417
+ /**
418
+ * Progress emitter for optimizer execution progress.
419
+ * Emits progress updates during optimizer execution.
420
+ */
421
+ const progressOptimizerEmitter = new functoolsKit.Subject();
422
+ /**
423
+ * Performance emitter for execution metrics.
424
+ * Emits performance metrics for profiling and bottleneck detection.
425
+ */
426
+ const performanceEmitter = new functoolsKit.Subject();
427
+ /**
428
+ * Walker emitter for strategy comparison progress.
429
+ * Emits progress updates during walker execution (each strategy completion).
430
+ */
431
+ const walkerEmitter = new functoolsKit.Subject();
432
+ /**
433
+ * Walker complete emitter for strategy comparison completion.
434
+ * Emits when all strategies have been tested and final results are available.
435
+ */
436
+ const walkerCompleteSubject = new functoolsKit.Subject();
437
+ /**
438
+ * Walker stop emitter for walker cancellation events.
439
+ * Emits when a walker comparison is stopped/cancelled.
440
+ *
441
+ * Includes walkerName to support multiple walkers running on the same symbol.
442
+ */
443
+ const walkerStopSubject = new functoolsKit.Subject();
444
+ /**
445
+ * Validation emitter for risk validation errors.
446
+ * Emits when risk validation functions throw errors during signal checking.
447
+ */
448
+ const validationSubject = new functoolsKit.Subject();
449
+ /**
450
+ * Partial profit emitter for profit level milestones.
451
+ * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
452
+ */
453
+ const partialProfitSubject = new functoolsKit.Subject();
454
+ /**
455
+ * Partial loss emitter for loss level milestones.
456
+ * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
457
+ */
458
+ const partialLossSubject = new functoolsKit.Subject();
459
+ /**
460
+ * Risk rejection emitter for risk management violations.
461
+ * Emits ONLY when a signal is rejected due to risk validation failure.
462
+ * Does not emit for allowed signals (prevents spam).
463
+ */
464
+ const riskSubject = new functoolsKit.Subject();
465
+ /**
466
+ * Ping emitter for scheduled signal monitoring events.
467
+ * Emits every minute when a scheduled signal is being monitored (waiting for activation).
468
+ * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
469
+ */
470
+ const pingSubject = new functoolsKit.Subject();
471
+
472
+ var emitters = /*#__PURE__*/Object.freeze({
473
+ __proto__: null,
474
+ doneBacktestSubject: doneBacktestSubject,
475
+ doneLiveSubject: doneLiveSubject,
476
+ doneWalkerSubject: doneWalkerSubject,
477
+ errorEmitter: errorEmitter,
478
+ exitEmitter: exitEmitter,
479
+ partialLossSubject: partialLossSubject,
480
+ partialProfitSubject: partialProfitSubject,
481
+ performanceEmitter: performanceEmitter,
482
+ pingSubject: pingSubject,
483
+ progressBacktestEmitter: progressBacktestEmitter,
484
+ progressOptimizerEmitter: progressOptimizerEmitter,
485
+ progressWalkerEmitter: progressWalkerEmitter,
486
+ riskSubject: riskSubject,
487
+ signalBacktestEmitter: signalBacktestEmitter,
488
+ signalEmitter: signalEmitter,
489
+ signalLiveEmitter: signalLiveEmitter,
490
+ validationSubject: validationSubject,
491
+ walkerCompleteSubject: walkerCompleteSubject,
492
+ walkerEmitter: walkerEmitter,
493
+ walkerStopSubject: walkerStopSubject
494
+ });
495
+
366
496
  const INTERVAL_MINUTES$4 = {
367
497
  "1m": 1,
368
498
  "3m": 3,
@@ -464,6 +594,33 @@ const GET_CANDLES_FN = async (dto, since, self) => {
464
594
  }
465
595
  throw lastError;
466
596
  };
597
+ /**
598
+ * Wrapper to call onCandleData callback with error handling.
599
+ * Catches and logs any errors thrown by the user-provided callback.
600
+ *
601
+ * @param self - ClientExchange instance reference
602
+ * @param symbol - Trading pair symbol
603
+ * @param interval - Candle interval
604
+ * @param since - Start date for candle data
605
+ * @param limit - Number of candles
606
+ * @param data - Array of candle data
607
+ */
608
+ const CALL_CANDLE_DATA_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, interval, since, limit, data) => {
609
+ if (self.params.callbacks?.onCandleData) {
610
+ await self.params.callbacks.onCandleData(symbol, interval, since, limit, data);
611
+ }
612
+ }, {
613
+ fallback: (error) => {
614
+ const message = "ClientExchange CALL_CANDLE_DATA_CALLBACKS_FN thrown";
615
+ const payload = {
616
+ error: functoolsKit.errorData(error),
617
+ message: functoolsKit.getErrorMessage(error),
618
+ };
619
+ backtest$1.loggerService.warn(message, payload);
620
+ console.warn(message, payload);
621
+ errorEmitter.next(error);
622
+ },
623
+ });
467
624
  /**
468
625
  * Client implementation for exchange data access.
469
626
  *
@@ -522,9 +679,7 @@ class ClientExchange {
522
679
  if (filteredData.length < limit) {
523
680
  this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
524
681
  }
525
- if (this.params.callbacks?.onCandleData) {
526
- this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
527
- }
682
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, filteredData);
528
683
  return filteredData;
529
684
  }
530
685
  /**
@@ -559,9 +714,7 @@ class ClientExchange {
559
714
  if (filteredData.length < limit) {
560
715
  this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
561
716
  }
562
- if (this.params.callbacks?.onCandleData) {
563
- this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
564
- }
717
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, filteredData);
565
718
  return filteredData;
566
719
  }
567
720
  /**
@@ -772,6 +925,11 @@ class ExchangeConnectionService {
772
925
  /**
773
926
  * Calculates profit/loss for a closed signal with slippage and fees.
774
927
  *
928
+ * For signals with partial closes:
929
+ * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
930
+ * - Each partial close has its own fees and slippage
931
+ * - Total fees = 2 × (number of partial closes + 1 final close) × CC_PERCENT_FEE
932
+ *
775
933
  * Formula breakdown:
776
934
  * 1. Apply slippage to open/close prices (worse execution)
777
935
  * - LONG: buy higher (+slippage), sell lower (-slippage)
@@ -779,27 +937,113 @@ class ExchangeConnectionService {
779
937
  * 2. Calculate raw PNL percentage
780
938
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
781
939
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
782
- * 3. Subtract total fees (0.1% * 2 = 0.2%)
940
+ * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
783
941
  *
784
- * @param signal - Closed signal with position details
785
- * @param priceClose - Actual close price at exit
942
+ * @param signal - Closed signal with position details and optional partial history
943
+ * @param priceClose - Actual close price at final exit
786
944
  * @returns PNL data with percentage and prices
787
945
  *
788
946
  * @example
789
947
  * ```typescript
948
+ * // Signal without partial closes
790
949
  * const pnl = toProfitLossDto(
791
950
  * {
792
951
  * position: "long",
793
- * priceOpen: 50000,
794
- * // ... other signal fields
952
+ * priceOpen: 100,
953
+ * },
954
+ * 110 // close at +10%
955
+ * );
956
+ * console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
957
+ *
958
+ * // Signal with partial closes
959
+ * const pnlPartial = toProfitLossDto(
960
+ * {
961
+ * position: "long",
962
+ * priceOpen: 100,
963
+ * _partial: [
964
+ * { type: "profit", percent: 30, price: 120 }, // +20% on 30%
965
+ * { type: "profit", percent: 40, price: 115 }, // +15% on 40%
966
+ * ],
795
967
  * },
796
- * 51000 // close price
968
+ * 105 // final close at +5% for remaining 30%
797
969
  * );
798
- * console.log(pnl.pnlPercentage); // e.g., 1.8% (after slippage and fees)
970
+ * // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
799
971
  * ```
800
972
  */
801
973
  const toProfitLossDto = (signal, priceClose) => {
802
974
  const priceOpen = signal.priceOpen;
975
+ // Calculate weighted PNL with partial closes
976
+ if (signal._partial && signal._partial.length > 0) {
977
+ let totalWeightedPnl = 0;
978
+ let totalFees = 0;
979
+ // Calculate PNL for each partial close
980
+ for (const partial of signal._partial) {
981
+ const partialPercent = partial.percent;
982
+ const partialPrice = partial.price;
983
+ // Apply slippage to prices
984
+ let priceOpenWithSlippage;
985
+ let priceCloseWithSlippage;
986
+ if (signal.position === "long") {
987
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
988
+ priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
989
+ }
990
+ else {
991
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
992
+ priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
993
+ }
994
+ // Calculate PNL for this partial
995
+ let partialPnl;
996
+ if (signal.position === "long") {
997
+ partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
998
+ }
999
+ else {
1000
+ partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
1001
+ }
1002
+ // Weight by percentage of position closed
1003
+ const weightedPnl = (partialPercent / 100) * partialPnl;
1004
+ totalWeightedPnl += weightedPnl;
1005
+ // Each partial has fees for open + close (2 transactions)
1006
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
1007
+ }
1008
+ // Calculate PNL for remaining position (if any)
1009
+ // Compute totalClosed from _partial array
1010
+ const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
1011
+ const remainingPercent = 100 - totalClosed;
1012
+ if (remainingPercent > 0) {
1013
+ // Apply slippage
1014
+ let priceOpenWithSlippage;
1015
+ let priceCloseWithSlippage;
1016
+ if (signal.position === "long") {
1017
+ priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
1018
+ priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
1019
+ }
1020
+ else {
1021
+ priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
1022
+ priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
1023
+ }
1024
+ // Calculate PNL for remaining
1025
+ let remainingPnl;
1026
+ if (signal.position === "long") {
1027
+ remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
1028
+ }
1029
+ else {
1030
+ remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
1031
+ }
1032
+ // Weight by remaining percentage
1033
+ const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
1034
+ totalWeightedPnl += weightedRemainingPnl;
1035
+ // Final close also has fees
1036
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
1037
+ }
1038
+ // Subtract total fees from weighted PNL
1039
+ const pnlPercentage = totalWeightedPnl - totalFees;
1040
+ return {
1041
+ pnlPercentage,
1042
+ priceOpen,
1043
+ priceClose,
1044
+ };
1045
+ }
1046
+ // Original logic for signals without partial closes
803
1047
  let priceOpenWithSlippage;
804
1048
  let priceCloseWithSlippage;
805
1049
  if (signal.position === "long") {
@@ -1648,412 +1892,376 @@ class PersistPartialUtils {
1648
1892
  const PersistPartialAdapter = new PersistPartialUtils();
1649
1893
 
1650
1894
  /**
1651
- * Global signal emitter for all trading events (live + backtest).
1652
- * Emits all signal events regardless of execution mode.
1653
- */
1654
- const signalEmitter = new functoolsKit.Subject();
1655
- /**
1656
- * Live trading signal emitter.
1657
- * Emits only signals from live trading execution.
1658
- */
1659
- const signalLiveEmitter = new functoolsKit.Subject();
1660
- /**
1661
- * Backtest signal emitter.
1662
- * Emits only signals from backtest execution.
1895
+ * Converts markdown content to plain text with minimal formatting
1896
+ * @param content - Markdown string to convert
1897
+ * @returns Plain text representation
1663
1898
  */
1664
- const signalBacktestEmitter = new functoolsKit.Subject();
1899
+ const toPlainString = (content) => {
1900
+ if (!content) {
1901
+ return "";
1902
+ }
1903
+ let text = content;
1904
+ // Remove code blocks
1905
+ text = text.replace(/```[\s\S]*?```/g, "");
1906
+ text = text.replace(/`([^`]+)`/g, "$1");
1907
+ // Remove images
1908
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
1909
+ // Convert links to text only (keep link text, remove URL)
1910
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
1911
+ // Remove headers (convert to plain text)
1912
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
1913
+ // Remove bold and italic markers
1914
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
1915
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
1916
+ text = text.replace(/\*(.+?)\*/g, "$1");
1917
+ text = text.replace(/___(.+?)___/g, "$1");
1918
+ text = text.replace(/__(.+?)__/g, "$1");
1919
+ text = text.replace(/_(.+?)_/g, "$1");
1920
+ // Remove strikethrough
1921
+ text = text.replace(/~~(.+?)~~/g, "$1");
1922
+ // Convert lists to plain text with bullets
1923
+ text = text.replace(/^\s*[-*+]\s+/gm, "• ");
1924
+ text = text.replace(/^\s*\d+\.\s+/gm, "• ");
1925
+ // Remove blockquotes
1926
+ text = text.replace(/^\s*>\s+/gm, "");
1927
+ // Remove horizontal rules
1928
+ text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
1929
+ // Remove HTML tags
1930
+ text = text.replace(/<[^>]+>/g, "");
1931
+ // Remove excessive whitespace and normalize line breaks
1932
+ text = text.replace(/\n[\s\n]*\n/g, "\n");
1933
+ text = text.replace(/[ \t]+/g, " ");
1934
+ // Remove all newline characters
1935
+ text = text.replace(/\n/g, " ");
1936
+ // Remove excessive spaces after newline removal
1937
+ text = text.replace(/\s+/g, " ");
1938
+ return text.trim();
1939
+ };
1940
+
1941
+ const INTERVAL_MINUTES$3 = {
1942
+ "1m": 1,
1943
+ "3m": 3,
1944
+ "5m": 5,
1945
+ "15m": 15,
1946
+ "30m": 30,
1947
+ "1h": 60,
1948
+ };
1949
+ const TIMEOUT_SYMBOL = Symbol('timeout');
1665
1950
  /**
1666
- * Error emitter for background execution errors.
1667
- * Emits errors caught in background tasks (Live.background, Backtest.background).
1951
+ * Converts internal signal to public API format.
1952
+ *
1953
+ * This function is used AFTER position opens for external callbacks and API.
1954
+ * It hides internal implementation details while exposing effective values:
1955
+ *
1956
+ * - Replaces internal _trailingPriceStopLoss with effective priceStopLoss
1957
+ * - Preserves original stop-loss in originalPriceStopLoss for reference
1958
+ * - Ensures external code never sees private _trailingPriceStopLoss field
1959
+ * - Maintains backward compatibility with non-trailing positions
1960
+ *
1961
+ * Key differences from TO_RISK_SIGNAL (in ClientRisk.ts):
1962
+ * - Used AFTER position opens (vs BEFORE for risk validation)
1963
+ * - Works only with ISignalRow/IScheduledSignalRow (vs ISignalDto)
1964
+ * - No currentPrice fallback needed (priceOpen always present in opened signals)
1965
+ * - Returns IPublicSignalRow (vs IRiskSignalRow for risk checks)
1966
+ *
1967
+ * Use cases:
1968
+ * - All strategy callbacks (onOpen, onClose, onActive, etc.)
1969
+ * - External API responses (getPendingSignal, getScheduledSignal)
1970
+ * - Event emissions and logging
1971
+ * - Integration with ClientPartial and ClientRisk
1972
+ *
1973
+ * @param signal - Internal signal row with optional trailing stop-loss
1974
+ * @returns Signal in IPublicSignalRow format with effective stop-loss and hidden internals
1975
+ *
1976
+ * @example
1977
+ * ```typescript
1978
+ * // Signal without trailing SL
1979
+ * const publicSignal = TO_PUBLIC_SIGNAL(signal);
1980
+ * // publicSignal.priceStopLoss = signal.priceStopLoss
1981
+ * // publicSignal.originalPriceStopLoss = signal.priceStopLoss
1982
+ *
1983
+ * // Signal with trailing SL
1984
+ * const publicSignal = TO_PUBLIC_SIGNAL(signalWithTrailing);
1985
+ * // publicSignal.priceStopLoss = signal._trailingPriceStopLoss (effective)
1986
+ * // publicSignal.originalPriceStopLoss = signal.priceStopLoss (original)
1987
+ * // publicSignal._trailingPriceStopLoss = undefined (hidden from external API)
1988
+ * ```
1668
1989
  */
1669
- const errorEmitter = new functoolsKit.Subject();
1670
- /**
1671
- * Exit emitter for critical errors that require process termination.
1672
- * Emits errors that should terminate the current execution (Backtest, Live, Walker).
1673
- * Unlike errorEmitter (for recoverable errors), exitEmitter signals fatal errors.
1674
- */
1675
- const exitEmitter = new functoolsKit.Subject();
1676
- /**
1677
- * Done emitter for live background execution completion.
1678
- * Emits when live background tasks complete (Live.background).
1679
- */
1680
- const doneLiveSubject = new functoolsKit.Subject();
1681
- /**
1682
- * Done emitter for backtest background execution completion.
1683
- * Emits when backtest background tasks complete (Backtest.background).
1684
- */
1685
- const doneBacktestSubject = new functoolsKit.Subject();
1686
- /**
1687
- * Done emitter for walker background execution completion.
1688
- * Emits when walker background tasks complete (Walker.background).
1689
- */
1690
- const doneWalkerSubject = new functoolsKit.Subject();
1691
- /**
1692
- * Progress emitter for backtest execution progress.
1693
- * Emits progress updates during backtest execution.
1694
- */
1695
- const progressBacktestEmitter = new functoolsKit.Subject();
1696
- /**
1697
- * Progress emitter for walker execution progress.
1698
- * Emits progress updates during walker execution.
1699
- */
1700
- const progressWalkerEmitter = new functoolsKit.Subject();
1701
- /**
1702
- * Progress emitter for optimizer execution progress.
1703
- * Emits progress updates during optimizer execution.
1704
- */
1705
- const progressOptimizerEmitter = new functoolsKit.Subject();
1706
- /**
1707
- * Performance emitter for execution metrics.
1708
- * Emits performance metrics for profiling and bottleneck detection.
1709
- */
1710
- const performanceEmitter = new functoolsKit.Subject();
1711
- /**
1712
- * Walker emitter for strategy comparison progress.
1713
- * Emits progress updates during walker execution (each strategy completion).
1714
- */
1715
- const walkerEmitter = new functoolsKit.Subject();
1716
- /**
1717
- * Walker complete emitter for strategy comparison completion.
1718
- * Emits when all strategies have been tested and final results are available.
1719
- */
1720
- const walkerCompleteSubject = new functoolsKit.Subject();
1721
- /**
1722
- * Walker stop emitter for walker cancellation events.
1723
- * Emits when a walker comparison is stopped/cancelled.
1724
- *
1725
- * Includes walkerName to support multiple walkers running on the same symbol.
1726
- */
1727
- const walkerStopSubject = new functoolsKit.Subject();
1728
- /**
1729
- * Validation emitter for risk validation errors.
1730
- * Emits when risk validation functions throw errors during signal checking.
1731
- */
1732
- const validationSubject = new functoolsKit.Subject();
1733
- /**
1734
- * Partial profit emitter for profit level milestones.
1735
- * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
1736
- */
1737
- const partialProfitSubject = new functoolsKit.Subject();
1738
- /**
1739
- * Partial loss emitter for loss level milestones.
1740
- * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1741
- */
1742
- const partialLossSubject = new functoolsKit.Subject();
1743
- /**
1744
- * Risk rejection emitter for risk management violations.
1745
- * Emits ONLY when a signal is rejected due to risk validation failure.
1746
- * Does not emit for allowed signals (prevents spam).
1747
- */
1748
- const riskSubject = new functoolsKit.Subject();
1749
- /**
1750
- * Ping emitter for scheduled signal monitoring events.
1751
- * Emits every minute when a scheduled signal is being monitored (waiting for activation).
1752
- * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
1753
- */
1754
- const pingSubject = new functoolsKit.Subject();
1755
-
1756
- var emitters = /*#__PURE__*/Object.freeze({
1757
- __proto__: null,
1758
- doneBacktestSubject: doneBacktestSubject,
1759
- doneLiveSubject: doneLiveSubject,
1760
- doneWalkerSubject: doneWalkerSubject,
1761
- errorEmitter: errorEmitter,
1762
- exitEmitter: exitEmitter,
1763
- partialLossSubject: partialLossSubject,
1764
- partialProfitSubject: partialProfitSubject,
1765
- performanceEmitter: performanceEmitter,
1766
- pingSubject: pingSubject,
1767
- progressBacktestEmitter: progressBacktestEmitter,
1768
- progressOptimizerEmitter: progressOptimizerEmitter,
1769
- progressWalkerEmitter: progressWalkerEmitter,
1770
- riskSubject: riskSubject,
1771
- signalBacktestEmitter: signalBacktestEmitter,
1772
- signalEmitter: signalEmitter,
1773
- signalLiveEmitter: signalLiveEmitter,
1774
- validationSubject: validationSubject,
1775
- walkerCompleteSubject: walkerCompleteSubject,
1776
- walkerEmitter: walkerEmitter,
1777
- walkerStopSubject: walkerStopSubject
1778
- });
1779
-
1780
- /**
1781
- * Converts markdown content to plain text with minimal formatting
1782
- * @param content - Markdown string to convert
1783
- * @returns Plain text representation
1784
- */
1785
- const toPlainString = (content) => {
1786
- if (!content) {
1787
- return "";
1990
+ const TO_PUBLIC_SIGNAL = (signal) => {
1991
+ if (signal._trailingPriceStopLoss !== undefined) {
1992
+ return {
1993
+ ...signal,
1994
+ priceStopLoss: signal._trailingPriceStopLoss,
1995
+ originalPriceStopLoss: signal.priceStopLoss,
1996
+ };
1788
1997
  }
1789
- let text = content;
1790
- // Remove code blocks
1791
- text = text.replace(/```[\s\S]*?```/g, "");
1792
- text = text.replace(/`([^`]+)`/g, "$1");
1793
- // Remove images
1794
- text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
1795
- // Convert links to text only (keep link text, remove URL)
1796
- text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
1797
- // Remove headers (convert to plain text)
1798
- text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
1799
- // Remove bold and italic markers
1800
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
1801
- text = text.replace(/\*\*(.+?)\*\*/g, "$1");
1802
- text = text.replace(/\*(.+?)\*/g, "$1");
1803
- text = text.replace(/___(.+?)___/g, "$1");
1804
- text = text.replace(/__(.+?)__/g, "$1");
1805
- text = text.replace(/_(.+?)_/g, "$1");
1806
- // Remove strikethrough
1807
- text = text.replace(/~~(.+?)~~/g, "$1");
1808
- // Convert lists to plain text with bullets
1809
- text = text.replace(/^\s*[-*+]\s+/gm, "• ");
1810
- text = text.replace(/^\s*\d+\.\s+/gm, "• ");
1811
- // Remove blockquotes
1812
- text = text.replace(/^\s*>\s+/gm, "");
1813
- // Remove horizontal rules
1814
- text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
1815
- // Remove HTML tags
1816
- text = text.replace(/<[^>]+>/g, "");
1817
- // Remove excessive whitespace and normalize line breaks
1818
- text = text.replace(/\n[\s\n]*\n/g, "\n");
1819
- text = text.replace(/[ \t]+/g, " ");
1820
- // Remove all newline characters
1821
- text = text.replace(/\n/g, " ");
1822
- // Remove excessive spaces after newline removal
1823
- text = text.replace(/\s+/g, " ");
1824
- return text.trim();
1825
- };
1826
-
1827
- const INTERVAL_MINUTES$3 = {
1828
- "1m": 1,
1829
- "3m": 3,
1830
- "5m": 5,
1831
- "15m": 15,
1832
- "30m": 30,
1833
- "1h": 60,
1998
+ return {
1999
+ ...signal,
2000
+ originalPriceStopLoss: signal.priceStopLoss,
2001
+ };
1834
2002
  };
1835
- const TIMEOUT_SYMBOL = Symbol('timeout');
1836
2003
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1837
2004
  const errors = [];
1838
2005
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
1839
- if (signal.id === undefined || signal.id === null || signal.id === '') {
1840
- errors.push('id is required and must be a non-empty string');
1841
- }
1842
- if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
1843
- errors.push('exchangeName is required');
1844
- }
1845
- if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
1846
- errors.push('strategyName is required');
1847
- }
1848
- if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
1849
- errors.push('symbol is required and must be a non-empty string');
1850
- }
1851
- if (signal._isScheduled === undefined || signal._isScheduled === null) {
1852
- errors.push('_isScheduled is required');
1853
- }
1854
- if (signal.position === undefined || signal.position === null) {
1855
- errors.push('position is required and must be "long" or "short"');
1856
- }
1857
- if (signal.position !== "long" && signal.position !== "short") {
1858
- errors.push(`position must be "long" or "short", got "${signal.position}"`);
2006
+ {
2007
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
2008
+ errors.push('id is required and must be a non-empty string');
2009
+ }
2010
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
2011
+ errors.push('exchangeName is required');
2012
+ }
2013
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
2014
+ errors.push('strategyName is required');
2015
+ }
2016
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
2017
+ errors.push('symbol is required and must be a non-empty string');
2018
+ }
2019
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
2020
+ errors.push('_isScheduled is required');
2021
+ }
2022
+ if (signal.position === undefined || signal.position === null) {
2023
+ errors.push('position is required and must be "long" or "short"');
2024
+ }
2025
+ if (signal.position !== "long" && signal.position !== "short") {
2026
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
2027
+ }
1859
2028
  }
1860
2029
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1861
- if (typeof currentPrice !== "number") {
1862
- errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
1863
- }
1864
- if (!isFinite(currentPrice)) {
1865
- errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
1866
- }
1867
- if (isFinite(currentPrice) && currentPrice <= 0) {
1868
- errors.push(`currentPrice must be positive, got ${currentPrice}`);
2030
+ {
2031
+ if (typeof currentPrice !== "number") {
2032
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
2033
+ }
2034
+ if (!isFinite(currentPrice)) {
2035
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
2036
+ }
2037
+ if (isFinite(currentPrice) && currentPrice <= 0) {
2038
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
2039
+ }
1869
2040
  }
1870
2041
  // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
1871
- if (typeof signal.priceOpen !== "number") {
1872
- errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
1873
- }
1874
- if (!isFinite(signal.priceOpen)) {
1875
- errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
1876
- }
1877
- if (typeof signal.priceTakeProfit !== "number") {
1878
- errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
1879
- }
1880
- if (!isFinite(signal.priceTakeProfit)) {
1881
- errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
1882
- }
1883
- if (typeof signal.priceStopLoss !== "number") {
1884
- errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
1885
- }
1886
- if (!isFinite(signal.priceStopLoss)) {
1887
- errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2042
+ {
2043
+ if (typeof signal.priceOpen !== "number") {
2044
+ errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
2045
+ }
2046
+ if (!isFinite(signal.priceOpen)) {
2047
+ errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
2048
+ }
2049
+ if (typeof signal.priceTakeProfit !== "number") {
2050
+ errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
2051
+ }
2052
+ if (!isFinite(signal.priceTakeProfit)) {
2053
+ errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
2054
+ }
2055
+ if (typeof signal.priceStopLoss !== "number") {
2056
+ errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2057
+ }
2058
+ if (!isFinite(signal.priceStopLoss)) {
2059
+ errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
2060
+ }
1888
2061
  }
1889
2062
  // Валидация цен (только если они конечные)
1890
- if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
1891
- errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
1892
- }
1893
- if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
1894
- errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
1895
- }
1896
- if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
1897
- errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
2063
+ {
2064
+ if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
2065
+ errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
2066
+ }
2067
+ if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
2068
+ errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
2069
+ }
2070
+ if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
2071
+ errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
2072
+ }
1898
2073
  }
1899
2074
  // Валидация для long позиции
1900
2075
  if (signal.position === "long") {
1901
- if (signal.priceTakeProfit <= signal.priceOpen) {
1902
- errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
1903
- }
1904
- if (signal.priceStopLoss >= signal.priceOpen) {
1905
- errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
2076
+ // Проверка соотношения цен для long
2077
+ {
2078
+ if (signal.priceTakeProfit <= signal.priceOpen) {
2079
+ errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
2080
+ }
2081
+ if (signal.priceStopLoss >= signal.priceOpen) {
2082
+ errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
2083
+ }
1906
2084
  }
1907
2085
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
1908
- if (!isScheduled && isFinite(currentPrice)) {
1909
- // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
1910
- // SL < currentPrice < TP
1911
- if (currentPrice <= signal.priceStopLoss) {
1912
- errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
1913
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
1914
- }
1915
- if (currentPrice >= signal.priceTakeProfit) {
1916
- errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
1917
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2086
+ {
2087
+ if (!isScheduled && isFinite(currentPrice)) {
2088
+ // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
2089
+ // SL < currentPrice < TP
2090
+ if (currentPrice <= signal.priceStopLoss) {
2091
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2092
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2093
+ }
2094
+ if (currentPrice >= signal.priceTakeProfit) {
2095
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2096
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2097
+ }
1918
2098
  }
1919
2099
  }
1920
2100
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
1921
- if (isScheduled && isFinite(signal.priceOpen)) {
1922
- // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
1923
- // SL < priceOpen < TP
1924
- if (signal.priceOpen <= signal.priceStopLoss) {
1925
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
1926
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
1927
- }
1928
- if (signal.priceOpen >= signal.priceTakeProfit) {
1929
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
1930
- `Signal would close immediately on activation. This is logically impossible for LONG position.`);
2101
+ {
2102
+ if (isScheduled && isFinite(signal.priceOpen)) {
2103
+ // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
2104
+ // SL < priceOpen < TP
2105
+ if (signal.priceOpen <= signal.priceStopLoss) {
2106
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
2107
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2108
+ }
2109
+ if (signal.priceOpen >= signal.priceTakeProfit) {
2110
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
2111
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
2112
+ }
1931
2113
  }
1932
2114
  }
1933
2115
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1934
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1935
- const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
1936
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1937
- errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
1938
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
1939
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2116
+ {
2117
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2118
+ const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
2119
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2120
+ errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2121
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2122
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2123
+ }
1940
2124
  }
1941
2125
  }
1942
2126
  // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
1943
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1944
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
1945
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1946
- errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
1947
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
1948
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2127
+ {
2128
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2129
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2130
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2131
+ errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2132
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2133
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2134
+ }
1949
2135
  }
1950
2136
  }
1951
2137
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
1952
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1953
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
1954
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1955
- errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
1956
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
1957
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2138
+ {
2139
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2140
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
2141
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2142
+ errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2143
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2144
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2145
+ }
1958
2146
  }
1959
2147
  }
1960
2148
  }
1961
2149
  // Валидация для short позиции
1962
2150
  if (signal.position === "short") {
1963
- if (signal.priceTakeProfit >= signal.priceOpen) {
1964
- errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
1965
- }
1966
- if (signal.priceStopLoss <= signal.priceOpen) {
1967
- errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
2151
+ // Проверка соотношения цен для short
2152
+ {
2153
+ if (signal.priceTakeProfit >= signal.priceOpen) {
2154
+ errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
2155
+ }
2156
+ if (signal.priceStopLoss <= signal.priceOpen) {
2157
+ errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
2158
+ }
1968
2159
  }
1969
2160
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
1970
- if (!isScheduled && isFinite(currentPrice)) {
1971
- // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
1972
- // TP < currentPrice < SL
1973
- if (currentPrice >= signal.priceStopLoss) {
1974
- errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
1975
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
1976
- }
1977
- if (currentPrice <= signal.priceTakeProfit) {
1978
- errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
1979
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2161
+ {
2162
+ if (!isScheduled && isFinite(currentPrice)) {
2163
+ // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
2164
+ // TP < currentPrice < SL
2165
+ if (currentPrice >= signal.priceStopLoss) {
2166
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2167
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
2168
+ }
2169
+ if (currentPrice <= signal.priceTakeProfit) {
2170
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2171
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
2172
+ }
1980
2173
  }
1981
2174
  }
1982
2175
  // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
1983
- if (isScheduled && isFinite(signal.priceOpen)) {
1984
- // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
1985
- // TP < priceOpen < SL
1986
- if (signal.priceOpen >= signal.priceStopLoss) {
1987
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
1988
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
1989
- }
1990
- if (signal.priceOpen <= signal.priceTakeProfit) {
1991
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
1992
- `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
2176
+ {
2177
+ if (isScheduled && isFinite(signal.priceOpen)) {
2178
+ // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
2179
+ // TP < priceOpen < SL
2180
+ if (signal.priceOpen >= signal.priceStopLoss) {
2181
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
2182
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
2183
+ }
2184
+ if (signal.priceOpen <= signal.priceTakeProfit) {
2185
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
2186
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
2187
+ }
1993
2188
  }
1994
2189
  }
1995
2190
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
1996
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1997
- const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
1998
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
1999
- errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2000
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2001
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2191
+ {
2192
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2193
+ const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
2194
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
2195
+ errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
2196
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
2197
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
2198
+ }
2002
2199
  }
2003
2200
  }
2004
2201
  // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
2005
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2006
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2007
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2008
- errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2009
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2010
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2202
+ {
2203
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2204
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2205
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
2206
+ errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2207
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
2208
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2209
+ }
2011
2210
  }
2012
2211
  }
2013
2212
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
2014
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2015
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2016
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2017
- errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2018
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2019
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2213
+ {
2214
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2215
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
2216
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
2217
+ errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
2218
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
2219
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
2220
+ }
2020
2221
  }
2021
2222
  }
2022
2223
  }
2023
2224
  // Валидация временных параметров
2024
- if (typeof signal.minuteEstimatedTime !== "number") {
2025
- errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
2026
- }
2027
- if (signal.minuteEstimatedTime <= 0) {
2028
- errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
2029
- }
2030
- if (!Number.isInteger(signal.minuteEstimatedTime)) {
2031
- errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
2032
- }
2033
- if (!isFinite(signal.minuteEstimatedTime)) {
2034
- errors.push(`minuteEstimatedTime must be a finite number, got ${signal.minuteEstimatedTime}`);
2225
+ {
2226
+ if (typeof signal.minuteEstimatedTime !== "number") {
2227
+ errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
2228
+ }
2229
+ if (signal.minuteEstimatedTime <= 0) {
2230
+ errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
2231
+ }
2232
+ if (!Number.isInteger(signal.minuteEstimatedTime)) {
2233
+ errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
2234
+ }
2235
+ if (!isFinite(signal.minuteEstimatedTime)) {
2236
+ errors.push(`minuteEstimatedTime must be a finite number, got ${signal.minuteEstimatedTime}`);
2237
+ }
2035
2238
  }
2036
2239
  // ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
2037
- if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2038
- if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2039
- const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
2040
- const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
2041
- errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
2042
- `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
2043
- `Eternal signals block risk limits and prevent new trades.`);
2240
+ {
2241
+ if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2242
+ if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
2243
+ const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
2244
+ const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
2245
+ errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
2246
+ `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
2247
+ `Eternal signals block risk limits and prevent new trades.`);
2248
+ }
2044
2249
  }
2045
2250
  }
2046
- if (typeof signal.scheduledAt !== "number") {
2047
- errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
2048
- }
2049
- if (signal.scheduledAt <= 0) {
2050
- errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
2051
- }
2052
- if (typeof signal.pendingAt !== "number") {
2053
- errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
2054
- }
2055
- if (signal.pendingAt <= 0) {
2056
- errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
2251
+ // Валидация временных меток
2252
+ {
2253
+ if (typeof signal.scheduledAt !== "number") {
2254
+ errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
2255
+ }
2256
+ if (signal.scheduledAt <= 0) {
2257
+ errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
2258
+ }
2259
+ if (typeof signal.pendingAt !== "number") {
2260
+ errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
2261
+ }
2262
+ if (signal.pendingAt <= 0) {
2263
+ errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
2264
+ }
2057
2265
  }
2058
2266
  // Кидаем ошибку если есть проблемы
2059
2267
  if (errors.length > 0) {
@@ -2090,15 +2298,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
2090
2298
  if (self._isStopped) {
2091
2299
  return null;
2092
2300
  }
2093
- if (await functoolsKit.not(self.params.risk.checkSignal({
2094
- pendingSignal: signal,
2095
- symbol: self.params.execution.context.symbol,
2096
- strategyName: self.params.method.context.strategyName,
2097
- exchangeName: self.params.method.context.exchangeName,
2098
- frameName: self.params.method.context.frameName,
2099
- currentPrice,
2100
- timestamp: currentTime,
2101
- }))) {
2301
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest))) {
2102
2302
  return null;
2103
2303
  }
2104
2304
  // Если priceOpen указан - проверяем нужно ли ждать активации или открыть сразу
@@ -2207,10 +2407,9 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
2207
2407
  }
2208
2408
  self._pendingSignal = pendingSignal;
2209
2409
  // Call onActive callback for restored signal
2210
- if (self.params.callbacks?.onActive) {
2211
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2212
- self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
2213
- }
2410
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2411
+ const currentTime = self.params.execution.context.when.getTime();
2412
+ await CALL_ACTIVE_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, self.params.execution.context.backtest);
2214
2413
  }
2215
2414
  // Restore scheduled signal
2216
2415
  const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.execution.context.symbol, self.params.strategyName);
@@ -2223,10 +2422,174 @@ const WAIT_FOR_INIT_FN$2 = async (self) => {
2223
2422
  }
2224
2423
  self._scheduledSignal = scheduledSignal;
2225
2424
  // Call onSchedule callback for restored scheduled signal
2226
- if (self.params.callbacks?.onSchedule) {
2227
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2228
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
2425
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
2426
+ const currentTime = self.params.execution.context.when.getTime();
2427
+ await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduledSignal, currentPrice, currentTime, self.params.execution.context.backtest);
2428
+ }
2429
+ };
2430
+ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
2431
+ // Initialize partial array if not present
2432
+ if (!signal._partial)
2433
+ signal._partial = [];
2434
+ // Calculate current totals (computed values)
2435
+ const tpClosed = signal._partial
2436
+ .filter((p) => p.type === "profit")
2437
+ .reduce((sum, p) => sum + p.percent, 0);
2438
+ const slClosed = signal._partial
2439
+ .filter((p) => p.type === "loss")
2440
+ .reduce((sum, p) => sum + p.percent, 0);
2441
+ const totalClosed = tpClosed + slClosed;
2442
+ // Check if would exceed 100% total closed
2443
+ const newTotalClosed = totalClosed + percentToClose;
2444
+ if (newTotalClosed > 100) {
2445
+ self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
2446
+ signalId: signal.id,
2447
+ currentTotalClosed: totalClosed,
2448
+ percentToClose,
2449
+ newTotalClosed,
2450
+ });
2451
+ return;
2452
+ }
2453
+ // Add new partial close entry
2454
+ signal._partial.push({
2455
+ type: "profit",
2456
+ percent: percentToClose,
2457
+ price: currentPrice,
2458
+ });
2459
+ self.params.logger.info("PARTIAL_PROFIT_FN executed", {
2460
+ signalId: signal.id,
2461
+ percentClosed: percentToClose,
2462
+ totalClosed: newTotalClosed,
2463
+ currentPrice,
2464
+ tpClosed: tpClosed + percentToClose,
2465
+ });
2466
+ };
2467
+ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2468
+ // Initialize partial array if not present
2469
+ if (!signal._partial)
2470
+ signal._partial = [];
2471
+ // Calculate current totals (computed values)
2472
+ const tpClosed = signal._partial
2473
+ .filter((p) => p.type === "profit")
2474
+ .reduce((sum, p) => sum + p.percent, 0);
2475
+ const slClosed = signal._partial
2476
+ .filter((p) => p.type === "loss")
2477
+ .reduce((sum, p) => sum + p.percent, 0);
2478
+ const totalClosed = tpClosed + slClosed;
2479
+ // Check if would exceed 100% total closed
2480
+ const newTotalClosed = totalClosed + percentToClose;
2481
+ if (newTotalClosed > 100) {
2482
+ self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
2483
+ signalId: signal.id,
2484
+ currentTotalClosed: totalClosed,
2485
+ percentToClose,
2486
+ newTotalClosed,
2487
+ });
2488
+ return;
2489
+ }
2490
+ // Add new partial close entry
2491
+ signal._partial.push({
2492
+ type: "loss",
2493
+ percent: percentToClose,
2494
+ price: currentPrice,
2495
+ });
2496
+ self.params.logger.warn("PARTIAL_LOSS_FN executed", {
2497
+ signalId: signal.id,
2498
+ percentClosed: percentToClose,
2499
+ totalClosed: newTotalClosed,
2500
+ currentPrice,
2501
+ slClosed: slClosed + percentToClose,
2502
+ });
2503
+ };
2504
+ const TRAILING_STOP_FN = (self, signal, percentShift) => {
2505
+ // Calculate distance between entry and original stop-loss AS PERCENTAGE of entry price
2506
+ const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
2507
+ // Calculate new stop-loss distance percentage by adding shift
2508
+ // Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
2509
+ // Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
2510
+ const newSlDistancePercent = slDistancePercent + percentShift;
2511
+ // Calculate new stop-loss price based on new distance percentage
2512
+ // Negative newSlDistancePercent means SL crosses entry into profit zone
2513
+ let newStopLoss;
2514
+ if (signal.position === "long") {
2515
+ // LONG: SL is below entry (or above entry if in profit zone)
2516
+ // Formula: entry * (1 - newDistance%)
2517
+ // Example: entry=100, originalSL=90 (10%), shift=-15% → newDistance=-5% → 100 * 1.05 = 105 (profit zone)
2518
+ // Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
2519
+ // Example: entry=100, originalSL=90 (10%), shift=+5% → newDistance=15% → 100 * 0.85 = 85 (looser)
2520
+ newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
2521
+ }
2522
+ else {
2523
+ // SHORT: SL is above entry (or below entry if in profit zone)
2524
+ // Formula: entry * (1 + newDistance%)
2525
+ // Example: entry=100, originalSL=110 (10%), shift=-15% → newDistance=-5% → 100 * 0.95 = 95 (profit zone)
2526
+ // Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
2527
+ // Example: entry=100, originalSL=110 (10%), shift=+5% → newDistance=15% → 100 * 1.15 = 115 (looser)
2528
+ newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
2529
+ }
2530
+ // Get current effective stop-loss (trailing or original)
2531
+ const currentStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
2532
+ // Determine if this is the first trailing stop call (direction not set yet)
2533
+ const isFirstCall = signal._trailingPriceStopLoss === undefined;
2534
+ if (isFirstCall) {
2535
+ // First call: set the direction and update SL unconditionally
2536
+ signal._trailingPriceStopLoss = newStopLoss;
2537
+ self.params.logger.info("TRAILING_STOP_FN executed (first call - direction set)", {
2538
+ signalId: signal.id,
2539
+ position: signal.position,
2540
+ priceOpen: signal.priceOpen,
2541
+ originalStopLoss: signal.priceStopLoss,
2542
+ originalDistancePercent: slDistancePercent,
2543
+ previousStopLoss: currentStopLoss,
2544
+ newStopLoss,
2545
+ newDistancePercent: newSlDistancePercent,
2546
+ percentShift,
2547
+ inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
2548
+ direction: newStopLoss > currentStopLoss ? "up" : "down",
2549
+ });
2550
+ }
2551
+ else {
2552
+ // Subsequent calls: only update if new SL continues in the same direction
2553
+ const movingUp = newStopLoss > currentStopLoss;
2554
+ const movingDown = newStopLoss < currentStopLoss;
2555
+ // Determine initial direction based on first trailing SL vs original SL
2556
+ const initialDirection = signal._trailingPriceStopLoss > signal.priceStopLoss ? "up" : "down";
2557
+ let shouldUpdate = false;
2558
+ if (initialDirection === "up" && movingUp) {
2559
+ // Direction is UP, and new SL continues moving up
2560
+ shouldUpdate = true;
2561
+ }
2562
+ else if (initialDirection === "down" && movingDown) {
2563
+ // Direction is DOWN, and new SL continues moving down
2564
+ shouldUpdate = true;
2229
2565
  }
2566
+ if (!shouldUpdate) {
2567
+ self.params.logger.debug("TRAILING_STOP_FN: new SL not in same direction, skipping", {
2568
+ signalId: signal.id,
2569
+ position: signal.position,
2570
+ currentStopLoss,
2571
+ newStopLoss,
2572
+ percentShift,
2573
+ initialDirection,
2574
+ attemptedDirection: movingUp ? "up" : movingDown ? "down" : "same",
2575
+ });
2576
+ return;
2577
+ }
2578
+ // Update trailing stop-loss
2579
+ signal._trailingPriceStopLoss = newStopLoss;
2580
+ self.params.logger.info("TRAILING_STOP_FN executed", {
2581
+ signalId: signal.id,
2582
+ position: signal.position,
2583
+ priceOpen: signal.priceOpen,
2584
+ originalStopLoss: signal.priceStopLoss,
2585
+ originalDistancePercent: slDistancePercent,
2586
+ previousStopLoss: currentStopLoss,
2587
+ newStopLoss,
2588
+ newDistancePercent: newSlDistancePercent,
2589
+ percentShift,
2590
+ inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
2591
+ direction: initialDirection,
2592
+ });
2230
2593
  }
2231
2594
  };
2232
2595
  const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
@@ -2244,12 +2607,10 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
2244
2607
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
2245
2608
  });
2246
2609
  await self.setScheduledSignal(null);
2247
- if (self.params.callbacks?.onCancel) {
2248
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
2249
- }
2610
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2250
2611
  const result = {
2251
2612
  action: "cancelled",
2252
- signal: scheduled,
2613
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2253
2614
  currentPrice: currentPrice,
2254
2615
  closeTimestamp: currentTime,
2255
2616
  strategyName: self.params.method.context.strategyName,
@@ -2259,9 +2620,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
2259
2620
  backtest: self.params.execution.context.backtest,
2260
2621
  reason: "timeout",
2261
2622
  };
2262
- if (self.params.callbacks?.onTick) {
2263
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2264
- }
2623
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2265
2624
  return result;
2266
2625
  };
2267
2626
  const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
@@ -2302,14 +2661,13 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
2302
2661
  priceStopLoss: scheduled.priceStopLoss,
2303
2662
  });
2304
2663
  await self.setScheduledSignal(null);
2305
- if (self.params.callbacks?.onCancel) {
2306
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
2307
- }
2664
+ const currentTime = self.params.execution.context.when.getTime();
2665
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
2308
2666
  const result = {
2309
2667
  action: "cancelled",
2310
- signal: scheduled,
2668
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2311
2669
  currentPrice: currentPrice,
2312
- closeTimestamp: self.params.execution.context.when.getTime(),
2670
+ closeTimestamp: currentTime,
2313
2671
  strategyName: self.params.method.context.strategyName,
2314
2672
  exchangeName: self.params.method.context.exchangeName,
2315
2673
  frameName: self.params.method.context.frameName,
@@ -2317,9 +2675,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
2317
2675
  backtest: self.params.execution.context.backtest,
2318
2676
  reason: "price_reject",
2319
2677
  };
2320
- if (self.params.callbacks?.onTick) {
2321
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2322
- }
2678
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2323
2679
  return result;
2324
2680
  };
2325
2681
  const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
@@ -2346,15 +2702,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2346
2702
  scheduledAt: scheduled.scheduledAt,
2347
2703
  pendingAt: activationTime,
2348
2704
  });
2349
- if (await functoolsKit.not(self.params.risk.checkSignal({
2350
- symbol: self.params.execution.context.symbol,
2351
- pendingSignal: scheduled,
2352
- strategyName: self.params.method.context.strategyName,
2353
- exchangeName: self.params.method.context.exchangeName,
2354
- frameName: self.params.method.context.frameName,
2355
- currentPrice: scheduled.priceOpen,
2356
- timestamp: activationTime,
2357
- }))) {
2705
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
2358
2706
  self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
2359
2707
  symbol: self.params.execution.context.symbol,
2360
2708
  signalId: scheduled.id,
@@ -2370,18 +2718,11 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2370
2718
  _isScheduled: false,
2371
2719
  };
2372
2720
  await self.setPendingSignal(activatedSignal);
2373
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2374
- strategyName: self.params.method.context.strategyName,
2375
- riskName: self.params.riskName,
2376
- exchangeName: self.params.method.context.exchangeName,
2377
- frameName: self.params.method.context.frameName,
2378
- });
2379
- if (self.params.callbacks?.onOpen) {
2380
- self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
2381
- }
2721
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
2722
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
2382
2723
  const result = {
2383
2724
  action: "opened",
2384
- signal: self._pendingSignal,
2725
+ signal: TO_PUBLIC_SIGNAL(self._pendingSignal),
2385
2726
  strategyName: self.params.method.context.strategyName,
2386
2727
  exchangeName: self.params.method.context.exchangeName,
2387
2728
  frameName: self.params.method.context.frameName,
@@ -2389,18 +2730,23 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
2389
2730
  currentPrice: self._pendingSignal.priceOpen,
2390
2731
  backtest: self.params.execution.context.backtest,
2391
2732
  };
2392
- if (self.params.callbacks?.onTick) {
2393
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2394
- }
2733
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, activationTime, self.params.execution.context.backtest);
2395
2734
  return result;
2396
2735
  };
2397
- const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, timestamp) => {
2398
- // Call system onPing callback first (emits to pingSubject)
2399
- 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);
2400
- // Call user onPing callback only if signal is still active (not cancelled, not activated)
2401
- if (self.params.callbacks?.onPing) {
2402
- await self.params.callbacks.onPing(self.params.execution.context.symbol, scheduled, new Date(timestamp), self.params.execution.context.backtest);
2403
- }
2736
+ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, scheduled, timestamp, backtest) => {
2737
+ await ExecutionContextService.runInContext(async () => {
2738
+ const publicSignal = TO_PUBLIC_SIGNAL(scheduled);
2739
+ // Call system onPing callback first (emits to pingSubject)
2740
+ await self.params.onPing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
2741
+ // Call user onPing callback only if signal is still active (not cancelled, not activated)
2742
+ if (self.params.callbacks?.onPing) {
2743
+ await self.params.callbacks.onPing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
2744
+ }
2745
+ }, {
2746
+ when: new Date(timestamp),
2747
+ symbol: symbol,
2748
+ backtest: backtest,
2749
+ });
2404
2750
  }, {
2405
2751
  fallback: (error) => {
2406
2752
  const message = "ClientStrategy CALL_PING_CALLBACKS_FN thrown";
@@ -2413,11 +2759,319 @@ const CALL_PING_CALLBACKS_FN = functoolsKit.trycatch(async (self, scheduled, tim
2413
2759
  errorEmitter.next(error);
2414
2760
  },
2415
2761
  });
2762
+ const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2763
+ await ExecutionContextService.runInContext(async () => {
2764
+ if (self.params.callbacks?.onActive) {
2765
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2766
+ await self.params.callbacks.onActive(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2767
+ }
2768
+ }, {
2769
+ when: new Date(timestamp),
2770
+ symbol: symbol,
2771
+ backtest: backtest,
2772
+ });
2773
+ }, {
2774
+ fallback: (error) => {
2775
+ const message = "ClientStrategy CALL_ACTIVE_CALLBACKS_FN thrown";
2776
+ const payload = {
2777
+ error: functoolsKit.errorData(error),
2778
+ message: functoolsKit.getErrorMessage(error),
2779
+ };
2780
+ backtest$1.loggerService.warn(message, payload);
2781
+ console.warn(message, payload);
2782
+ errorEmitter.next(error);
2783
+ },
2784
+ });
2785
+ const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2786
+ await ExecutionContextService.runInContext(async () => {
2787
+ if (self.params.callbacks?.onSchedule) {
2788
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2789
+ await self.params.callbacks.onSchedule(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2790
+ }
2791
+ }, {
2792
+ when: new Date(timestamp),
2793
+ symbol: symbol,
2794
+ backtest: backtest,
2795
+ });
2796
+ }, {
2797
+ fallback: (error) => {
2798
+ const message = "ClientStrategy CALL_SCHEDULE_CALLBACKS_FN thrown";
2799
+ const payload = {
2800
+ error: functoolsKit.errorData(error),
2801
+ message: functoolsKit.getErrorMessage(error),
2802
+ };
2803
+ backtest$1.loggerService.warn(message, payload);
2804
+ console.warn(message, payload);
2805
+ errorEmitter.next(error);
2806
+ },
2807
+ });
2808
+ const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2809
+ await ExecutionContextService.runInContext(async () => {
2810
+ if (self.params.callbacks?.onCancel) {
2811
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2812
+ await self.params.callbacks.onCancel(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2813
+ }
2814
+ }, {
2815
+ when: new Date(timestamp),
2816
+ symbol: symbol,
2817
+ backtest: backtest,
2818
+ });
2819
+ }, {
2820
+ fallback: (error) => {
2821
+ const message = "ClientStrategy CALL_CANCEL_CALLBACKS_FN thrown";
2822
+ const payload = {
2823
+ error: functoolsKit.errorData(error),
2824
+ message: functoolsKit.getErrorMessage(error),
2825
+ };
2826
+ backtest$1.loggerService.warn(message, payload);
2827
+ console.warn(message, payload);
2828
+ errorEmitter.next(error);
2829
+ },
2830
+ });
2831
+ const CALL_OPEN_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, priceOpen, timestamp, backtest) => {
2832
+ await ExecutionContextService.runInContext(async () => {
2833
+ if (self.params.callbacks?.onOpen) {
2834
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2835
+ await self.params.callbacks.onOpen(self.params.execution.context.symbol, publicSignal, priceOpen, self.params.execution.context.backtest);
2836
+ }
2837
+ }, {
2838
+ when: new Date(timestamp),
2839
+ symbol: symbol,
2840
+ backtest: backtest,
2841
+ });
2842
+ }, {
2843
+ fallback: (error) => {
2844
+ const message = "ClientStrategy CALL_OPEN_CALLBACKS_FN thrown";
2845
+ const payload = {
2846
+ error: functoolsKit.errorData(error),
2847
+ message: functoolsKit.getErrorMessage(error),
2848
+ };
2849
+ backtest$1.loggerService.warn(message, payload);
2850
+ console.warn(message, payload);
2851
+ errorEmitter.next(error);
2852
+ },
2853
+ });
2854
+ const CALL_CLOSE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2855
+ await ExecutionContextService.runInContext(async () => {
2856
+ if (self.params.callbacks?.onClose) {
2857
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2858
+ await self.params.callbacks.onClose(self.params.execution.context.symbol, publicSignal, currentPrice, self.params.execution.context.backtest);
2859
+ }
2860
+ }, {
2861
+ when: new Date(timestamp),
2862
+ symbol: symbol,
2863
+ backtest: backtest,
2864
+ });
2865
+ }, {
2866
+ fallback: (error) => {
2867
+ const message = "ClientStrategy CALL_CLOSE_CALLBACKS_FN thrown";
2868
+ const payload = {
2869
+ error: functoolsKit.errorData(error),
2870
+ message: functoolsKit.getErrorMessage(error),
2871
+ };
2872
+ backtest$1.loggerService.warn(message, payload);
2873
+ console.warn(message, payload);
2874
+ errorEmitter.next(error);
2875
+ },
2876
+ });
2877
+ const CALL_TICK_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, result, timestamp, backtest) => {
2878
+ await ExecutionContextService.runInContext(async () => {
2879
+ if (self.params.callbacks?.onTick) {
2880
+ await self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2881
+ }
2882
+ }, {
2883
+ when: new Date(timestamp),
2884
+ symbol: symbol,
2885
+ backtest: backtest,
2886
+ });
2887
+ }, {
2888
+ fallback: (error) => {
2889
+ const message = "ClientStrategy CALL_TICK_CALLBACKS_FN thrown";
2890
+ const payload = {
2891
+ error: functoolsKit.errorData(error),
2892
+ message: functoolsKit.getErrorMessage(error),
2893
+ };
2894
+ backtest$1.loggerService.warn(message, payload);
2895
+ console.warn(message, payload);
2896
+ errorEmitter.next(error);
2897
+ },
2898
+ });
2899
+ const CALL_IDLE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, currentPrice, timestamp, backtest) => {
2900
+ await ExecutionContextService.runInContext(async () => {
2901
+ if (self.params.callbacks?.onIdle) {
2902
+ await self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2903
+ }
2904
+ }, {
2905
+ when: new Date(timestamp),
2906
+ symbol: symbol,
2907
+ backtest: backtest,
2908
+ });
2909
+ }, {
2910
+ fallback: (error) => {
2911
+ const message = "ClientStrategy CALL_IDLE_CALLBACKS_FN thrown";
2912
+ const payload = {
2913
+ error: functoolsKit.errorData(error),
2914
+ message: functoolsKit.getErrorMessage(error),
2915
+ };
2916
+ backtest$1.loggerService.warn(message, payload);
2917
+ console.warn(message, payload);
2918
+ errorEmitter.next(error);
2919
+ },
2920
+ });
2921
+ const CALL_RISK_ADD_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
2922
+ await ExecutionContextService.runInContext(async () => {
2923
+ await self.params.risk.addSignal(symbol, {
2924
+ strategyName: self.params.method.context.strategyName,
2925
+ riskName: self.params.riskName,
2926
+ exchangeName: self.params.method.context.exchangeName,
2927
+ frameName: self.params.method.context.frameName,
2928
+ });
2929
+ }, {
2930
+ when: new Date(timestamp),
2931
+ symbol: symbol,
2932
+ backtest: backtest,
2933
+ });
2934
+ }, {
2935
+ fallback: (error) => {
2936
+ const message = "ClientStrategy CALL_RISK_ADD_SIGNAL_FN thrown";
2937
+ const payload = {
2938
+ error: functoolsKit.errorData(error),
2939
+ message: functoolsKit.getErrorMessage(error),
2940
+ };
2941
+ backtest$1.loggerService.warn(message, payload);
2942
+ console.warn(message, payload);
2943
+ errorEmitter.next(error);
2944
+ },
2945
+ });
2946
+ const CALL_RISK_REMOVE_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
2947
+ await ExecutionContextService.runInContext(async () => {
2948
+ await self.params.risk.removeSignal(symbol, {
2949
+ strategyName: self.params.method.context.strategyName,
2950
+ riskName: self.params.riskName,
2951
+ exchangeName: self.params.method.context.exchangeName,
2952
+ frameName: self.params.method.context.frameName,
2953
+ });
2954
+ }, {
2955
+ when: new Date(timestamp),
2956
+ symbol: symbol,
2957
+ backtest: backtest,
2958
+ });
2959
+ }, {
2960
+ fallback: (error) => {
2961
+ const message = "ClientStrategy CALL_RISK_REMOVE_SIGNAL_FN thrown";
2962
+ const payload = {
2963
+ error: functoolsKit.errorData(error),
2964
+ message: functoolsKit.getErrorMessage(error),
2965
+ };
2966
+ backtest$1.loggerService.warn(message, payload);
2967
+ console.warn(message, payload);
2968
+ errorEmitter.next(error);
2969
+ },
2970
+ });
2971
+ const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
2972
+ await ExecutionContextService.runInContext(async () => {
2973
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
2974
+ await self.params.partial.clear(symbol, publicSignal, currentPrice, backtest);
2975
+ }, {
2976
+ when: new Date(timestamp),
2977
+ symbol: symbol,
2978
+ backtest: backtest,
2979
+ });
2980
+ }, {
2981
+ fallback: (error) => {
2982
+ const message = "ClientStrategy CALL_PARTIAL_CLEAR_FN thrown";
2983
+ const payload = {
2984
+ error: functoolsKit.errorData(error),
2985
+ message: functoolsKit.getErrorMessage(error),
2986
+ };
2987
+ backtest$1.loggerService.warn(message, payload);
2988
+ console.warn(message, payload);
2989
+ errorEmitter.next(error);
2990
+ },
2991
+ });
2992
+ const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
2993
+ return await ExecutionContextService.runInContext(async () => {
2994
+ return await self.params.risk.checkSignal({
2995
+ pendingSignal,
2996
+ symbol: symbol,
2997
+ strategyName: self.params.method.context.strategyName,
2998
+ exchangeName: self.params.method.context.exchangeName,
2999
+ frameName: self.params.method.context.frameName,
3000
+ currentPrice,
3001
+ timestamp,
3002
+ });
3003
+ }, {
3004
+ when: new Date(timestamp),
3005
+ symbol: symbol,
3006
+ backtest: backtest,
3007
+ });
3008
+ }, {
3009
+ defaultValue: false,
3010
+ fallback: (error) => {
3011
+ const message = "ClientStrategy CALL_RISK_CHECK_SIGNAL_FN thrown";
3012
+ const payload = {
3013
+ error: functoolsKit.errorData(error),
3014
+ message: functoolsKit.getErrorMessage(error),
3015
+ };
3016
+ backtest$1.loggerService.warn(message, payload);
3017
+ console.warn(message, payload);
3018
+ errorEmitter.next(error);
3019
+ },
3020
+ });
3021
+ const CALL_PARTIAL_PROFIT_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentTp, timestamp, backtest) => {
3022
+ await ExecutionContextService.runInContext(async () => {
3023
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
3024
+ await self.params.partial.profit(symbol, publicSignal, currentPrice, percentTp, backtest, new Date(timestamp));
3025
+ if (self.params.callbacks?.onPartialProfit) {
3026
+ await self.params.callbacks.onPartialProfit(symbol, publicSignal, currentPrice, percentTp, backtest);
3027
+ }
3028
+ }, {
3029
+ when: new Date(timestamp),
3030
+ symbol: symbol,
3031
+ backtest: backtest,
3032
+ });
3033
+ }, {
3034
+ fallback: (error) => {
3035
+ const message = "ClientStrategy CALL_PARTIAL_PROFIT_CALLBACKS_FN thrown";
3036
+ const payload = {
3037
+ error: functoolsKit.errorData(error),
3038
+ message: functoolsKit.getErrorMessage(error),
3039
+ };
3040
+ backtest$1.loggerService.warn(message, payload);
3041
+ console.warn(message, payload);
3042
+ errorEmitter.next(error);
3043
+ },
3044
+ });
3045
+ const CALL_PARTIAL_LOSS_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, signal, currentPrice, percentSl, timestamp, backtest) => {
3046
+ await ExecutionContextService.runInContext(async () => {
3047
+ const publicSignal = TO_PUBLIC_SIGNAL(signal);
3048
+ await self.params.partial.loss(symbol, publicSignal, currentPrice, percentSl, backtest, new Date(timestamp));
3049
+ if (self.params.callbacks?.onPartialLoss) {
3050
+ await self.params.callbacks.onPartialLoss(symbol, publicSignal, currentPrice, percentSl, backtest);
3051
+ }
3052
+ }, {
3053
+ when: new Date(timestamp),
3054
+ symbol: symbol,
3055
+ backtest: backtest,
3056
+ });
3057
+ }, {
3058
+ fallback: (error) => {
3059
+ const message = "ClientStrategy CALL_PARTIAL_LOSS_CALLBACKS_FN thrown";
3060
+ const payload = {
3061
+ error: functoolsKit.errorData(error),
3062
+ message: functoolsKit.getErrorMessage(error),
3063
+ };
3064
+ backtest$1.loggerService.warn(message, payload);
3065
+ console.warn(message, payload);
3066
+ errorEmitter.next(error);
3067
+ },
3068
+ });
2416
3069
  const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
2417
- await CALL_PING_CALLBACKS_FN(self, scheduled, self.params.execution.context.when.getTime());
3070
+ const currentTime = self.params.execution.context.when.getTime();
3071
+ await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
2418
3072
  const result = {
2419
3073
  action: "active",
2420
- signal: scheduled,
3074
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2421
3075
  currentPrice: currentPrice,
2422
3076
  strategyName: self.params.method.context.strategyName,
2423
3077
  exchangeName: self.params.method.context.exchangeName,
@@ -2427,13 +3081,12 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2427
3081
  percentSl: 0,
2428
3082
  backtest: self.params.execution.context.backtest,
2429
3083
  };
2430
- if (self.params.callbacks?.onTick) {
2431
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2432
- }
3084
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2433
3085
  return result;
2434
3086
  };
2435
3087
  const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2436
3088
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
3089
+ const currentTime = self.params.execution.context.when.getTime();
2437
3090
  self.params.logger.info("ClientStrategy scheduled signal created", {
2438
3091
  symbol: self.params.execution.context.symbol,
2439
3092
  signalId: signal.id,
@@ -2441,12 +3094,10 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2441
3094
  priceOpen: signal.priceOpen,
2442
3095
  currentPrice: currentPrice,
2443
3096
  });
2444
- if (self.params.callbacks?.onSchedule) {
2445
- self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2446
- }
3097
+ await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2447
3098
  const result = {
2448
3099
  action: "scheduled",
2449
- signal: signal,
3100
+ signal: TO_PUBLIC_SIGNAL(signal),
2450
3101
  strategyName: self.params.method.context.strategyName,
2451
3102
  exchangeName: self.params.method.context.exchangeName,
2452
3103
  frameName: self.params.method.context.frameName,
@@ -2454,35 +3105,19 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
2454
3105
  currentPrice: currentPrice,
2455
3106
  backtest: self.params.execution.context.backtest,
2456
3107
  };
2457
- if (self.params.callbacks?.onTick) {
2458
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2459
- }
3108
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2460
3109
  return result;
2461
3110
  };
2462
3111
  const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2463
- if (await functoolsKit.not(self.params.risk.checkSignal({
2464
- pendingSignal: signal,
2465
- symbol: self.params.execution.context.symbol,
2466
- strategyName: self.params.method.context.strategyName,
2467
- exchangeName: self.params.method.context.exchangeName,
2468
- frameName: self.params.method.context.frameName,
2469
- currentPrice: signal.priceOpen,
2470
- timestamp: self.params.execution.context.when.getTime(),
2471
- }))) {
3112
+ const currentTime = self.params.execution.context.when.getTime();
3113
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest))) {
2472
3114
  return null;
2473
3115
  }
2474
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2475
- strategyName: self.params.method.context.strategyName,
2476
- riskName: self.params.riskName,
2477
- exchangeName: self.params.method.context.exchangeName,
2478
- frameName: self.params.method.context.frameName,
2479
- });
2480
- if (self.params.callbacks?.onOpen) {
2481
- self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
2482
- }
3116
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
3117
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
2483
3118
  const result = {
2484
3119
  action: "opened",
2485
- signal: signal,
3120
+ signal: TO_PUBLIC_SIGNAL(signal),
2486
3121
  strategyName: self.params.method.context.strategyName,
2487
3122
  exchangeName: self.params.method.context.exchangeName,
2488
3123
  frameName: self.params.method.context.frameName,
@@ -2490,9 +3125,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
2490
3125
  currentPrice: signal.priceOpen,
2491
3126
  backtest: self.params.execution.context.backtest,
2492
3127
  };
2493
- if (self.params.callbacks?.onTick) {
2494
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2495
- }
3128
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2496
3129
  return result;
2497
3130
  };
2498
3131
  const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
@@ -2513,19 +3146,21 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
2513
3146
  return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
2514
3147
  "take_profit");
2515
3148
  }
2516
- // Check stop loss
2517
- if (signal.position === "long" && averagePrice <= signal.priceStopLoss) {
2518
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
3149
+ // Check stop loss (use trailing SL if set, otherwise original SL)
3150
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3151
+ if (signal.position === "long" && averagePrice <= effectiveStopLoss) {
3152
+ return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveStopLoss, // КРИТИЧНО: используем точную цену SL (trailing or original)
2519
3153
  "stop_loss");
2520
3154
  }
2521
- if (signal.position === "short" && averagePrice >= signal.priceStopLoss) {
2522
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
3155
+ if (signal.position === "short" && averagePrice >= effectiveStopLoss) {
3156
+ return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveStopLoss, // КРИТИЧНО: используем точную цену SL (trailing or original)
2523
3157
  "stop_loss");
2524
3158
  }
2525
3159
  return null;
2526
3160
  };
2527
3161
  const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
2528
3162
  const pnl = toProfitLossDto(signal, currentPrice);
3163
+ const currentTime = self.params.execution.context.when.getTime();
2529
3164
  self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
2530
3165
  symbol: self.params.execution.context.symbol,
2531
3166
  signalId: signal.id,
@@ -2533,24 +3168,17 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2533
3168
  priceClose: currentPrice,
2534
3169
  pnlPercentage: pnl.pnlPercentage,
2535
3170
  });
2536
- if (self.params.callbacks?.onClose) {
2537
- self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2538
- }
3171
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
2539
3172
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2540
- await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2541
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2542
- strategyName: self.params.method.context.strategyName,
2543
- riskName: self.params.riskName,
2544
- exchangeName: self.params.method.context.exchangeName,
2545
- frameName: self.params.method.context.frameName,
2546
- });
3173
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
3174
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
2547
3175
  await self.setPendingSignal(null);
2548
3176
  const result = {
2549
3177
  action: "closed",
2550
- signal: signal,
3178
+ signal: TO_PUBLIC_SIGNAL(signal),
2551
3179
  currentPrice: currentPrice,
2552
3180
  closeReason: closeReason,
2553
- closeTimestamp: self.params.execution.context.when.getTime(),
3181
+ closeTimestamp: currentTime,
2554
3182
  pnl: pnl,
2555
3183
  strategyName: self.params.method.context.strategyName,
2556
3184
  exchangeName: self.params.method.context.exchangeName,
@@ -2558,14 +3186,13 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2558
3186
  symbol: self.params.execution.context.symbol,
2559
3187
  backtest: self.params.execution.context.backtest,
2560
3188
  };
2561
- if (self.params.callbacks?.onTick) {
2562
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2563
- }
3189
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2564
3190
  return result;
2565
3191
  };
2566
3192
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2567
3193
  let percentTp = 0;
2568
3194
  let percentSl = 0;
3195
+ const currentTime = self.params.execution.context.when.getTime();
2569
3196
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2570
3197
  {
2571
3198
  if (signal.position === "long") {
@@ -2576,20 +3203,15 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2576
3203
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2577
3204
  const progressPercent = (currentDistance / tpDistance) * 100;
2578
3205
  percentTp = Math.min(progressPercent, 100);
2579
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2580
- if (self.params.callbacks?.onPartialProfit) {
2581
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2582
- }
3206
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
2583
3207
  }
2584
3208
  else if (currentDistance < 0) {
2585
- // Moving towards SL
2586
- const slDistance = signal.priceOpen - signal.priceStopLoss;
3209
+ // Moving towards SL (use trailing SL if set)
3210
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3211
+ const slDistance = signal.priceOpen - effectiveStopLoss;
2587
3212
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2588
3213
  percentSl = Math.min(progressPercent, 100);
2589
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2590
- if (self.params.callbacks?.onPartialLoss) {
2591
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2592
- }
3214
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
2593
3215
  }
2594
3216
  }
2595
3217
  else if (signal.position === "short") {
@@ -2600,26 +3222,21 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2600
3222
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2601
3223
  const progressPercent = (currentDistance / tpDistance) * 100;
2602
3224
  percentTp = Math.min(progressPercent, 100);
2603
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2604
- if (self.params.callbacks?.onPartialProfit) {
2605
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2606
- }
3225
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
2607
3226
  }
2608
3227
  if (currentDistance < 0) {
2609
- // Moving towards SL
2610
- const slDistance = signal.priceStopLoss - signal.priceOpen;
3228
+ // Moving towards SL (use trailing SL if set)
3229
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3230
+ const slDistance = effectiveStopLoss - signal.priceOpen;
2611
3231
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2612
3232
  percentSl = Math.min(progressPercent, 100);
2613
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2614
- if (self.params.callbacks?.onPartialLoss) {
2615
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2616
- }
3233
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
2617
3234
  }
2618
3235
  }
2619
3236
  }
2620
3237
  const result = {
2621
3238
  action: "active",
2622
- signal: signal,
3239
+ signal: TO_PUBLIC_SIGNAL(signal),
2623
3240
  currentPrice: currentPrice,
2624
3241
  strategyName: self.params.method.context.strategyName,
2625
3242
  exchangeName: self.params.method.context.exchangeName,
@@ -2629,15 +3246,12 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2629
3246
  percentSl,
2630
3247
  backtest: self.params.execution.context.backtest,
2631
3248
  };
2632
- if (self.params.callbacks?.onTick) {
2633
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2634
- }
3249
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2635
3250
  return result;
2636
3251
  };
2637
3252
  const RETURN_IDLE_FN = async (self, currentPrice) => {
2638
- if (self.params.callbacks?.onIdle) {
2639
- self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
2640
- }
3253
+ const currentTime = self.params.execution.context.when.getTime();
3254
+ await CALL_IDLE_CALLBACKS_FN(self, self.params.execution.context.symbol, currentPrice, currentTime, self.params.execution.context.backtest);
2641
3255
  const result = {
2642
3256
  action: "idle",
2643
3257
  signal: null,
@@ -2648,9 +3262,7 @@ const RETURN_IDLE_FN = async (self, currentPrice) => {
2648
3262
  currentPrice: currentPrice,
2649
3263
  backtest: self.params.execution.context.backtest,
2650
3264
  };
2651
- if (self.params.callbacks?.onTick) {
2652
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2653
- }
3265
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
2654
3266
  return result;
2655
3267
  };
2656
3268
  const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp, reason) => {
@@ -2663,12 +3275,10 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2663
3275
  reason,
2664
3276
  });
2665
3277
  await self.setScheduledSignal(null);
2666
- if (self.params.callbacks?.onCancel) {
2667
- self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
2668
- }
3278
+ await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, averagePrice, closeTimestamp, self.params.execution.context.backtest);
2669
3279
  const result = {
2670
3280
  action: "cancelled",
2671
- signal: scheduled,
3281
+ signal: TO_PUBLIC_SIGNAL(scheduled),
2672
3282
  currentPrice: averagePrice,
2673
3283
  closeTimestamp: closeTimestamp,
2674
3284
  strategyName: self.params.method.context.strategyName,
@@ -2678,9 +3288,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2678
3288
  backtest: self.params.execution.context.backtest,
2679
3289
  reason,
2680
3290
  };
2681
- if (self.params.callbacks?.onTick) {
2682
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2683
- }
3291
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
2684
3292
  return result;
2685
3293
  };
2686
3294
  const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
@@ -2704,15 +3312,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2704
3312
  scheduledAt: scheduled.scheduledAt,
2705
3313
  pendingAt: activationTime,
2706
3314
  });
2707
- if (await functoolsKit.not(self.params.risk.checkSignal({
2708
- pendingSignal: scheduled,
2709
- symbol: self.params.execution.context.symbol,
2710
- strategyName: self.params.method.context.strategyName,
2711
- exchangeName: self.params.method.context.exchangeName,
2712
- frameName: self.params.method.context.frameName,
2713
- currentPrice: scheduled.priceOpen,
2714
- timestamp: activationTime,
2715
- }))) {
3315
+ if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, scheduled, scheduled.priceOpen, activationTime, self.params.execution.context.backtest))) {
2716
3316
  self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
2717
3317
  symbol: self.params.execution.context.symbol,
2718
3318
  signalId: scheduled.id,
@@ -2728,15 +3328,8 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2728
3328
  _isScheduled: false,
2729
3329
  };
2730
3330
  await self.setPendingSignal(activatedSignal);
2731
- await self.params.risk.addSignal(self.params.execution.context.symbol, {
2732
- strategyName: self.params.method.context.strategyName,
2733
- riskName: self.params.riskName,
2734
- exchangeName: self.params.method.context.exchangeName,
2735
- frameName: self.params.method.context.frameName,
2736
- });
2737
- if (self.params.callbacks?.onOpen) {
2738
- self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
2739
- }
3331
+ await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
3332
+ await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
2740
3333
  return true;
2741
3334
  };
2742
3335
  const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
@@ -2755,21 +3348,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2755
3348
  if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
2756
3349
  self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
2757
3350
  }
2758
- if (self.params.callbacks?.onClose) {
2759
- self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2760
- }
3351
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
2761
3352
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2762
- await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2763
- await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2764
- strategyName: self.params.method.context.strategyName,
2765
- riskName: self.params.riskName,
2766
- exchangeName: self.params.method.context.exchangeName,
2767
- frameName: self.params.method.context.frameName,
2768
- });
3353
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
3354
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
2769
3355
  await self.setPendingSignal(null);
2770
3356
  const result = {
2771
3357
  action: "closed",
2772
- signal: signal,
3358
+ signal: TO_PUBLIC_SIGNAL(signal),
2773
3359
  currentPrice: averagePrice,
2774
3360
  closeReason: closeReason,
2775
3361
  closeTimestamp: closeTimestamp,
@@ -2780,9 +3366,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2780
3366
  symbol: self.params.execution.context.symbol,
2781
3367
  backtest: self.params.execution.context.backtest,
2782
3368
  };
2783
- if (self.params.callbacks?.onTick) {
2784
- self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
2785
- }
3369
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
2786
3370
  return result;
2787
3371
  };
2788
3372
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
@@ -2857,7 +3441,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
2857
3441
  result: null,
2858
3442
  };
2859
3443
  }
2860
- await CALL_PING_CALLBACKS_FN(self, scheduled, candle.timestamp);
3444
+ await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true);
2861
3445
  }
2862
3446
  return {
2863
3447
  activated: false,
@@ -2895,13 +3479,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2895
3479
  }
2896
3480
  // Check TP/SL only if not expired
2897
3481
  // КРИТИЧНО: используем candle.high/low для точной проверки достижения TP/SL
3482
+ // КРИТИЧНО: используем trailing SL если установлен
3483
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
2898
3484
  if (!shouldClose && signal.position === "long") {
2899
3485
  // Для LONG: TP срабатывает если high >= TP, SL если low <= SL
2900
3486
  if (currentCandle.high >= signal.priceTakeProfit) {
2901
3487
  shouldClose = true;
2902
3488
  closeReason = "take_profit";
2903
3489
  }
2904
- else if (currentCandle.low <= signal.priceStopLoss) {
3490
+ else if (currentCandle.low <= effectiveStopLoss) {
2905
3491
  shouldClose = true;
2906
3492
  closeReason = "stop_loss";
2907
3493
  }
@@ -2912,7 +3498,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2912
3498
  shouldClose = true;
2913
3499
  closeReason = "take_profit";
2914
3500
  }
2915
- else if (currentCandle.high >= signal.priceStopLoss) {
3501
+ else if (currentCandle.high >= effectiveStopLoss) {
2916
3502
  shouldClose = true;
2917
3503
  closeReason = "stop_loss";
2918
3504
  }
@@ -2924,7 +3510,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2924
3510
  closePrice = signal.priceTakeProfit;
2925
3511
  }
2926
3512
  else if (closeReason === "stop_loss") {
2927
- closePrice = signal.priceStopLoss;
3513
+ closePrice = effectiveStopLoss; // Используем trailing SL если установлен
2928
3514
  }
2929
3515
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2930
3516
  }
@@ -2938,19 +3524,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2938
3524
  // Moving towards TP
2939
3525
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2940
3526
  const progressPercent = (currentDistance / tpDistance) * 100;
2941
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2942
- if (self.params.callbacks?.onPartialProfit) {
2943
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2944
- }
3527
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2945
3528
  }
2946
3529
  else if (currentDistance < 0) {
2947
- // Moving towards SL
2948
- const slDistance = signal.priceOpen - signal.priceStopLoss;
3530
+ // Moving towards SL (use trailing SL if set)
3531
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3532
+ const slDistance = signal.priceOpen - effectiveStopLoss;
2949
3533
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2950
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2951
- if (self.params.callbacks?.onPartialLoss) {
2952
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2953
- }
3534
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2954
3535
  }
2955
3536
  }
2956
3537
  else if (signal.position === "short") {
@@ -2960,19 +3541,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2960
3541
  // Moving towards TP
2961
3542
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2962
3543
  const progressPercent = (currentDistance / tpDistance) * 100;
2963
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2964
- if (self.params.callbacks?.onPartialProfit) {
2965
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2966
- }
3544
+ await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2967
3545
  }
2968
3546
  if (currentDistance < 0) {
2969
- // Moving towards SL
2970
- const slDistance = signal.priceStopLoss - signal.priceOpen;
3547
+ // Moving towards SL (use trailing SL if set)
3548
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
3549
+ const slDistance = effectiveStopLoss - signal.priceOpen;
2971
3550
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2972
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2973
- if (self.params.callbacks?.onPartialLoss) {
2974
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2975
- }
3551
+ await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
2976
3552
  }
2977
3553
  }
2978
3554
  }
@@ -3042,7 +3618,8 @@ class ClientStrategy {
3042
3618
  // КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
3043
3619
  // даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
3044
3620
  if (this.params.callbacks?.onWrite) {
3045
- this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, this.params.execution.context.backtest);
3621
+ const publicSignal = this._pendingSignal ? TO_PUBLIC_SIGNAL(this._pendingSignal) : null;
3622
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, this.params.execution.context.backtest);
3046
3623
  }
3047
3624
  if (this.params.execution.context.backtest) {
3048
3625
  return;
@@ -3077,7 +3654,7 @@ class ClientStrategy {
3077
3654
  this.params.logger.debug("ClientStrategy getPendingSignal", {
3078
3655
  symbol,
3079
3656
  });
3080
- return this._pendingSignal;
3657
+ return this._pendingSignal ? TO_PUBLIC_SIGNAL(this._pendingSignal) : null;
3081
3658
  }
3082
3659
  /**
3083
3660
  * Retrieves the current scheduled signal.
@@ -3088,7 +3665,7 @@ class ClientStrategy {
3088
3665
  this.params.logger.debug("ClientStrategy getScheduledSignal", {
3089
3666
  symbol,
3090
3667
  });
3091
- return this._scheduledSignal;
3668
+ return this._scheduledSignal ? TO_PUBLIC_SIGNAL(this._scheduledSignal) : null;
3092
3669
  }
3093
3670
  /**
3094
3671
  * Returns the stopped state of the strategy.
@@ -3158,12 +3735,10 @@ class ClientStrategy {
3158
3735
  signalId: cancelledSignal.id,
3159
3736
  });
3160
3737
  // Call onCancel callback
3161
- if (this.params.callbacks?.onCancel) {
3162
- this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
3163
- }
3738
+ await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
3164
3739
  const result = {
3165
3740
  action: "cancelled",
3166
- signal: cancelledSignal,
3741
+ signal: TO_PUBLIC_SIGNAL(cancelledSignal),
3167
3742
  currentPrice,
3168
3743
  closeTimestamp: currentTime,
3169
3744
  strategyName: this.params.method.context.strategyName,
@@ -3272,14 +3847,13 @@ class ClientStrategy {
3272
3847
  const currentPrice = await this.params.exchange.getAveragePrice(symbol);
3273
3848
  const cancelledSignal = this._cancelledSignal;
3274
3849
  this._cancelledSignal = null; // Clear after using
3275
- if (this.params.callbacks?.onCancel) {
3276
- this.params.callbacks.onCancel(this.params.execution.context.symbol, cancelledSignal, currentPrice, this.params.execution.context.backtest);
3277
- }
3850
+ const closeTimestamp = this.params.execution.context.when.getTime();
3851
+ await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
3278
3852
  const cancelledResult = {
3279
3853
  action: "cancelled",
3280
- signal: cancelledSignal,
3854
+ signal: TO_PUBLIC_SIGNAL(cancelledSignal),
3281
3855
  currentPrice,
3282
- closeTimestamp: this.params.execution.context.when.getTime(),
3856
+ closeTimestamp: closeTimestamp,
3283
3857
  strategyName: this.params.method.context.strategyName,
3284
3858
  exchangeName: this.params.method.context.exchangeName,
3285
3859
  frameName: this.params.method.context.frameName,
@@ -3339,7 +3913,7 @@ class ClientStrategy {
3339
3913
  // but this is correct behavior if someone calls backtest() with partial data
3340
3914
  const result = {
3341
3915
  action: "active",
3342
- signal: scheduled,
3916
+ signal: TO_PUBLIC_SIGNAL(scheduled),
3343
3917
  currentPrice: lastPrice,
3344
3918
  percentSl: 0,
3345
3919
  percentTp: 0,
@@ -3458,6 +4032,273 @@ class ClientStrategy {
3458
4032
  }
3459
4033
  await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName);
3460
4034
  }
4035
+ /**
4036
+ * Executes partial close at profit level (moving toward TP).
4037
+ *
4038
+ * Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
4039
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4040
+ *
4041
+ * Behavior:
4042
+ * - Adds entry to signal's `_partial` array with type "profit"
4043
+ * - Validates percentToClose is in range (0, 100]
4044
+ * - Silently skips if total closed would exceed 100%
4045
+ * - Persists updated signal state (backtest and live modes)
4046
+ * - Calls onWrite callback for persistence testing
4047
+ *
4048
+ * Validation:
4049
+ * - Throws if no pending signal exists
4050
+ * - Throws if percentToClose is not a finite number
4051
+ * - Throws if percentToClose <= 0 or > 100
4052
+ * - Throws if currentPrice is not a positive finite number
4053
+ * - Throws if currentPrice is not moving toward TP:
4054
+ * - LONG: currentPrice must be > priceOpen
4055
+ * - SHORT: currentPrice must be < priceOpen
4056
+ *
4057
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4058
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4059
+ * @param currentPrice - Current market price for this partial close (must be in profit direction)
4060
+ * @param backtest - Whether running in backtest mode (controls persistence)
4061
+ * @returns Promise that resolves when state is updated and persisted
4062
+ *
4063
+ * @example
4064
+ * ```typescript
4065
+ * // Close 30% of position at profit (moving toward TP)
4066
+ * await strategy.partialProfit("BTCUSDT", 30, 45000, false);
4067
+ *
4068
+ * // Later close another 20%
4069
+ * await strategy.partialProfit("BTCUSDT", 20, 46000, false);
4070
+ *
4071
+ * // Final close will calculate weighted PNL from all partials
4072
+ * ```
4073
+ */
4074
+ async partialProfit(symbol, percentToClose, currentPrice, backtest) {
4075
+ this.params.logger.debug("ClientStrategy partialProfit", {
4076
+ symbol,
4077
+ percentToClose,
4078
+ currentPrice,
4079
+ hasPendingSignal: this._pendingSignal !== null,
4080
+ });
4081
+ // Validation: must have pending signal
4082
+ if (!this._pendingSignal) {
4083
+ throw new Error(`ClientStrategy partialProfit: No pending signal exists for symbol=${symbol}`);
4084
+ }
4085
+ // Validation: percentToClose must be valid
4086
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
4087
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
4088
+ }
4089
+ if (percentToClose <= 0) {
4090
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be > 0, got ${percentToClose}`);
4091
+ }
4092
+ if (percentToClose > 100) {
4093
+ throw new Error(`ClientStrategy partialProfit: percentToClose must be <= 100, got ${percentToClose}`);
4094
+ }
4095
+ // Validation: currentPrice must be valid
4096
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
4097
+ throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
4098
+ }
4099
+ // Validation: currentPrice must be moving toward TP (profit direction)
4100
+ if (this._pendingSignal.position === "long") {
4101
+ // For LONG: currentPrice must be higher than priceOpen (moving toward TP)
4102
+ if (currentPrice <= this._pendingSignal.priceOpen) {
4103
+ throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
4104
+ }
4105
+ }
4106
+ else {
4107
+ // For SHORT: currentPrice must be lower than priceOpen (moving toward TP)
4108
+ if (currentPrice >= this._pendingSignal.priceOpen) {
4109
+ throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
4110
+ }
4111
+ }
4112
+ // Execute partial close logic
4113
+ PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
4114
+ // Persist updated signal state (inline setPendingSignal content)
4115
+ // Note: this._pendingSignal already mutated by PARTIAL_PROFIT_FN, no reassignment needed
4116
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
4117
+ pendingSignal: this._pendingSignal,
4118
+ });
4119
+ // Call onWrite callback for testing persist storage
4120
+ if (this.params.callbacks?.onWrite) {
4121
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
4122
+ }
4123
+ if (!backtest) {
4124
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
4125
+ }
4126
+ }
4127
+ /**
4128
+ * Executes partial close at loss level (moving toward SL).
4129
+ *
4130
+ * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
4131
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4132
+ *
4133
+ * Behavior:
4134
+ * - Adds entry to signal's `_partial` array with type "loss"
4135
+ * - Validates percentToClose is in range (0, 100]
4136
+ * - Silently skips if total closed would exceed 100%
4137
+ * - Persists updated signal state (backtest and live modes)
4138
+ * - Calls onWrite callback for persistence testing
4139
+ *
4140
+ * Validation:
4141
+ * - Throws if no pending signal exists
4142
+ * - Throws if percentToClose is not a finite number
4143
+ * - Throws if percentToClose <= 0 or > 100
4144
+ * - Throws if currentPrice is not a positive finite number
4145
+ * - Throws if currentPrice is not moving toward SL:
4146
+ * - LONG: currentPrice must be < priceOpen
4147
+ * - SHORT: currentPrice must be > priceOpen
4148
+ *
4149
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4150
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4151
+ * @param currentPrice - Current market price for this partial close (must be in loss direction)
4152
+ * @param backtest - Whether running in backtest mode (controls persistence)
4153
+ * @returns Promise that resolves when state is updated and persisted
4154
+ *
4155
+ * @example
4156
+ * ```typescript
4157
+ * // Close 40% of position at loss (moving toward SL)
4158
+ * await strategy.partialLoss("BTCUSDT", 40, 38000, false);
4159
+ *
4160
+ * // Later close another 30%
4161
+ * await strategy.partialLoss("BTCUSDT", 30, 37000, false);
4162
+ *
4163
+ * // Final close will calculate weighted PNL from all partials
4164
+ * ```
4165
+ */
4166
+ async partialLoss(symbol, percentToClose, currentPrice, backtest) {
4167
+ this.params.logger.debug("ClientStrategy partialLoss", {
4168
+ symbol,
4169
+ percentToClose,
4170
+ currentPrice,
4171
+ hasPendingSignal: this._pendingSignal !== null,
4172
+ });
4173
+ // Validation: must have pending signal
4174
+ if (!this._pendingSignal) {
4175
+ throw new Error(`ClientStrategy partialLoss: No pending signal exists for symbol=${symbol}`);
4176
+ }
4177
+ // Validation: percentToClose must be valid
4178
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose)) {
4179
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be a finite number, got ${percentToClose} (${typeof percentToClose})`);
4180
+ }
4181
+ if (percentToClose <= 0) {
4182
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be > 0, got ${percentToClose}`);
4183
+ }
4184
+ if (percentToClose > 100) {
4185
+ throw new Error(`ClientStrategy partialLoss: percentToClose must be <= 100, got ${percentToClose}`);
4186
+ }
4187
+ // Validation: currentPrice must be valid
4188
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
4189
+ throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
4190
+ }
4191
+ // Validation: currentPrice must be moving toward SL (loss direction)
4192
+ if (this._pendingSignal.position === "long") {
4193
+ // For LONG: currentPrice must be lower than priceOpen (moving toward SL)
4194
+ if (currentPrice >= this._pendingSignal.priceOpen) {
4195
+ throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
4196
+ }
4197
+ }
4198
+ else {
4199
+ // For SHORT: currentPrice must be higher than priceOpen (moving toward SL)
4200
+ if (currentPrice <= this._pendingSignal.priceOpen) {
4201
+ throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
4202
+ }
4203
+ }
4204
+ // Execute partial close logic
4205
+ PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
4206
+ // Persist updated signal state (inline setPendingSignal content)
4207
+ // Note: this._pendingSignal already mutated by PARTIAL_LOSS_FN, no reassignment needed
4208
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
4209
+ pendingSignal: this._pendingSignal,
4210
+ });
4211
+ // Call onWrite callback for testing persist storage
4212
+ if (this.params.callbacks?.onWrite) {
4213
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
4214
+ }
4215
+ if (!backtest) {
4216
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
4217
+ }
4218
+ }
4219
+ /**
4220
+ * Adjusts trailing stop-loss by shifting distance between entry and original SL.
4221
+ *
4222
+ * Calculates new SL based on percentage shift of the distance (entry - originalSL):
4223
+ * - Negative %: tightens stop (moves SL closer to entry, reduces risk)
4224
+ * - Positive %: loosens stop (moves SL away from entry, allows more drawdown)
4225
+ *
4226
+ * For LONG position (entry=100, originalSL=90, distance=10):
4227
+ * - percentShift = -50: newSL = 100 - 10*(1-0.5) = 95 (tighter, closer to entry)
4228
+ * - percentShift = +20: newSL = 100 - 10*(1+0.2) = 88 (looser, away from entry)
4229
+ *
4230
+ * For SHORT position (entry=100, originalSL=110, distance=10):
4231
+ * - percentShift = -50: newSL = 100 + 10*(1-0.5) = 105 (tighter, closer to entry)
4232
+ * - percentShift = +20: newSL = 100 + 10*(1+0.2) = 112 (looser, away from entry)
4233
+ *
4234
+ * Trailing behavior:
4235
+ * - Only updates if new SL is BETTER (protects more profit)
4236
+ * - For LONG: only accepts higher SL (never moves down)
4237
+ * - For SHORT: only accepts lower SL (never moves up)
4238
+ * - Validates that SL never crosses entry price
4239
+ * - Stores in _trailingPriceStopLoss, original priceStopLoss preserved
4240
+ *
4241
+ * Validation:
4242
+ * - Throws if no pending signal exists
4243
+ * - Throws if percentShift is not a finite number
4244
+ * - Throws if percentShift < -100 or > 100
4245
+ * - Throws if percentShift === 0
4246
+ * - Skips if new SL would cross entry price
4247
+ *
4248
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4249
+ * @param percentShift - Percentage shift of SL distance [-100, 100], excluding 0
4250
+ * @param backtest - Whether running in backtest mode (controls persistence)
4251
+ * @returns Promise that resolves when trailing SL is updated and persisted
4252
+ *
4253
+ * @example
4254
+ * ```typescript
4255
+ * // LONG position: entry=100, originalSL=90, distance=10
4256
+ *
4257
+ * // Move SL 50% closer to entry (tighten)
4258
+ * await strategy.trailingStop("BTCUSDT", -50, false);
4259
+ * // newSL = 100 - 10*(1-0.5) = 95
4260
+ *
4261
+ * // Move SL 30% away from entry (loosen, allow more drawdown)
4262
+ * await strategy.trailingStop("BTCUSDT", 30, false);
4263
+ * // newSL = 100 - 10*(1+0.3) = 87 (SKIPPED: worse than current 95)
4264
+ * ```
4265
+ */
4266
+ async trailingStop(symbol, percentShift, backtest) {
4267
+ this.params.logger.debug("ClientStrategy trailingStop", {
4268
+ symbol,
4269
+ percentShift,
4270
+ hasPendingSignal: this._pendingSignal !== null,
4271
+ });
4272
+ // Validation: must have pending signal
4273
+ if (!this._pendingSignal) {
4274
+ throw new Error(`ClientStrategy trailingStop: No pending signal exists for symbol=${symbol}`);
4275
+ }
4276
+ // Validation: percentShift must be valid
4277
+ if (typeof percentShift !== "number" || !isFinite(percentShift)) {
4278
+ throw new Error(`ClientStrategy trailingStop: percentShift must be a finite number, got ${percentShift} (${typeof percentShift})`);
4279
+ }
4280
+ if (percentShift < -100 || percentShift > 100) {
4281
+ throw new Error(`ClientStrategy trailingStop: percentShift must be in range [-100, 100], got ${percentShift}`);
4282
+ }
4283
+ if (percentShift === 0) {
4284
+ throw new Error(`ClientStrategy trailingStop: percentShift cannot be 0`);
4285
+ }
4286
+ // Execute trailing logic
4287
+ TRAILING_STOP_FN(this, this._pendingSignal, percentShift);
4288
+ // Persist updated signal state (inline setPendingSignal content)
4289
+ // Note: this._pendingSignal already mutated by TRAILING_STOP_FN, no reassignment needed
4290
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
4291
+ pendingSignal: this._pendingSignal,
4292
+ });
4293
+ // Call onWrite callback for testing persist storage
4294
+ if (this.params.callbacks?.onWrite) {
4295
+ const publicSignal = TO_PUBLIC_SIGNAL(this._pendingSignal);
4296
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
4297
+ }
4298
+ if (!backtest) {
4299
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName);
4300
+ }
4301
+ }
3461
4302
  }
3462
4303
 
3463
4304
  const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
@@ -3859,6 +4700,7 @@ class StrategyConnectionService {
3859
4700
  constructor() {
3860
4701
  this.loggerService = inject(TYPES.loggerService);
3861
4702
  this.executionContextService = inject(TYPES.executionContextService);
4703
+ this.methodContextService = inject(TYPES.methodContextService);
3862
4704
  this.strategySchemaService = inject(TYPES.strategySchemaService);
3863
4705
  this.riskConnectionService = inject(TYPES.riskConnectionService);
3864
4706
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
@@ -3882,7 +4724,7 @@ class StrategyConnectionService {
3882
4724
  symbol,
3883
4725
  interval,
3884
4726
  execution: this.executionContextService,
3885
- method: { context: { strategyName, exchangeName, frameName } },
4727
+ method: this.methodContextService,
3886
4728
  logger: this.loggerService,
3887
4729
  partial: this.partialConnectionService,
3888
4730
  exchange: this.exchangeConnectionService,
@@ -4084,6 +4926,118 @@ class StrategyConnectionService {
4084
4926
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4085
4927
  await strategy.cancel(symbol, backtest, cancelId);
4086
4928
  };
4929
+ /**
4930
+ * Executes partial close at profit level (moving toward TP).
4931
+ *
4932
+ * Closes a percentage of the pending position at the current price, recording it as a "profit" type partial.
4933
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4934
+ *
4935
+ * Delegates to ClientStrategy.partialProfit() with current execution context.
4936
+ *
4937
+ * @param backtest - Whether running in backtest mode
4938
+ * @param symbol - Trading pair symbol
4939
+ * @param context - Execution context with strategyName, exchangeName, frameName
4940
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4941
+ * @param currentPrice - Current market price for this partial close
4942
+ * @returns Promise that resolves when state is updated and persisted
4943
+ *
4944
+ * @example
4945
+ * ```typescript
4946
+ * // Close 30% of position at profit
4947
+ * await strategyConnectionService.partialProfit(
4948
+ * false,
4949
+ * "BTCUSDT",
4950
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4951
+ * 30,
4952
+ * 45000
4953
+ * );
4954
+ * ```
4955
+ */
4956
+ this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
4957
+ this.loggerService.log("strategyConnectionService partialProfit", {
4958
+ symbol,
4959
+ context,
4960
+ percentToClose,
4961
+ currentPrice,
4962
+ backtest,
4963
+ });
4964
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
4965
+ await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
4966
+ };
4967
+ /**
4968
+ * Executes partial close at loss level (moving toward SL).
4969
+ *
4970
+ * Closes a percentage of the pending position at the current price, recording it as a "loss" type partial.
4971
+ * The partial close is tracked in `_partial` array for weighted PNL calculation when position fully closes.
4972
+ *
4973
+ * Delegates to ClientStrategy.partialLoss() with current execution context.
4974
+ *
4975
+ * @param backtest - Whether running in backtest mode
4976
+ * @param symbol - Trading pair symbol
4977
+ * @param context - Execution context with strategyName, exchangeName, frameName
4978
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
4979
+ * @param currentPrice - Current market price for this partial close
4980
+ * @returns Promise that resolves when state is updated and persisted
4981
+ *
4982
+ * @example
4983
+ * ```typescript
4984
+ * // Close 40% of position at loss
4985
+ * await strategyConnectionService.partialLoss(
4986
+ * false,
4987
+ * "BTCUSDT",
4988
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
4989
+ * 40,
4990
+ * 38000
4991
+ * );
4992
+ * ```
4993
+ */
4994
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
4995
+ this.loggerService.log("strategyConnectionService partialLoss", {
4996
+ symbol,
4997
+ context,
4998
+ percentToClose,
4999
+ currentPrice,
5000
+ backtest,
5001
+ });
5002
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5003
+ await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
5004
+ };
5005
+ /**
5006
+ * Adjusts the trailing stop-loss distance for an active pending signal.
5007
+ *
5008
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
5009
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
5010
+ *
5011
+ * Delegates to ClientStrategy.trailingStop() with current execution context.
5012
+ *
5013
+ * @param backtest - Whether running in backtest mode
5014
+ * @param symbol - Trading pair symbol
5015
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
5016
+ * @param context - Execution context with strategyName, exchangeName, frameName
5017
+ * @returns Promise that resolves when trailing SL is updated
5018
+ *
5019
+ * @example
5020
+ * ```typescript
5021
+ * // LONG: entry=100, originalSL=90, distance=10
5022
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
5023
+ * await strategyConnectionService.trailingStop(
5024
+ * false,
5025
+ * "BTCUSDT",
5026
+ * -50,
5027
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
5028
+ * );
5029
+ * ```
5030
+ */
5031
+ this.trailingStop = async (backtest, symbol, percentShift, context) => {
5032
+ this.loggerService.log("strategyConnectionService trailingStop", {
5033
+ symbol,
5034
+ context,
5035
+ percentShift,
5036
+ backtest,
5037
+ });
5038
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5039
+ await strategy.trailingStop(symbol, percentShift, backtest);
5040
+ };
4087
5041
  }
4088
5042
  }
4089
5043
 
@@ -4106,6 +5060,32 @@ const INTERVAL_MINUTES$2 = {
4106
5060
  "1d": 1440,
4107
5061
  "3d": 4320,
4108
5062
  };
5063
+ /**
5064
+ * Wrapper to call onTimeframe callback with error handling.
5065
+ * Catches and logs any errors thrown by the user-provided callback.
5066
+ *
5067
+ * @param self - ClientFrame instance reference
5068
+ * @param timeframe - Array of generated timestamp dates
5069
+ * @param startDate - Start date of the backtest period
5070
+ * @param endDate - Effective end date of the backtest period
5071
+ * @param interval - Frame interval used for generation
5072
+ */
5073
+ const CALL_TIMEFRAME_CALLBACKS_FN = functoolsKit.trycatch(async (self, timeframe, startDate, endDate, interval) => {
5074
+ if (self.params.callbacks?.onTimeframe) {
5075
+ await self.params.callbacks.onTimeframe(timeframe, startDate, endDate, interval);
5076
+ }
5077
+ }, {
5078
+ fallback: (error) => {
5079
+ const message = "ClientFrame CALL_TIMEFRAME_CALLBACKS_FN thrown";
5080
+ const payload = {
5081
+ error: functoolsKit.errorData(error),
5082
+ message: functoolsKit.getErrorMessage(error),
5083
+ };
5084
+ backtest$1.loggerService.warn(message, payload);
5085
+ console.warn(message, payload);
5086
+ errorEmitter.next(error);
5087
+ },
5088
+ });
4109
5089
  /**
4110
5090
  * Generates timeframe array from startDate to endDate with specified interval.
4111
5091
  * Uses prototype function pattern for memory efficiency.
@@ -4135,9 +5115,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
4135
5115
  timeframes.push(new Date(currentDate));
4136
5116
  currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
4137
5117
  }
4138
- if (self.params.callbacks?.onTimeframe) {
4139
- self.params.callbacks.onTimeframe(timeframes, startDate, effectiveEndDate, interval);
4140
- }
5118
+ await CALL_TIMEFRAME_CALLBACKS_FN(self, timeframes, startDate, effectiveEndDate, interval);
4141
5119
  return timeframes;
4142
5120
  };
4143
5121
  /**
@@ -4296,6 +5274,30 @@ const calculateATRBased = (params, schema) => {
4296
5274
  const stopDistance = atr * atrMultiplier;
4297
5275
  return riskAmount / stopDistance;
4298
5276
  };
5277
+ /**
5278
+ * Wrapper to call onCalculate callback with error handling.
5279
+ * Catches and logs any errors thrown by the user-provided callback.
5280
+ *
5281
+ * @param self - ClientSizing instance reference
5282
+ * @param quantity - Calculated position size
5283
+ * @param params - Parameters used for size calculation
5284
+ */
5285
+ const CALL_CALCULATE_CALLBACKS_FN = functoolsKit.trycatch(async (self, quantity, params) => {
5286
+ if (self.params.callbacks?.onCalculate) {
5287
+ await self.params.callbacks.onCalculate(quantity, params);
5288
+ }
5289
+ }, {
5290
+ fallback: (error) => {
5291
+ const message = "ClientSizing CALL_CALCULATE_CALLBACKS_FN thrown";
5292
+ const payload = {
5293
+ error: functoolsKit.errorData(error),
5294
+ message: functoolsKit.getErrorMessage(error),
5295
+ };
5296
+ backtest$1.loggerService.warn(message, payload);
5297
+ console.warn(message, payload);
5298
+ errorEmitter.next(error);
5299
+ },
5300
+ });
4299
5301
  /**
4300
5302
  * Main calculation function routing to specific sizing method.
4301
5303
  * Applies min/max constraints after calculation.
@@ -4349,9 +5351,7 @@ const CALCULATE_FN = async (params, self) => {
4349
5351
  quantity = Math.min(quantity, schema.maxPositionSize);
4350
5352
  }
4351
5353
  // Trigger callback if defined
4352
- if (schema.callbacks?.onCalculate) {
4353
- schema.callbacks.onCalculate(quantity, params);
4354
- }
5354
+ await CALL_CALCULATE_CALLBACKS_FN(self, quantity, params);
4355
5355
  return quantity;
4356
5356
  };
4357
5357
  /**
@@ -4467,6 +5467,50 @@ const get = (object, path) => {
4467
5467
 
4468
5468
  /** Symbol indicating that positions need to be fetched from persistence */
4469
5469
  const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
5470
+ /**
5471
+ * Converts signal to risk validation format.
5472
+ *
5473
+ * This function is used BEFORE position opens during risk checks.
5474
+ * It ensures all required fields are present for risk validation:
5475
+ *
5476
+ * - Falls back to currentPrice if priceOpen is not set (for ISignalDto/scheduled signals)
5477
+ * - Replaces priceStopLoss with trailing SL if active (for positions with trailing stops)
5478
+ * - Preserves original stop-loss in originalPriceStopLoss for reference
5479
+ *
5480
+ * Use cases:
5481
+ * - Risk validation before opening a position (checkSignal)
5482
+ * - Pre-flight validation of scheduled signals
5483
+ * - Calculating position size based on stop-loss distance
5484
+ *
5485
+ * @param signal - Signal DTO or row (may not have priceOpen for scheduled signals)
5486
+ * @param currentPrice - Current market price, used as fallback for priceOpen if not set
5487
+ * @returns Signal in IRiskSignalRow format with guaranteed priceOpen and effective stop-loss
5488
+ *
5489
+ * @example
5490
+ * ```typescript
5491
+ * // For scheduled signal without priceOpen
5492
+ * const riskSignal = TO_RISK_SIGNAL(scheduledSignal, 45000);
5493
+ * // riskSignal.priceOpen = 45000 (fallback to currentPrice)
5494
+ *
5495
+ * // For signal with trailing SL
5496
+ * const riskSignal = TO_RISK_SIGNAL(activeSignal, 46000);
5497
+ * // riskSignal.priceStopLoss = activeSignal._trailingPriceStopLoss
5498
+ * ```
5499
+ */
5500
+ const TO_RISK_SIGNAL = (signal, currentPrice) => {
5501
+ if ("_trailingPriceStopLoss" in signal) {
5502
+ return {
5503
+ ...signal,
5504
+ priceStopLoss: signal._trailingPriceStopLoss,
5505
+ originalPriceStopLoss: signal.priceStopLoss,
5506
+ };
5507
+ }
5508
+ return {
5509
+ ...signal,
5510
+ priceOpen: signal.priceOpen ?? currentPrice,
5511
+ originalPriceStopLoss: signal.priceStopLoss,
5512
+ };
5513
+ };
4470
5514
  /** Key generator for active position map */
4471
5515
  const CREATE_NAME_FN = (strategyName, exchangeName, symbol) => `${strategyName}_${exchangeName}_${symbol}`;
4472
5516
  /** Wrapper to execute risk validation function with error handling */
@@ -4486,7 +5530,41 @@ const DO_VALIDATION_FN = async (validation, params) => {
4486
5530
  return payload.message;
4487
5531
  }
4488
5532
  };
4489
- /**
5533
+ /** Wrapper to call onRejected callback with error handling */
5534
+ const CALL_REJECTED_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, params) => {
5535
+ if (self.params.callbacks?.onRejected) {
5536
+ await self.params.callbacks.onRejected(symbol, params);
5537
+ }
5538
+ }, {
5539
+ fallback: (error) => {
5540
+ const message = "ClientRisk CALL_REJECTED_CALLBACKS_FN thrown";
5541
+ const payload = {
5542
+ error: functoolsKit.errorData(error),
5543
+ message: functoolsKit.getErrorMessage(error),
5544
+ };
5545
+ backtest$1.loggerService.warn(message, payload);
5546
+ console.warn(message, payload);
5547
+ errorEmitter.next(error);
5548
+ },
5549
+ });
5550
+ /** Wrapper to call onAllowed callback with error handling */
5551
+ const CALL_ALLOWED_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, params) => {
5552
+ if (self.params.callbacks?.onAllowed) {
5553
+ await self.params.callbacks.onAllowed(symbol, params);
5554
+ }
5555
+ }, {
5556
+ fallback: (error) => {
5557
+ const message = "ClientRisk CALL_ALLOWED_CALLBACKS_FN thrown";
5558
+ const payload = {
5559
+ error: functoolsKit.errorData(error),
5560
+ message: functoolsKit.getErrorMessage(error),
5561
+ };
5562
+ backtest$1.loggerService.warn(message, payload);
5563
+ console.warn(message, payload);
5564
+ errorEmitter.next(error);
5565
+ },
5566
+ });
5567
+ /**
4490
5568
  * Initializes active positions by reading from persistence.
4491
5569
  * Uses singleshot pattern to ensure it only runs once.
4492
5570
  * This function is exported for use in tests or other modules.
@@ -4556,6 +5634,7 @@ class ClientRisk {
4556
5634
  const riskMap = this._activePositions;
4557
5635
  const payload = {
4558
5636
  ...params,
5637
+ pendingSignal: TO_RISK_SIGNAL(params.pendingSignal, params.currentPrice),
4559
5638
  activePositionCount: riskMap.size,
4560
5639
  activePositions: Array.from(riskMap.values()),
4561
5640
  };
@@ -4590,15 +5669,11 @@ class ClientRisk {
4590
5669
  // Call params.onRejected for riskSubject emission
4591
5670
  await this.params.onRejected(params.symbol, params, riskMap.size, rejectionResult, params.timestamp, this.params.backtest);
4592
5671
  // Call schema callbacks.onRejected if defined
4593
- if (this.params.callbacks?.onRejected) {
4594
- this.params.callbacks.onRejected(params.symbol, params);
4595
- }
5672
+ await CALL_REJECTED_CALLBACKS_FN(this, params.symbol, params);
4596
5673
  return false;
4597
5674
  }
4598
5675
  // All checks passed
4599
- if (this.params.callbacks?.onAllowed) {
4600
- this.params.callbacks.onAllowed(params.symbol, params);
4601
- }
5676
+ await CALL_ALLOWED_CALLBACKS_FN(this, params.symbol, params);
4602
5677
  return true;
4603
5678
  };
4604
5679
  }
@@ -5210,6 +6285,118 @@ class StrategyCoreService {
5210
6285
  }
5211
6286
  return await this.strategyConnectionService.clear(payload);
5212
6287
  };
6288
+ /**
6289
+ * Executes partial close at profit level (moving toward TP).
6290
+ *
6291
+ * Validates strategy existence and delegates to connection service
6292
+ * to close a percentage of the pending position at profit.
6293
+ *
6294
+ * Does not require execution context as this is a direct state mutation.
6295
+ *
6296
+ * @param backtest - Whether running in backtest mode
6297
+ * @param symbol - Trading pair symbol
6298
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
6299
+ * @param currentPrice - Current market price for this partial close (must be in profit direction)
6300
+ * @param context - Execution context with strategyName, exchangeName, frameName
6301
+ * @returns Promise that resolves when state is updated and persisted
6302
+ *
6303
+ * @example
6304
+ * ```typescript
6305
+ * // Close 30% of position at profit
6306
+ * await strategyCoreService.partialProfit(
6307
+ * false,
6308
+ * "BTCUSDT",
6309
+ * 30,
6310
+ * 45000,
6311
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
6312
+ * );
6313
+ * ```
6314
+ */
6315
+ this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
6316
+ this.loggerService.log("strategyCoreService partialProfit", {
6317
+ symbol,
6318
+ percentToClose,
6319
+ currentPrice,
6320
+ context,
6321
+ backtest,
6322
+ });
6323
+ await this.validate(symbol, context);
6324
+ return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
6325
+ };
6326
+ /**
6327
+ * Executes partial close at loss level (moving toward SL).
6328
+ *
6329
+ * Validates strategy existence and delegates to connection service
6330
+ * to close a percentage of the pending position at loss.
6331
+ *
6332
+ * Does not require execution context as this is a direct state mutation.
6333
+ *
6334
+ * @param backtest - Whether running in backtest mode
6335
+ * @param symbol - Trading pair symbol
6336
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
6337
+ * @param currentPrice - Current market price for this partial close (must be in loss direction)
6338
+ * @param context - Execution context with strategyName, exchangeName, frameName
6339
+ * @returns Promise that resolves when state is updated and persisted
6340
+ *
6341
+ * @example
6342
+ * ```typescript
6343
+ * // Close 40% of position at loss
6344
+ * await strategyCoreService.partialLoss(
6345
+ * false,
6346
+ * "BTCUSDT",
6347
+ * 40,
6348
+ * 38000,
6349
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
6350
+ * );
6351
+ * ```
6352
+ */
6353
+ this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
6354
+ this.loggerService.log("strategyCoreService partialLoss", {
6355
+ symbol,
6356
+ percentToClose,
6357
+ currentPrice,
6358
+ context,
6359
+ backtest,
6360
+ });
6361
+ await this.validate(symbol, context);
6362
+ return await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
6363
+ };
6364
+ /**
6365
+ * Adjusts the trailing stop-loss distance for an active pending signal.
6366
+ *
6367
+ * Validates strategy existence and delegates to connection service
6368
+ * to update the stop-loss distance by a percentage adjustment.
6369
+ *
6370
+ * Does not require execution context as this is a direct state mutation.
6371
+ *
6372
+ * @param backtest - Whether running in backtest mode
6373
+ * @param symbol - Trading pair symbol
6374
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
6375
+ * @param context - Execution context with strategyName, exchangeName, frameName
6376
+ * @returns Promise that resolves when trailing SL is updated
6377
+ *
6378
+ * @example
6379
+ * ```typescript
6380
+ * // LONG: entry=100, originalSL=90, distance=10
6381
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
6382
+ * await strategyCoreService.trailingStop(
6383
+ * false,
6384
+ * "BTCUSDT",
6385
+ * -50,
6386
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
6387
+ * );
6388
+ * ```
6389
+ */
6390
+ this.trailingStop = async (backtest, symbol, percentShift, context) => {
6391
+ this.loggerService.log("strategyCoreService trailingStop", {
6392
+ symbol,
6393
+ percentShift,
6394
+ context,
6395
+ backtest,
6396
+ });
6397
+ await this.validate(symbol, context);
6398
+ return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, context);
6399
+ };
5213
6400
  }
5214
6401
  }
5215
6402
 
@@ -6350,6 +7537,104 @@ class LiveLogicPrivateService {
6350
7537
  }
6351
7538
  }
6352
7539
 
7540
+ /**
7541
+ * Wrapper to call onStrategyStart callback with error handling.
7542
+ * Catches and logs any errors thrown by the user-provided callback.
7543
+ *
7544
+ * @param walkerSchema - Walker schema containing callbacks
7545
+ * @param strategyName - Name of the strategy being tested
7546
+ * @param symbol - Trading pair symbol
7547
+ */
7548
+ const CALL_STRATEGY_START_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol) => {
7549
+ if (walkerSchema.callbacks?.onStrategyStart) {
7550
+ await walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
7551
+ }
7552
+ }, {
7553
+ fallback: (error) => {
7554
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_START_CALLBACKS_FN thrown";
7555
+ const payload = {
7556
+ error: functoolsKit.errorData(error),
7557
+ message: functoolsKit.getErrorMessage(error),
7558
+ };
7559
+ backtest$1.loggerService.warn(message, payload);
7560
+ console.warn(message, payload);
7561
+ errorEmitter.next(error);
7562
+ },
7563
+ });
7564
+ /**
7565
+ * Wrapper to call onStrategyError callback with error handling.
7566
+ * Catches and logs any errors thrown by the user-provided callback.
7567
+ *
7568
+ * @param walkerSchema - Walker schema containing callbacks
7569
+ * @param strategyName - Name of the strategy that failed
7570
+ * @param symbol - Trading pair symbol
7571
+ * @param error - Error that occurred during strategy execution
7572
+ */
7573
+ const CALL_STRATEGY_ERROR_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol, error) => {
7574
+ if (walkerSchema.callbacks?.onStrategyError) {
7575
+ await walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
7576
+ }
7577
+ }, {
7578
+ fallback: (error) => {
7579
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_ERROR_CALLBACKS_FN thrown";
7580
+ const payload = {
7581
+ error: functoolsKit.errorData(error),
7582
+ message: functoolsKit.getErrorMessage(error),
7583
+ };
7584
+ backtest$1.loggerService.warn(message, payload);
7585
+ console.warn(message, payload);
7586
+ errorEmitter.next(error);
7587
+ },
7588
+ });
7589
+ /**
7590
+ * Wrapper to call onStrategyComplete callback with error handling.
7591
+ * Catches and logs any errors thrown by the user-provided callback.
7592
+ *
7593
+ * @param walkerSchema - Walker schema containing callbacks
7594
+ * @param strategyName - Name of the strategy that completed
7595
+ * @param symbol - Trading pair symbol
7596
+ * @param stats - Backtest statistics for the strategy
7597
+ * @param metricValue - Calculated metric value for comparison
7598
+ */
7599
+ const CALL_STRATEGY_COMPLETE_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, strategyName, symbol, stats, metricValue) => {
7600
+ if (walkerSchema.callbacks?.onStrategyComplete) {
7601
+ await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
7602
+ }
7603
+ }, {
7604
+ fallback: (error) => {
7605
+ const message = "WalkerLogicPrivateService CALL_STRATEGY_COMPLETE_CALLBACKS_FN thrown";
7606
+ const payload = {
7607
+ error: functoolsKit.errorData(error),
7608
+ message: functoolsKit.getErrorMessage(error),
7609
+ };
7610
+ backtest$1.loggerService.warn(message, payload);
7611
+ console.warn(message, payload);
7612
+ errorEmitter.next(error);
7613
+ },
7614
+ });
7615
+ /**
7616
+ * Wrapper to call onComplete callback with error handling.
7617
+ * Catches and logs any errors thrown by the user-provided callback.
7618
+ *
7619
+ * @param walkerSchema - Walker schema containing callbacks
7620
+ * @param finalResults - Final results with all strategies ranked
7621
+ */
7622
+ const CALL_COMPLETE_CALLBACKS_FN = functoolsKit.trycatch(async (walkerSchema, finalResults) => {
7623
+ if (walkerSchema.callbacks?.onComplete) {
7624
+ await walkerSchema.callbacks.onComplete(finalResults);
7625
+ }
7626
+ }, {
7627
+ fallback: (error) => {
7628
+ const message = "WalkerLogicPrivateService CALL_COMPLETE_CALLBACKS_FN thrown";
7629
+ const payload = {
7630
+ error: functoolsKit.errorData(error),
7631
+ message: functoolsKit.getErrorMessage(error),
7632
+ };
7633
+ backtest$1.loggerService.warn(message, payload);
7634
+ console.warn(message, payload);
7635
+ errorEmitter.next(error);
7636
+ },
7637
+ });
6353
7638
  /**
6354
7639
  * Private service for walker orchestration (strategy comparison).
6355
7640
  *
@@ -6435,9 +7720,7 @@ class WalkerLogicPrivateService {
6435
7720
  break;
6436
7721
  }
6437
7722
  // Call onStrategyStart callback if provided
6438
- if (walkerSchema.callbacks?.onStrategyStart) {
6439
- walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
6440
- }
7723
+ await CALL_STRATEGY_START_CALLBACKS_FN(walkerSchema, strategyName, symbol);
6441
7724
  this.loggerService.info("walkerLogicPrivateService testing strategy", {
6442
7725
  strategyName,
6443
7726
  symbol,
@@ -6459,9 +7742,7 @@ class WalkerLogicPrivateService {
6459
7742
  });
6460
7743
  await errorEmitter.next(error);
6461
7744
  // Call onStrategyError callback if provided
6462
- if (walkerSchema.callbacks?.onStrategyError) {
6463
- walkerSchema.callbacks.onStrategyError(strategyName, symbol, error);
6464
- }
7745
+ await CALL_STRATEGY_ERROR_CALLBACKS_FN(walkerSchema, strategyName, symbol, error);
6465
7746
  continue;
6466
7747
  }
6467
7748
  this.loggerService.info("walkerLogicPrivateService backtest complete", {
@@ -6512,9 +7793,7 @@ class WalkerLogicPrivateService {
6512
7793
  progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
6513
7794
  });
6514
7795
  // Call onStrategyComplete callback if provided
6515
- if (walkerSchema.callbacks?.onStrategyComplete) {
6516
- await walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
6517
- }
7796
+ await CALL_STRATEGY_COMPLETE_CALLBACKS_FN(walkerSchema, strategyName, symbol, stats, metricValue);
6518
7797
  await walkerEmitter.next(walkerContract);
6519
7798
  yield walkerContract;
6520
7799
  }
@@ -6537,9 +7816,7 @@ class WalkerLogicPrivateService {
6537
7816
  : null,
6538
7817
  };
6539
7818
  // Call onComplete callback if provided with final best results
6540
- if (walkerSchema.callbacks?.onComplete) {
6541
- walkerSchema.callbacks.onComplete(finalResults);
6542
- }
7819
+ await CALL_COMPLETE_CALLBACKS_FN(walkerSchema, finalResults);
6543
7820
  await walkerCompleteSubject.next(finalResults);
6544
7821
  }
6545
7822
  }
@@ -6932,6 +8209,21 @@ const backtest_columns = [
6932
8209
  },
6933
8210
  isVisible: () => true,
6934
8211
  },
8212
+ {
8213
+ key: "partialCloses",
8214
+ label: "Partial Closes",
8215
+ format: (data) => {
8216
+ const partial = data.signal._partial;
8217
+ if (!partial || partial.length === 0)
8218
+ return "N/A";
8219
+ const profitCount = partial.filter(p => p.type === "profit").length;
8220
+ const lossCount = partial.filter(p => p.type === "loss").length;
8221
+ const profitPercent = partial.filter(p => p.type === "profit").reduce((sum, p) => sum + p.percent, 0);
8222
+ const lossPercent = partial.filter(p => p.type === "loss").reduce((sum, p) => sum + p.percent, 0);
8223
+ return `${partial.length} (↑${profitCount}: ${profitPercent.toFixed(1)}%, ↓${lossCount}: ${lossPercent.toFixed(1)}%)`;
8224
+ },
8225
+ isVisible: () => true,
8226
+ },
6935
8227
  {
6936
8228
  key: "closeReason",
6937
8229
  label: "Close Reason",
@@ -7007,49 +8299,49 @@ const heat_columns = [
7007
8299
  {
7008
8300
  key: "totalPnl",
7009
8301
  label: "Total PNL",
7010
- format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%+.2f%%") : "N/A",
8302
+ format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%") : "N/A",
7011
8303
  isVisible: () => true,
7012
8304
  },
7013
8305
  {
7014
8306
  key: "sharpeRatio",
7015
8307
  label: "Sharpe",
7016
- format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio, "%.2f") : "N/A",
8308
+ format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio) : "N/A",
7017
8309
  isVisible: () => true,
7018
8310
  },
7019
8311
  {
7020
8312
  key: "profitFactor",
7021
8313
  label: "PF",
7022
- format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor, "%.2f") : "N/A",
8314
+ format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor) : "N/A",
7023
8315
  isVisible: () => true,
7024
8316
  },
7025
8317
  {
7026
8318
  key: "expectancy",
7027
8319
  label: "Expect",
7028
- format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%+.2f%%") : "N/A",
8320
+ format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%") : "N/A",
7029
8321
  isVisible: () => true,
7030
8322
  },
7031
8323
  {
7032
8324
  key: "winRate",
7033
8325
  label: "WR",
7034
- format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%.1f%%") : "N/A",
8326
+ format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%") : "N/A",
7035
8327
  isVisible: () => true,
7036
8328
  },
7037
8329
  {
7038
8330
  key: "avgWin",
7039
8331
  label: "Avg Win",
7040
- format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%+.2f%%") : "N/A",
8332
+ format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%") : "N/A",
7041
8333
  isVisible: () => true,
7042
8334
  },
7043
8335
  {
7044
8336
  key: "avgLoss",
7045
8337
  label: "Avg Loss",
7046
- format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%+.2f%%") : "N/A",
8338
+ format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%") : "N/A",
7047
8339
  isVisible: () => true,
7048
8340
  },
7049
8341
  {
7050
8342
  key: "maxDrawdown",
7051
8343
  label: "Max DD",
7052
- format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%.2f%%") : "N/A",
8344
+ format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%") : "N/A",
7053
8345
  isVisible: () => true,
7054
8346
  },
7055
8347
  {
@@ -8287,7 +9579,7 @@ class BacktestMarkdownService {
8287
9579
  */
8288
9580
  this.init = functoolsKit.singleshot(async () => {
8289
9581
  this.loggerService.log("backtestMarkdownService init");
8290
- signalBacktestEmitter.subscribe(this.tick);
9582
+ this.unsubscribe = signalBacktestEmitter.subscribe(this.tick);
8291
9583
  });
8292
9584
  }
8293
9585
  }
@@ -8816,7 +10108,7 @@ class LiveMarkdownService {
8816
10108
  */
8817
10109
  this.init = functoolsKit.singleshot(async () => {
8818
10110
  this.loggerService.log("liveMarkdownService init");
8819
- signalLiveEmitter.subscribe(this.tick);
10111
+ this.unsubscribe = signalLiveEmitter.subscribe(this.tick);
8820
10112
  });
8821
10113
  }
8822
10114
  }
@@ -9245,7 +10537,7 @@ class ScheduleMarkdownService {
9245
10537
  */
9246
10538
  this.init = functoolsKit.singleshot(async () => {
9247
10539
  this.loggerService.log("scheduleMarkdownService init");
9248
- signalEmitter.subscribe(this.tick);
10540
+ this.unsubscribe = signalEmitter.subscribe(this.tick);
9249
10541
  });
9250
10542
  }
9251
10543
  }
@@ -9613,7 +10905,7 @@ class PerformanceMarkdownService {
9613
10905
  */
9614
10906
  this.init = functoolsKit.singleshot(async () => {
9615
10907
  this.loggerService.log("performanceMarkdownService init");
9616
- performanceEmitter.subscribe(this.track);
10908
+ this.unsubscribe = performanceEmitter.subscribe(this.track);
9617
10909
  });
9618
10910
  }
9619
10911
  }
@@ -10022,7 +11314,7 @@ class WalkerMarkdownService {
10022
11314
  */
10023
11315
  this.init = functoolsKit.singleshot(async () => {
10024
11316
  this.loggerService.log("walkerMarkdownService init");
10025
- walkerEmitter.subscribe(this.tick);
11317
+ this.unsubscribe = walkerEmitter.subscribe(this.tick);
10026
11318
  });
10027
11319
  }
10028
11320
  }
@@ -10324,7 +11616,7 @@ class HeatmapStorage {
10324
11616
  return [
10325
11617
  `# Portfolio Heatmap: ${strategyName}`,
10326
11618
  "",
10327
- `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
11619
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
10328
11620
  "",
10329
11621
  table
10330
11622
  ].join("\n");
@@ -10549,7 +11841,7 @@ class HeatMarkdownService {
10549
11841
  */
10550
11842
  this.init = functoolsKit.singleshot(async () => {
10551
11843
  this.loggerService.log("heatMarkdownService init");
10552
- signalEmitter.subscribe(this.tick);
11844
+ this.unsubscribe = signalEmitter.subscribe(this.tick);
10553
11845
  });
10554
11846
  }
10555
11847
  }
@@ -11428,12 +12720,16 @@ class OptimizerTemplateService {
11428
12720
  ``,
11429
12721
  `listenWalkerComplete((results) => {`,
11430
12722
  ` console.log("Walker completed:", results.bestStrategy);`,
11431
- ` Walker.dump("${escapedSymbol}", results.walkerName);`,
12723
+ ` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
11432
12724
  `});`,
11433
12725
  ``,
11434
12726
  `listenDoneBacktest((event) => {`,
11435
12727
  ` console.log("Backtest completed:", event.symbol);`,
11436
- ` Backtest.dump(event.symbol, event.strategyName);`,
12728
+ ` Backtest.dump(event.symbol, {`,
12729
+ ` strategyName: event.strategyName,`,
12730
+ ` exchangeName: event.exchangeName,`,
12731
+ ` frameName: event.frameName`,
12732
+ ` });`,
11437
12733
  `});`,
11438
12734
  ``,
11439
12735
  `listenError((error) => {`,
@@ -11467,12 +12763,10 @@ class OptimizerTemplateService {
11467
12763
  ` }`,
11468
12764
  ``,
11469
12765
  ` {`,
11470
- ` let summary = "# Outline Result Summary\\n";`,
12766
+ ` let summary = "# Outline Result Summary\\n\\n";`,
11471
12767
  ``,
11472
12768
  ` {`,
11473
- ` summary += "\\n";`,
11474
- ` summary += \`**ResultId**: \${resultId}\\n\`;`,
11475
- ` summary += "\\n";`,
12769
+ ` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
11476
12770
  ` }`,
11477
12771
  ``,
11478
12772
  ` if (result) {`,
@@ -11488,7 +12782,7 @@ class OptimizerTemplateService {
11488
12782
  ` systemMessages.forEach((msg, idx) => {`,
11489
12783
  ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
11490
12784
  ` summary += msg.content;`,
11491
- ` summary += "\\n";`,
12785
+ ` summary += "\\n\\n";`,
11492
12786
  ` });`,
11493
12787
  ` }`,
11494
12788
  ``,
@@ -13311,8 +14605,9 @@ class PartialMarkdownService {
13311
14605
  */
13312
14606
  this.init = functoolsKit.singleshot(async () => {
13313
14607
  this.loggerService.log("partialMarkdownService init");
13314
- partialProfitSubject.subscribe(this.tickProfit);
13315
- partialLossSubject.subscribe(this.tickLoss);
14608
+ const unProfit = partialProfitSubject.subscribe(this.tickProfit);
14609
+ const unLoss = partialLossSubject.subscribe(this.tickLoss);
14610
+ this.unsubscribe = functoolsKit.compose(() => unProfit(), () => unLoss());
13316
14611
  });
13317
14612
  }
13318
14613
  }
@@ -14057,7 +15352,7 @@ class RiskMarkdownService {
14057
15352
  */
14058
15353
  this.init = functoolsKit.singleshot(async () => {
14059
15354
  this.loggerService.log("riskMarkdownService init");
14060
- riskSubject.subscribe(this.tickRejection);
15355
+ this.unsubscribe = riskSubject.subscribe(this.tickRejection);
14061
15356
  });
14062
15357
  }
14063
15358
  }
@@ -14512,95 +15807,402 @@ const validateInternal = async (args) => {
14512
15807
  *
14513
15808
  * @example
14514
15809
  * ```typescript
14515
- * // Validate specific entity types
14516
- * await validate({
14517
- * RiskName: { CONSERVATIVE: "conservative" },
14518
- * SizingName: { FIXED_1000: "fixed-1000" },
14519
- * });
15810
+ * // Validate specific entity types
15811
+ * await validate({
15812
+ * RiskName: { CONSERVATIVE: "conservative" },
15813
+ * SizingName: { FIXED_1000: "fixed-1000" },
15814
+ * });
15815
+ * ```
15816
+ */
15817
+ async function validate(args = {}) {
15818
+ backtest$1.loggerService.log(METHOD_NAME);
15819
+ return await validateInternal(args);
15820
+ }
15821
+
15822
+ const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
15823
+ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
15824
+ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
15825
+ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
15826
+ const GET_DATE_METHOD_NAME = "exchange.getDate";
15827
+ const GET_MODE_METHOD_NAME = "exchange.getMode";
15828
+ const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
15829
+ /**
15830
+ * Checks if trade context is active (execution and method contexts).
15831
+ *
15832
+ * Returns true when both contexts are active, which is required for calling
15833
+ * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
15834
+ * getDate, and getMode.
15835
+ *
15836
+ * @returns true if trade context is active, false otherwise
15837
+ *
15838
+ * @example
15839
+ * ```typescript
15840
+ * import { hasTradeContext, getCandles } from "backtest-kit";
15841
+ *
15842
+ * if (hasTradeContext()) {
15843
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
15844
+ * } else {
15845
+ * console.log("Trade context not active");
15846
+ * }
15847
+ * ```
15848
+ */
15849
+ function hasTradeContext() {
15850
+ backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
15851
+ return ExecutionContextService.hasContext() && MethodContextService.hasContext();
15852
+ }
15853
+ /**
15854
+ * Fetches historical candle data from the registered exchange.
15855
+ *
15856
+ * Candles are fetched backwards from the current execution context time.
15857
+ * Uses the exchange's getCandles implementation.
15858
+ *
15859
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15860
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
15861
+ * @param limit - Number of candles to fetch
15862
+ * @returns Promise resolving to array of candle data
15863
+ *
15864
+ * @example
15865
+ * ```typescript
15866
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
15867
+ * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
15868
+ * ```
15869
+ */
15870
+ async function getCandles(symbol, interval, limit) {
15871
+ backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
15872
+ symbol,
15873
+ interval,
15874
+ limit,
15875
+ });
15876
+ if (!ExecutionContextService.hasContext()) {
15877
+ throw new Error("getCandles requires an execution context");
15878
+ }
15879
+ if (!MethodContextService.hasContext()) {
15880
+ throw new Error("getCandles requires a method context");
15881
+ }
15882
+ return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
15883
+ }
15884
+ /**
15885
+ * Calculates VWAP (Volume Weighted Average Price) for a symbol.
15886
+ *
15887
+ * Uses the last 5 1-minute candles to calculate:
15888
+ * - Typical Price = (high + low + close) / 3
15889
+ * - VWAP = sum(typical_price * volume) / sum(volume)
15890
+ *
15891
+ * If volume is zero, returns simple average of close prices.
15892
+ *
15893
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15894
+ * @returns Promise resolving to VWAP price
15895
+ *
15896
+ * @example
15897
+ * ```typescript
15898
+ * const vwap = await getAveragePrice("BTCUSDT");
15899
+ * console.log(vwap); // 50125.43
15900
+ * ```
15901
+ */
15902
+ async function getAveragePrice(symbol) {
15903
+ backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
15904
+ symbol,
15905
+ });
15906
+ if (!ExecutionContextService.hasContext()) {
15907
+ throw new Error("getAveragePrice requires an execution context");
15908
+ }
15909
+ if (!MethodContextService.hasContext()) {
15910
+ throw new Error("getAveragePrice requires a method context");
15911
+ }
15912
+ return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
15913
+ }
15914
+ /**
15915
+ * Formats a price value according to exchange rules.
15916
+ *
15917
+ * Uses the exchange's formatPrice implementation for proper decimal places.
15918
+ *
15919
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15920
+ * @param price - Raw price value
15921
+ * @returns Promise resolving to formatted price string
15922
+ *
15923
+ * @example
15924
+ * ```typescript
15925
+ * const formatted = await formatPrice("BTCUSDT", 50000.123456);
15926
+ * console.log(formatted); // "50000.12"
15927
+ * ```
15928
+ */
15929
+ async function formatPrice(symbol, price) {
15930
+ backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
15931
+ symbol,
15932
+ price,
15933
+ });
15934
+ if (!MethodContextService.hasContext()) {
15935
+ throw new Error("formatPrice requires a method context");
15936
+ }
15937
+ return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
15938
+ }
15939
+ /**
15940
+ * Formats a quantity value according to exchange rules.
15941
+ *
15942
+ * Uses the exchange's formatQuantity implementation for proper decimal places.
15943
+ *
15944
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
15945
+ * @param quantity - Raw quantity value
15946
+ * @returns Promise resolving to formatted quantity string
15947
+ *
15948
+ * @example
15949
+ * ```typescript
15950
+ * const formatted = await formatQuantity("BTCUSDT", 0.123456789);
15951
+ * console.log(formatted); // "0.12345678"
15952
+ * ```
15953
+ */
15954
+ async function formatQuantity(symbol, quantity) {
15955
+ backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
15956
+ symbol,
15957
+ quantity,
15958
+ });
15959
+ if (!MethodContextService.hasContext()) {
15960
+ throw new Error("formatQuantity requires a method context");
15961
+ }
15962
+ return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
15963
+ }
15964
+ /**
15965
+ * Gets the current date from execution context.
15966
+ *
15967
+ * In backtest mode: returns the current timeframe date being processed
15968
+ * In live mode: returns current real-time date
15969
+ *
15970
+ * @returns Promise resolving to current execution context date
15971
+ *
15972
+ * @example
15973
+ * ```typescript
15974
+ * const date = await getDate();
15975
+ * console.log(date); // 2024-01-01T12:00:00.000Z
15976
+ * ```
15977
+ */
15978
+ async function getDate() {
15979
+ backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
15980
+ if (!ExecutionContextService.hasContext()) {
15981
+ throw new Error("getDate requires an execution context");
15982
+ }
15983
+ const { when } = backtest$1.executionContextService.context;
15984
+ return new Date(when.getTime());
15985
+ }
15986
+ /**
15987
+ * Gets the current execution mode.
15988
+ *
15989
+ * @returns Promise resolving to "backtest" or "live"
15990
+ *
15991
+ * @example
15992
+ * ```typescript
15993
+ * const mode = await getMode();
15994
+ * if (mode === "backtest") {
15995
+ * console.log("Running in backtest mode");
15996
+ * } else {
15997
+ * console.log("Running in live mode");
15998
+ * }
15999
+ * ```
16000
+ */
16001
+ async function getMode() {
16002
+ backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
16003
+ if (!ExecutionContextService.hasContext()) {
16004
+ throw new Error("getMode requires an execution context");
16005
+ }
16006
+ const { backtest: bt } = backtest$1.executionContextService.context;
16007
+ return bt ? "backtest" : "live";
16008
+ }
16009
+
16010
+ const STOP_METHOD_NAME = "strategy.stop";
16011
+ const CANCEL_METHOD_NAME = "strategy.cancel";
16012
+ const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
16013
+ const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
16014
+ const TRAILING_STOP_METHOD_NAME = "strategy.trailingStop";
16015
+ /**
16016
+ * Stops the strategy from generating new signals.
16017
+ *
16018
+ * Sets internal flag to prevent strategy from opening new signals.
16019
+ * Current active signal (if any) will complete normally.
16020
+ * Backtest/Live mode will stop at the next safe point (idle state or after signal closes).
16021
+ *
16022
+ * Automatically detects backtest/live mode from execution context.
16023
+ *
16024
+ * @param symbol - Trading pair symbol
16025
+ * @param strategyName - Strategy name to stop
16026
+ * @returns Promise that resolves when stop flag is set
16027
+ *
16028
+ * @example
16029
+ * ```typescript
16030
+ * import { stop } from "backtest-kit";
16031
+ *
16032
+ * // Stop strategy after some condition
16033
+ * await stop("BTCUSDT", "my-strategy");
16034
+ * ```
16035
+ */
16036
+ async function stop(symbol) {
16037
+ backtest$1.loggerService.info(STOP_METHOD_NAME, {
16038
+ symbol,
16039
+ });
16040
+ if (!ExecutionContextService.hasContext()) {
16041
+ throw new Error("stop requires an execution context");
16042
+ }
16043
+ if (!MethodContextService.hasContext()) {
16044
+ throw new Error("stop requires a method context");
16045
+ }
16046
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
16047
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
16048
+ await backtest$1.strategyCoreService.stop(isBacktest, symbol, {
16049
+ exchangeName,
16050
+ frameName,
16051
+ strategyName,
16052
+ });
16053
+ }
16054
+ /**
16055
+ * Cancels the scheduled signal without stopping the strategy.
16056
+ *
16057
+ * Clears the scheduled signal (waiting for priceOpen activation).
16058
+ * Does NOT affect active pending signals or strategy operation.
16059
+ * Does NOT set stop flag - strategy can continue generating new signals.
16060
+ *
16061
+ * Automatically detects backtest/live mode from execution context.
16062
+ *
16063
+ * @param symbol - Trading pair symbol
16064
+ * @param strategyName - Strategy name
16065
+ * @param cancelId - Optional cancellation ID for tracking user-initiated cancellations
16066
+ * @returns Promise that resolves when scheduled signal is cancelled
16067
+ *
16068
+ * @example
16069
+ * ```typescript
16070
+ * import { cancel } from "backtest-kit";
16071
+ *
16072
+ * // Cancel scheduled signal with custom ID
16073
+ * await cancel("BTCUSDT", "my-strategy", "manual-cancel-001");
16074
+ * ```
16075
+ */
16076
+ async function cancel(symbol, cancelId) {
16077
+ backtest$1.loggerService.info(CANCEL_METHOD_NAME, {
16078
+ symbol,
16079
+ cancelId,
16080
+ });
16081
+ if (!ExecutionContextService.hasContext()) {
16082
+ throw new Error("cancel requires an execution context");
16083
+ }
16084
+ if (!MethodContextService.hasContext()) {
16085
+ throw new Error("cancel requires a method context");
16086
+ }
16087
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
16088
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
16089
+ await backtest$1.strategyCoreService.cancel(isBacktest, symbol, { exchangeName, frameName, strategyName }, cancelId);
16090
+ }
16091
+ /**
16092
+ * Executes partial close at profit level (moving toward TP).
16093
+ *
16094
+ * Closes a percentage of the active pending position at profit.
16095
+ * Price must be moving toward take profit (in profit direction).
16096
+ *
16097
+ * Automatically detects backtest/live mode from execution context.
16098
+ *
16099
+ * @param symbol - Trading pair symbol
16100
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
16101
+ * @returns Promise that resolves when state is updated
16102
+ *
16103
+ * @throws Error if currentPrice is not in profit direction:
16104
+ * - LONG: currentPrice must be > priceOpen
16105
+ * - SHORT: currentPrice must be < priceOpen
16106
+ *
16107
+ * @example
16108
+ * ```typescript
16109
+ * import { partialProfit } from "backtest-kit";
16110
+ *
16111
+ * // Close 30% of LONG position at profit
16112
+ * await partialProfit("BTCUSDT", 30, 45000);
14520
16113
  * ```
14521
16114
  */
14522
- async function validate(args = {}) {
14523
- backtest$1.loggerService.log(METHOD_NAME);
14524
- return await validateInternal(args);
16115
+ async function partialProfit(symbol, percentToClose) {
16116
+ backtest$1.loggerService.info(PARTIAL_PROFIT_METHOD_NAME, {
16117
+ symbol,
16118
+ percentToClose,
16119
+ });
16120
+ if (!ExecutionContextService.hasContext()) {
16121
+ throw new Error("partialProfit requires an execution context");
16122
+ }
16123
+ if (!MethodContextService.hasContext()) {
16124
+ throw new Error("partialProfit requires a method context");
16125
+ }
16126
+ const currentPrice = await getAveragePrice(symbol);
16127
+ const { backtest: isBacktest } = backtest$1.executionContextService.context;
16128
+ const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
16129
+ await backtest$1.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
14525
16130
  }
14526
-
14527
- const STOP_METHOD_NAME = "strategy.stop";
14528
- const CANCEL_METHOD_NAME = "strategy.cancel";
14529
16131
  /**
14530
- * Stops the strategy from generating new signals.
16132
+ * Executes partial close at loss level (moving toward SL).
14531
16133
  *
14532
- * Sets internal flag to prevent strategy from opening new signals.
14533
- * Current active signal (if any) will complete normally.
14534
- * Backtest/Live mode will stop at the next safe point (idle state or after signal closes).
16134
+ * Closes a percentage of the active pending position at loss.
16135
+ * Price must be moving toward stop loss (in loss direction).
14535
16136
  *
14536
16137
  * Automatically detects backtest/live mode from execution context.
14537
16138
  *
14538
16139
  * @param symbol - Trading pair symbol
14539
- * @param strategyName - Strategy name to stop
14540
- * @returns Promise that resolves when stop flag is set
16140
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
16141
+ * @returns Promise that resolves when state is updated
16142
+ *
16143
+ * @throws Error if currentPrice is not in loss direction:
16144
+ * - LONG: currentPrice must be < priceOpen
16145
+ * - SHORT: currentPrice must be > priceOpen
14541
16146
  *
14542
16147
  * @example
14543
16148
  * ```typescript
14544
- * import { stop } from "backtest-kit";
16149
+ * import { partialLoss } from "backtest-kit";
14545
16150
  *
14546
- * // Stop strategy after some condition
14547
- * await stop("BTCUSDT", "my-strategy");
16151
+ * // Close 40% of LONG position at loss
16152
+ * await partialLoss("BTCUSDT", 40, 38000);
14548
16153
  * ```
14549
16154
  */
14550
- async function stop(symbol) {
14551
- backtest$1.loggerService.info(STOP_METHOD_NAME, {
16155
+ async function partialLoss(symbol, percentToClose) {
16156
+ backtest$1.loggerService.info(PARTIAL_LOSS_METHOD_NAME, {
14552
16157
  symbol,
16158
+ percentToClose,
14553
16159
  });
14554
16160
  if (!ExecutionContextService.hasContext()) {
14555
- throw new Error("stop requires an execution context");
16161
+ throw new Error("partialLoss requires an execution context");
14556
16162
  }
14557
16163
  if (!MethodContextService.hasContext()) {
14558
- throw new Error("stop requires a method context");
16164
+ throw new Error("partialLoss requires a method context");
14559
16165
  }
16166
+ const currentPrice = await getAveragePrice(symbol);
14560
16167
  const { backtest: isBacktest } = backtest$1.executionContextService.context;
14561
16168
  const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
14562
- await backtest$1.strategyCoreService.stop(isBacktest, symbol, {
14563
- exchangeName,
14564
- frameName,
14565
- strategyName,
14566
- });
16169
+ await backtest$1.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
14567
16170
  }
14568
16171
  /**
14569
- * Cancels the scheduled signal without stopping the strategy.
16172
+ * Adjusts the trailing stop-loss distance for an active pending signal.
14570
16173
  *
14571
- * Clears the scheduled signal (waiting for priceOpen activation).
14572
- * Does NOT affect active pending signals or strategy operation.
14573
- * Does NOT set stop flag - strategy can continue generating new signals.
16174
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
16175
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
14574
16176
  *
14575
16177
  * Automatically detects backtest/live mode from execution context.
14576
16178
  *
14577
16179
  * @param symbol - Trading pair symbol
14578
- * @param strategyName - Strategy name
14579
- * @param cancelId - Optional cancellation ID for tracking user-initiated cancellations
14580
- * @returns Promise that resolves when scheduled signal is cancelled
16180
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
16181
+ * @returns Promise that resolves when trailing SL is updated
14581
16182
  *
14582
16183
  * @example
14583
16184
  * ```typescript
14584
- * import { cancel } from "backtest-kit";
16185
+ * import { trailingStop } from "backtest-kit";
14585
16186
  *
14586
- * // Cancel scheduled signal with custom ID
14587
- * await cancel("BTCUSDT", "my-strategy", "manual-cancel-001");
16187
+ * // LONG: entry=100, originalSL=90, distance=10
16188
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
16189
+ * await trailingStop("BTCUSDT", -50);
14588
16190
  * ```
14589
16191
  */
14590
- async function cancel(symbol, cancelId) {
14591
- backtest$1.loggerService.info(CANCEL_METHOD_NAME, {
16192
+ async function trailingStop(symbol, percentShift) {
16193
+ backtest$1.loggerService.info(TRAILING_STOP_METHOD_NAME, {
14592
16194
  symbol,
14593
- cancelId,
16195
+ percentShift,
14594
16196
  });
14595
16197
  if (!ExecutionContextService.hasContext()) {
14596
- throw new Error("cancel requires an execution context");
16198
+ throw new Error("trailingStop requires an execution context");
14597
16199
  }
14598
16200
  if (!MethodContextService.hasContext()) {
14599
- throw new Error("cancel requires a method context");
16201
+ throw new Error("trailingStop requires a method context");
14600
16202
  }
14601
16203
  const { backtest: isBacktest } = backtest$1.executionContextService.context;
14602
16204
  const { exchangeName, frameName, strategyName } = backtest$1.methodContextService.context;
14603
- await backtest$1.strategyCoreService.cancel(isBacktest, symbol, { exchangeName, frameName, strategyName }, cancelId);
16205
+ await backtest$1.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, { exchangeName, frameName, strategyName });
14604
16206
  }
14605
16207
 
14606
16208
  /**
@@ -16318,224 +17920,36 @@ function listenPing(fn) {
16318
17920
  /**
16319
17921
  * Subscribes to filtered ping events with one-time execution.
16320
17922
  *
16321
- * Listens for events matching the filter predicate, then executes callback once
16322
- * and automatically unsubscribes. Useful for waiting for specific ping conditions.
16323
- *
16324
- * @param filterFn - Predicate to filter which events trigger the callback
16325
- * @param fn - Callback function to handle the filtered event (called only once)
16326
- * @returns Unsubscribe function to cancel the listener before it fires
16327
- *
16328
- * @example
16329
- * ```typescript
16330
- * import { listenPingOnce } from "./function/event";
16331
- *
16332
- * // Wait for first ping on BTCUSDT
16333
- * listenPingOnce(
16334
- * (event) => event.symbol === "BTCUSDT",
16335
- * (event) => console.log("First BTCUSDT ping received")
16336
- * );
16337
- *
16338
- * // Wait for ping in backtest mode
16339
- * const cancel = listenPingOnce(
16340
- * (event) => event.backtest === true,
16341
- * (event) => console.log("Backtest ping received at", new Date(event.timestamp))
16342
- * );
16343
- *
16344
- * // Cancel if needed before event fires
16345
- * cancel();
16346
- * ```
16347
- */
16348
- function listenPingOnce(filterFn, fn) {
16349
- backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
16350
- return pingSubject.filter(filterFn).once(fn);
16351
- }
16352
-
16353
- const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
16354
- const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
16355
- const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
16356
- const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
16357
- const GET_DATE_METHOD_NAME = "exchange.getDate";
16358
- const GET_MODE_METHOD_NAME = "exchange.getMode";
16359
- const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
16360
- /**
16361
- * Checks if trade context is active (execution and method contexts).
16362
- *
16363
- * Returns true when both contexts are active, which is required for calling
16364
- * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
16365
- * getDate, and getMode.
16366
- *
16367
- * @returns true if trade context is active, false otherwise
16368
- *
16369
- * @example
16370
- * ```typescript
16371
- * import { hasTradeContext, getCandles } from "backtest-kit";
16372
- *
16373
- * if (hasTradeContext()) {
16374
- * const candles = await getCandles("BTCUSDT", "1m", 100);
16375
- * } else {
16376
- * console.log("Trade context not active");
16377
- * }
16378
- * ```
16379
- */
16380
- function hasTradeContext() {
16381
- backtest$1.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
16382
- return ExecutionContextService.hasContext() && MethodContextService.hasContext();
16383
- }
16384
- /**
16385
- * Fetches historical candle data from the registered exchange.
16386
- *
16387
- * Candles are fetched backwards from the current execution context time.
16388
- * Uses the exchange's getCandles implementation.
16389
- *
16390
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16391
- * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
16392
- * @param limit - Number of candles to fetch
16393
- * @returns Promise resolving to array of candle data
16394
- *
16395
- * @example
16396
- * ```typescript
16397
- * const candles = await getCandles("BTCUSDT", "1m", 100);
16398
- * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
16399
- * ```
16400
- */
16401
- async function getCandles(symbol, interval, limit) {
16402
- backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
16403
- symbol,
16404
- interval,
16405
- limit,
16406
- });
16407
- if (!ExecutionContextService.hasContext()) {
16408
- throw new Error("getCandles requires an execution context");
16409
- }
16410
- if (!MethodContextService.hasContext()) {
16411
- throw new Error("getCandles requires a method context");
16412
- }
16413
- return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
16414
- }
16415
- /**
16416
- * Calculates VWAP (Volume Weighted Average Price) for a symbol.
16417
- *
16418
- * Uses the last 5 1-minute candles to calculate:
16419
- * - Typical Price = (high + low + close) / 3
16420
- * - VWAP = sum(typical_price * volume) / sum(volume)
16421
- *
16422
- * If volume is zero, returns simple average of close prices.
16423
- *
16424
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16425
- * @returns Promise resolving to VWAP price
16426
- *
16427
- * @example
16428
- * ```typescript
16429
- * const vwap = await getAveragePrice("BTCUSDT");
16430
- * console.log(vwap); // 50125.43
16431
- * ```
16432
- */
16433
- async function getAveragePrice(symbol) {
16434
- backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
16435
- symbol,
16436
- });
16437
- if (!ExecutionContextService.hasContext()) {
16438
- throw new Error("getAveragePrice requires an execution context");
16439
- }
16440
- if (!MethodContextService.hasContext()) {
16441
- throw new Error("getAveragePrice requires a method context");
16442
- }
16443
- return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
16444
- }
16445
- /**
16446
- * Formats a price value according to exchange rules.
16447
- *
16448
- * Uses the exchange's formatPrice implementation for proper decimal places.
16449
- *
16450
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16451
- * @param price - Raw price value
16452
- * @returns Promise resolving to formatted price string
16453
- *
16454
- * @example
16455
- * ```typescript
16456
- * const formatted = await formatPrice("BTCUSDT", 50000.123456);
16457
- * console.log(formatted); // "50000.12"
16458
- * ```
16459
- */
16460
- async function formatPrice(symbol, price) {
16461
- backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
16462
- symbol,
16463
- price,
16464
- });
16465
- if (!MethodContextService.hasContext()) {
16466
- throw new Error("formatPrice requires a method context");
16467
- }
16468
- return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
16469
- }
16470
- /**
16471
- * Formats a quantity value according to exchange rules.
16472
- *
16473
- * Uses the exchange's formatQuantity implementation for proper decimal places.
16474
- *
16475
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16476
- * @param quantity - Raw quantity value
16477
- * @returns Promise resolving to formatted quantity string
16478
- *
16479
- * @example
16480
- * ```typescript
16481
- * const formatted = await formatQuantity("BTCUSDT", 0.123456789);
16482
- * console.log(formatted); // "0.12345678"
16483
- * ```
16484
- */
16485
- async function formatQuantity(symbol, quantity) {
16486
- backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
16487
- symbol,
16488
- quantity,
16489
- });
16490
- if (!MethodContextService.hasContext()) {
16491
- throw new Error("formatQuantity requires a method context");
16492
- }
16493
- return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
16494
- }
16495
- /**
16496
- * Gets the current date from execution context.
16497
- *
16498
- * In backtest mode: returns the current timeframe date being processed
16499
- * In live mode: returns current real-time date
16500
- *
16501
- * @returns Promise resolving to current execution context date
16502
- *
16503
- * @example
16504
- * ```typescript
16505
- * const date = await getDate();
16506
- * console.log(date); // 2024-01-01T12:00:00.000Z
16507
- * ```
16508
- */
16509
- async function getDate() {
16510
- backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
16511
- if (!ExecutionContextService.hasContext()) {
16512
- throw new Error("getDate requires an execution context");
16513
- }
16514
- const { when } = backtest$1.executionContextService.context;
16515
- return new Date(when.getTime());
16516
- }
16517
- /**
16518
- * Gets the current execution mode.
16519
- *
16520
- * @returns Promise resolving to "backtest" or "live"
17923
+ * Listens for events matching the filter predicate, then executes callback once
17924
+ * and automatically unsubscribes. Useful for waiting for specific ping conditions.
17925
+ *
17926
+ * @param filterFn - Predicate to filter which events trigger the callback
17927
+ * @param fn - Callback function to handle the filtered event (called only once)
17928
+ * @returns Unsubscribe function to cancel the listener before it fires
16521
17929
  *
16522
17930
  * @example
16523
17931
  * ```typescript
16524
- * const mode = await getMode();
16525
- * if (mode === "backtest") {
16526
- * console.log("Running in backtest mode");
16527
- * } else {
16528
- * console.log("Running in live mode");
16529
- * }
17932
+ * import { listenPingOnce } from "./function/event";
17933
+ *
17934
+ * // Wait for first ping on BTCUSDT
17935
+ * listenPingOnce(
17936
+ * (event) => event.symbol === "BTCUSDT",
17937
+ * (event) => console.log("First BTCUSDT ping received")
17938
+ * );
17939
+ *
17940
+ * // Wait for ping in backtest mode
17941
+ * const cancel = listenPingOnce(
17942
+ * (event) => event.backtest === true,
17943
+ * (event) => console.log("Backtest ping received at", new Date(event.timestamp))
17944
+ * );
17945
+ *
17946
+ * // Cancel if needed before event fires
17947
+ * cancel();
16530
17948
  * ```
16531
17949
  */
16532
- async function getMode() {
16533
- backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
16534
- if (!ExecutionContextService.hasContext()) {
16535
- throw new Error("getMode requires an execution context");
16536
- }
16537
- const { backtest: bt } = backtest$1.executionContextService.context;
16538
- return bt ? "backtest" : "live";
17950
+ function listenPingOnce(filterFn, fn) {
17951
+ backtest$1.loggerService.log(LISTEN_PING_ONCE_METHOD_NAME);
17952
+ return pingSubject.filter(filterFn).once(fn);
16539
17953
  }
16540
17954
 
16541
17955
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
@@ -16625,6 +18039,9 @@ const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
16625
18039
  const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
16626
18040
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
16627
18041
  const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
18042
+ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
18043
+ const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
18044
+ const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.trailingStop";
16628
18045
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
16629
18046
  /**
16630
18047
  * Internal task function that runs backtest and handles completion.
@@ -16651,6 +18068,7 @@ const INSTANCE_TASK_FN$2 = async (symbol, context, self) => {
16651
18068
  await doneBacktestSubject.next({
16652
18069
  exchangeName: context.exchangeName,
16653
18070
  strategyName: context.strategyName,
18071
+ frameName: context.frameName,
16654
18072
  backtest: true,
16655
18073
  symbol,
16656
18074
  });
@@ -16872,6 +18290,7 @@ class BacktestInstance {
16872
18290
  await doneBacktestSubject.next({
16873
18291
  exchangeName: context.exchangeName,
16874
18292
  strategyName: context.strategyName,
18293
+ frameName: context.frameName,
16875
18294
  backtest: true,
16876
18295
  symbol,
16877
18296
  });
@@ -16984,6 +18403,10 @@ class BacktestUtils {
16984
18403
  * ```
16985
18404
  */
16986
18405
  this.getPendingSignal = async (symbol, context) => {
18406
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL, {
18407
+ symbol,
18408
+ context,
18409
+ });
16987
18410
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
16988
18411
  {
16989
18412
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17011,6 +18434,10 @@ class BacktestUtils {
17011
18434
  * ```
17012
18435
  */
17013
18436
  this.getScheduledSignal = async (symbol, context) => {
18437
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
18438
+ symbol,
18439
+ context,
18440
+ });
17014
18441
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
17015
18442
  {
17016
18443
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17044,6 +18471,10 @@ class BacktestUtils {
17044
18471
  * ```
17045
18472
  */
17046
18473
  this.stop = async (symbol, context) => {
18474
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_STOP, {
18475
+ symbol,
18476
+ context,
18477
+ });
17047
18478
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_STOP);
17048
18479
  {
17049
18480
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17078,6 +18509,11 @@ class BacktestUtils {
17078
18509
  * ```
17079
18510
  */
17080
18511
  this.cancel = async (symbol, context, cancelId) => {
18512
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_CANCEL, {
18513
+ symbol,
18514
+ context,
18515
+ cancelId,
18516
+ });
17081
18517
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CANCEL);
17082
18518
  {
17083
18519
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17088,6 +18524,130 @@ class BacktestUtils {
17088
18524
  }
17089
18525
  await backtest$1.strategyCoreService.cancel(true, symbol, context, cancelId);
17090
18526
  };
18527
+ /**
18528
+ * Executes partial close at profit level (moving toward TP).
18529
+ *
18530
+ * Closes a percentage of the active pending position at profit.
18531
+ * Price must be moving toward take profit (in profit direction).
18532
+ *
18533
+ * @param symbol - Trading pair symbol
18534
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
18535
+ * @param currentPrice - Current market price for this partial close
18536
+ * @param context - Execution context with strategyName, exchangeName, and frameName
18537
+ * @returns Promise that resolves when state is updated
18538
+ *
18539
+ * @throws Error if currentPrice is not in profit direction:
18540
+ * - LONG: currentPrice must be > priceOpen
18541
+ * - SHORT: currentPrice must be < priceOpen
18542
+ *
18543
+ * @example
18544
+ * ```typescript
18545
+ * // Close 30% of LONG position at profit
18546
+ * await Backtest.partialProfit("BTCUSDT", 30, 45000, {
18547
+ * exchangeName: "binance",
18548
+ * frameName: "frame1",
18549
+ * strategyName: "my-strategy"
18550
+ * });
18551
+ * ```
18552
+ */
18553
+ this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
18554
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT, {
18555
+ symbol,
18556
+ percentToClose,
18557
+ currentPrice,
18558
+ context,
18559
+ });
18560
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
18561
+ {
18562
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18563
+ riskName &&
18564
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
18565
+ riskList &&
18566
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
18567
+ }
18568
+ await backtest$1.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
18569
+ };
18570
+ /**
18571
+ * Executes partial close at loss level (moving toward SL).
18572
+ *
18573
+ * Closes a percentage of the active pending position at loss.
18574
+ * Price must be moving toward stop loss (in loss direction).
18575
+ *
18576
+ * @param symbol - Trading pair symbol
18577
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
18578
+ * @param currentPrice - Current market price for this partial close
18579
+ * @param context - Execution context with strategyName, exchangeName, and frameName
18580
+ * @returns Promise that resolves when state is updated
18581
+ *
18582
+ * @throws Error if currentPrice is not in loss direction:
18583
+ * - LONG: currentPrice must be < priceOpen
18584
+ * - SHORT: currentPrice must be > priceOpen
18585
+ *
18586
+ * @example
18587
+ * ```typescript
18588
+ * // Close 40% of LONG position at loss
18589
+ * await Backtest.partialLoss("BTCUSDT", 40, 38000, {
18590
+ * exchangeName: "binance",
18591
+ * frameName: "frame1",
18592
+ * strategyName: "my-strategy"
18593
+ * });
18594
+ * ```
18595
+ */
18596
+ this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
18597
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS, {
18598
+ symbol,
18599
+ percentToClose,
18600
+ currentPrice,
18601
+ context,
18602
+ });
18603
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
18604
+ {
18605
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18606
+ riskName &&
18607
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
18608
+ riskList &&
18609
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
18610
+ }
18611
+ await backtest$1.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
18612
+ };
18613
+ /**
18614
+ * Adjusts the trailing stop-loss distance for an active pending signal.
18615
+ *
18616
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
18617
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
18618
+ *
18619
+ * @param symbol - Trading pair symbol
18620
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
18621
+ * @param context - Execution context with strategyName, exchangeName, and frameName
18622
+ * @returns Promise that resolves when trailing SL is updated
18623
+ *
18624
+ * @example
18625
+ * ```typescript
18626
+ * // LONG: entry=100, originalSL=90, distance=10
18627
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
18628
+ * await Backtest.trailingStop("BTCUSDT", -50, {
18629
+ * exchangeName: "binance",
18630
+ * frameName: "frame1",
18631
+ * strategyName: "my-strategy"
18632
+ * });
18633
+ * ```
18634
+ */
18635
+ this.trailingStop = async (symbol, percentShift, context) => {
18636
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_STOP, {
18637
+ symbol,
18638
+ percentShift,
18639
+ context,
18640
+ });
18641
+ backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_STOP);
18642
+ {
18643
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
18644
+ riskName &&
18645
+ backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP);
18646
+ riskList &&
18647
+ riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP));
18648
+ }
18649
+ await backtest$1.strategyCoreService.trailingStop(true, symbol, percentShift, context);
18650
+ };
17091
18651
  /**
17092
18652
  * Gets statistical data from all closed signals for a symbol-strategy pair.
17093
18653
  *
@@ -17107,6 +18667,10 @@ class BacktestUtils {
17107
18667
  * ```
17108
18668
  */
17109
18669
  this.getData = async (symbol, context) => {
18670
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_DATA, {
18671
+ symbol,
18672
+ context,
18673
+ });
17110
18674
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_DATA);
17111
18675
  {
17112
18676
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17137,6 +18701,10 @@ class BacktestUtils {
17137
18701
  * ```
17138
18702
  */
17139
18703
  this.getReport = async (symbol, context, columns) => {
18704
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
18705
+ symbol,
18706
+ context,
18707
+ });
17140
18708
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_REPORT);
17141
18709
  {
17142
18710
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17174,6 +18742,11 @@ class BacktestUtils {
17174
18742
  * ```
17175
18743
  */
17176
18744
  this.dump = async (symbol, context, path, columns) => {
18745
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
18746
+ symbol,
18747
+ context,
18748
+ path,
18749
+ });
17177
18750
  backtest$1.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_DUMP);
17178
18751
  {
17179
18752
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17234,6 +18807,9 @@ const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
17234
18807
  const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
17235
18808
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
17236
18809
  const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
18810
+ const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
18811
+ const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
18812
+ const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.trailingStop";
17237
18813
  /**
17238
18814
  * Internal task function that runs live trading and handles completion.
17239
18815
  * Consumes live trading results and updates instance state flags.
@@ -17259,6 +18835,7 @@ const INSTANCE_TASK_FN$1 = async (symbol, context, self) => {
17259
18835
  await doneLiveSubject.next({
17260
18836
  exchangeName: context.exchangeName,
17261
18837
  strategyName: context.strategyName,
18838
+ frameName: "",
17262
18839
  backtest: false,
17263
18840
  symbol,
17264
18841
  });
@@ -17445,6 +19022,7 @@ class LiveInstance {
17445
19022
  await doneLiveSubject.next({
17446
19023
  exchangeName: context.exchangeName,
17447
19024
  strategyName: context.strategyName,
19025
+ frameName: "",
17448
19026
  backtest: false,
17449
19027
  symbol,
17450
19028
  });
@@ -17564,6 +19142,10 @@ class LiveUtils {
17564
19142
  * ```
17565
19143
  */
17566
19144
  this.getPendingSignal = async (symbol, context) => {
19145
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_PENDING_SIGNAL, {
19146
+ symbol,
19147
+ context,
19148
+ });
17567
19149
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
17568
19150
  {
17569
19151
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17593,6 +19175,10 @@ class LiveUtils {
17593
19175
  * ```
17594
19176
  */
17595
19177
  this.getScheduledSignal = async (symbol, context) => {
19178
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL, {
19179
+ symbol,
19180
+ context,
19181
+ });
17596
19182
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
17597
19183
  {
17598
19184
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17623,6 +19209,10 @@ class LiveUtils {
17623
19209
  * ```
17624
19210
  */
17625
19211
  this.stop = async (symbol, context) => {
19212
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_STOP, {
19213
+ symbol,
19214
+ context,
19215
+ });
17626
19216
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_STOP);
17627
19217
  {
17628
19218
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17659,6 +19249,11 @@ class LiveUtils {
17659
19249
  * ```
17660
19250
  */
17661
19251
  this.cancel = async (symbol, context, cancelId) => {
19252
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_CANCEL, {
19253
+ symbol,
19254
+ context,
19255
+ cancelId,
19256
+ });
17662
19257
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CANCEL);
17663
19258
  {
17664
19259
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17671,6 +19266,133 @@ class LiveUtils {
17671
19266
  frameName: "",
17672
19267
  }, cancelId);
17673
19268
  };
19269
+ /**
19270
+ * Executes partial close at profit level (moving toward TP).
19271
+ *
19272
+ * Closes a percentage of the active pending position at profit.
19273
+ * Price must be moving toward take profit (in profit direction).
19274
+ *
19275
+ * @param symbol - Trading pair symbol
19276
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
19277
+ * @param currentPrice - Current market price for this partial close
19278
+ * @param context - Execution context with strategyName and exchangeName
19279
+ * @returns Promise that resolves when state is updated
19280
+ *
19281
+ * @throws Error if currentPrice is not in profit direction:
19282
+ * - LONG: currentPrice must be > priceOpen
19283
+ * - SHORT: currentPrice must be < priceOpen
19284
+ *
19285
+ * @example
19286
+ * ```typescript
19287
+ * // Close 30% of LONG position at profit
19288
+ * await Live.partialProfit("BTCUSDT", 30, 45000, {
19289
+ * exchangeName: "binance",
19290
+ * strategyName: "my-strategy"
19291
+ * });
19292
+ * ```
19293
+ */
19294
+ this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
19295
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT, {
19296
+ symbol,
19297
+ percentToClose,
19298
+ currentPrice,
19299
+ context,
19300
+ });
19301
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
19302
+ {
19303
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
19304
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
19305
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
19306
+ }
19307
+ await backtest$1.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
19308
+ strategyName: context.strategyName,
19309
+ exchangeName: context.exchangeName,
19310
+ frameName: "",
19311
+ });
19312
+ };
19313
+ /**
19314
+ * Executes partial close at loss level (moving toward SL).
19315
+ *
19316
+ * Closes a percentage of the active pending position at loss.
19317
+ * Price must be moving toward stop loss (in loss direction).
19318
+ *
19319
+ * @param symbol - Trading pair symbol
19320
+ * @param percentToClose - Percentage of position to close (0-100, absolute value)
19321
+ * @param currentPrice - Current market price for this partial close
19322
+ * @param context - Execution context with strategyName and exchangeName
19323
+ * @returns Promise that resolves when state is updated
19324
+ *
19325
+ * @throws Error if currentPrice is not in loss direction:
19326
+ * - LONG: currentPrice must be < priceOpen
19327
+ * - SHORT: currentPrice must be > priceOpen
19328
+ *
19329
+ * @example
19330
+ * ```typescript
19331
+ * // Close 40% of LONG position at loss
19332
+ * await Live.partialLoss("BTCUSDT", 40, 38000, {
19333
+ * exchangeName: "binance",
19334
+ * strategyName: "my-strategy"
19335
+ * });
19336
+ * ```
19337
+ */
19338
+ this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
19339
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS, {
19340
+ symbol,
19341
+ percentToClose,
19342
+ currentPrice,
19343
+ context,
19344
+ });
19345
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS);
19346
+ {
19347
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
19348
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
19349
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
19350
+ }
19351
+ await backtest$1.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
19352
+ strategyName: context.strategyName,
19353
+ exchangeName: context.exchangeName,
19354
+ frameName: "",
19355
+ });
19356
+ };
19357
+ /**
19358
+ * Adjusts the trailing stop-loss distance for an active pending signal.
19359
+ *
19360
+ * Updates the stop-loss distance by a percentage adjustment relative to the original SL distance.
19361
+ * Positive percentShift tightens the SL (reduces distance), negative percentShift loosens it.
19362
+ *
19363
+ * @param symbol - Trading pair symbol
19364
+ * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
19365
+ * @param context - Execution context with strategyName and exchangeName
19366
+ * @returns Promise that resolves when trailing SL is updated
19367
+ *
19368
+ * @example
19369
+ * ```typescript
19370
+ * // LONG: entry=100, originalSL=90, distance=10
19371
+ * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
19372
+ * await Live.trailingStop("BTCUSDT", -50, {
19373
+ * exchangeName: "binance",
19374
+ * strategyName: "my-strategy"
19375
+ * });
19376
+ * ```
19377
+ */
19378
+ this.trailingStop = async (symbol, percentShift, context) => {
19379
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_TRAILING_STOP, {
19380
+ symbol,
19381
+ percentShift,
19382
+ context,
19383
+ });
19384
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_STOP);
19385
+ {
19386
+ const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
19387
+ riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
19388
+ riskList && riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
19389
+ }
19390
+ await backtest$1.strategyCoreService.trailingStop(false, symbol, percentShift, {
19391
+ strategyName: context.strategyName,
19392
+ exchangeName: context.exchangeName,
19393
+ frameName: "",
19394
+ });
19395
+ };
17674
19396
  /**
17675
19397
  * Gets statistical data from all live trading events for a symbol-strategy pair.
17676
19398
  *
@@ -17690,7 +19412,11 @@ class LiveUtils {
17690
19412
  * ```
17691
19413
  */
17692
19414
  this.getData = async (symbol, context) => {
17693
- backtest$1.strategyValidationService.validate(context.strategyName, "LiveUtils.getData");
19415
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_DATA, {
19416
+ symbol,
19417
+ context,
19418
+ });
19419
+ backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_DATA);
17694
19420
  {
17695
19421
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
17696
19422
  riskName && backtest$1.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
@@ -17718,6 +19444,10 @@ class LiveUtils {
17718
19444
  * ```
17719
19445
  */
17720
19446
  this.getReport = async (symbol, context, columns) => {
19447
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
19448
+ symbol,
19449
+ context,
19450
+ });
17721
19451
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_REPORT);
17722
19452
  {
17723
19453
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -17753,6 +19483,11 @@ class LiveUtils {
17753
19483
  * ```
17754
19484
  */
17755
19485
  this.dump = async (symbol, context, path, columns) => {
19486
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
19487
+ symbol,
19488
+ context,
19489
+ path,
19490
+ });
17756
19491
  backtest$1.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_DUMP);
17757
19492
  {
17758
19493
  const { riskName, riskList } = backtest$1.strategySchemaService.get(context.strategyName);
@@ -18099,6 +19834,7 @@ const INSTANCE_TASK_FN = async (symbol, context, self) => {
18099
19834
  await doneWalkerSubject.next({
18100
19835
  exchangeName: walkerSchema.exchangeName,
18101
19836
  strategyName: context.walkerName,
19837
+ frameName: walkerSchema.frameName,
18102
19838
  backtest: true,
18103
19839
  symbol,
18104
19840
  });
@@ -18289,6 +20025,7 @@ class WalkerInstance {
18289
20025
  doneWalkerSubject.next({
18290
20026
  exchangeName: walkerSchema.exchangeName,
18291
20027
  strategyName: context.walkerName,
20028
+ frameName: walkerSchema.frameName,
18292
20029
  backtest: true,
18293
20030
  symbol,
18294
20031
  });
@@ -18397,18 +20134,22 @@ class WalkerUtils {
18397
20134
  * Stop signal is filtered by walkerName to prevent interference.
18398
20135
  *
18399
20136
  * @param symbol - Trading pair symbol
18400
- * @param walkerName - Walker name to stop
20137
+ * @param context - Execution context with walker name
18401
20138
  * @returns Promise that resolves when all stop flags are set
18402
20139
  *
18403
20140
  * @example
18404
20141
  * ```typescript
18405
20142
  * // Stop walker and all its strategies
18406
- * await Walker.stop("BTCUSDT", "my-walker");
20143
+ * await Walker.stop("BTCUSDT", { walkerName: "my-walker" });
18407
20144
  * ```
18408
20145
  */
18409
- this.stop = async (symbol, walkerName) => {
18410
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_STOP);
18411
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
20146
+ this.stop = async (symbol, context) => {
20147
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_STOP, {
20148
+ symbol,
20149
+ context,
20150
+ });
20151
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_STOP);
20152
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18412
20153
  for (const strategyName of walkerSchema.strategies) {
18413
20154
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_STOP);
18414
20155
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18418,7 +20159,7 @@ class WalkerUtils {
18418
20159
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_STOP));
18419
20160
  }
18420
20161
  for (const strategyName of walkerSchema.strategies) {
18421
- await walkerStopSubject.next({ symbol, strategyName, walkerName });
20162
+ await walkerStopSubject.next({ symbol, strategyName, walkerName: context.walkerName });
18422
20163
  await backtest$1.strategyCoreService.stop(true, symbol, {
18423
20164
  strategyName,
18424
20165
  exchangeName: walkerSchema.exchangeName,
@@ -18430,18 +20171,22 @@ class WalkerUtils {
18430
20171
  * Gets walker results data from all strategy comparisons.
18431
20172
  *
18432
20173
  * @param symbol - Trading symbol
18433
- * @param walkerName - Walker name to get data for
20174
+ * @param context - Execution context with walker name
18434
20175
  * @returns Promise resolving to walker results data object
18435
20176
  *
18436
20177
  * @example
18437
20178
  * ```typescript
18438
- * const results = await Walker.getData("BTCUSDT", "my-walker");
20179
+ * const results = await Walker.getData("BTCUSDT", { walkerName: "my-walker" });
18439
20180
  * console.log(results.bestStrategy, results.bestMetric);
18440
20181
  * ```
18441
20182
  */
18442
- this.getData = async (symbol, walkerName) => {
18443
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_DATA);
18444
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
20183
+ this.getData = async (symbol, context) => {
20184
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_DATA, {
20185
+ symbol,
20186
+ context,
20187
+ });
20188
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_DATA);
20189
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18445
20190
  for (const strategyName of walkerSchema.strategies) {
18446
20191
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_DATA);
18447
20192
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18450,7 +20195,7 @@ class WalkerUtils {
18450
20195
  riskList &&
18451
20196
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_DATA));
18452
20197
  }
18453
- return await backtest$1.walkerMarkdownService.getData(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
20198
+ return await backtest$1.walkerMarkdownService.getData(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18454
20199
  exchangeName: walkerSchema.exchangeName,
18455
20200
  frameName: walkerSchema.frameName,
18456
20201
  });
@@ -18459,20 +20204,24 @@ class WalkerUtils {
18459
20204
  * Generates markdown report with all strategy comparisons for a walker.
18460
20205
  *
18461
20206
  * @param symbol - Trading symbol
18462
- * @param walkerName - Walker name to generate report for
20207
+ * @param context - Execution context with walker name
18463
20208
  * @param strategyColumns - Optional strategy columns configuration
18464
20209
  * @param pnlColumns - Optional PNL columns configuration
18465
20210
  * @returns Promise resolving to markdown formatted report string
18466
20211
  *
18467
20212
  * @example
18468
20213
  * ```typescript
18469
- * const markdown = await Walker.getReport("BTCUSDT", "my-walker");
20214
+ * const markdown = await Walker.getReport("BTCUSDT", { walkerName: "my-walker" });
18470
20215
  * console.log(markdown);
18471
20216
  * ```
18472
20217
  */
18473
- this.getReport = async (symbol, walkerName, strategyColumns, pnlColumns) => {
18474
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_GET_REPORT);
18475
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
20218
+ this.getReport = async (symbol, context, strategyColumns, pnlColumns) => {
20219
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_REPORT, {
20220
+ symbol,
20221
+ context,
20222
+ });
20223
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_GET_REPORT);
20224
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18476
20225
  for (const strategyName of walkerSchema.strategies) {
18477
20226
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_GET_REPORT);
18478
20227
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18481,7 +20230,7 @@ class WalkerUtils {
18481
20230
  riskList &&
18482
20231
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_GET_REPORT));
18483
20232
  }
18484
- return await backtest$1.walkerMarkdownService.getReport(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
20233
+ return await backtest$1.walkerMarkdownService.getReport(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18485
20234
  exchangeName: walkerSchema.exchangeName,
18486
20235
  frameName: walkerSchema.frameName,
18487
20236
  }, strategyColumns, pnlColumns);
@@ -18490,7 +20239,7 @@ class WalkerUtils {
18490
20239
  * Saves walker report to disk.
18491
20240
  *
18492
20241
  * @param symbol - Trading symbol
18493
- * @param walkerName - Walker name to save report for
20242
+ * @param context - Execution context with walker name
18494
20243
  * @param path - Optional directory path to save report (default: "./dump/walker")
18495
20244
  * @param strategyColumns - Optional strategy columns configuration
18496
20245
  * @param pnlColumns - Optional PNL columns configuration
@@ -18498,15 +20247,20 @@ class WalkerUtils {
18498
20247
  * @example
18499
20248
  * ```typescript
18500
20249
  * // Save to default path: ./dump/walker/my-walker.md
18501
- * await Walker.dump("BTCUSDT", "my-walker");
20250
+ * await Walker.dump("BTCUSDT", { walkerName: "my-walker" });
18502
20251
  *
18503
20252
  * // Save to custom path: ./custom/path/my-walker.md
18504
- * await Walker.dump("BTCUSDT", "my-walker", "./custom/path");
20253
+ * await Walker.dump("BTCUSDT", { walkerName: "my-walker" }, "./custom/path");
18505
20254
  * ```
18506
20255
  */
18507
- this.dump = async (symbol, walkerName, path, strategyColumns, pnlColumns) => {
18508
- backtest$1.walkerValidationService.validate(walkerName, WALKER_METHOD_NAME_DUMP);
18509
- const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
20256
+ this.dump = async (symbol, context, path, strategyColumns, pnlColumns) => {
20257
+ backtest$1.loggerService.info(WALKER_METHOD_NAME_DUMP, {
20258
+ symbol,
20259
+ context,
20260
+ path,
20261
+ });
20262
+ backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_DUMP);
20263
+ const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
18510
20264
  for (const strategyName of walkerSchema.strategies) {
18511
20265
  backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_DUMP);
18512
20266
  const { riskName, riskList } = backtest$1.strategySchemaService.get(strategyName);
@@ -18515,7 +20269,7 @@ class WalkerUtils {
18515
20269
  riskList &&
18516
20270
  riskList.forEach((riskName) => backtest$1.riskValidationService.validate(riskName, WALKER_METHOD_NAME_DUMP));
18517
20271
  }
18518
- await backtest$1.walkerMarkdownService.dump(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
20272
+ await backtest$1.walkerMarkdownService.dump(context.walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
18519
20273
  exchangeName: walkerSchema.exchangeName,
18520
20274
  frameName: walkerSchema.frameName,
18521
20275
  }, path, strategyColumns, pnlColumns);
@@ -20335,8 +22089,11 @@ exports.listenWalker = listenWalker;
20335
22089
  exports.listenWalkerComplete = listenWalkerComplete;
20336
22090
  exports.listenWalkerOnce = listenWalkerOnce;
20337
22091
  exports.listenWalkerProgress = listenWalkerProgress;
22092
+ exports.partialLoss = partialLoss;
22093
+ exports.partialProfit = partialProfit;
20338
22094
  exports.setColumns = setColumns;
20339
22095
  exports.setConfig = setConfig;
20340
22096
  exports.setLogger = setLogger;
20341
22097
  exports.stop = stop;
22098
+ exports.trailingStop = trailingStop;
20342
22099
  exports.validate = validate;