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