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