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