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