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