backtest-kit 5.9.0 → 5.11.0
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/README.md +17 -4
- package/build/index.cjs +812 -412
- package/build/index.mjs +809 -413
- package/package.json +1 -1
- package/types.d.ts +117 -10
package/build/index.mjs
CHANGED
|
@@ -555,6 +555,16 @@ const GLOBAL_CONFIG = {
|
|
|
555
555
|
* Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
|
|
556
556
|
*/
|
|
557
557
|
CC_ENABLE_PPPL_EVERYWHERE: false,
|
|
558
|
+
/**
|
|
559
|
+
* Enables trailing logic (Trailing Take / Trailing Stop) without requiring absorption conditions.
|
|
560
|
+
* Allows trailing mechanisms to be activated regardless of whether absorption has been detected.
|
|
561
|
+
*
|
|
562
|
+
* This can lead to earlier or more frequent trailing activation, improving reactivity to price movement,
|
|
563
|
+
* but may increase sensitivity to noise and result in premature exits.
|
|
564
|
+
*
|
|
565
|
+
* Default: false (trailing logic is applied only when absorption conditions are met)
|
|
566
|
+
*/
|
|
567
|
+
CC_ENABLE_TRAILING_EVERYWHERE: false,
|
|
558
568
|
/**
|
|
559
569
|
* Cost of entering a position (in USD).
|
|
560
570
|
* This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
|
|
@@ -869,6 +879,8 @@ const INTERVAL_MINUTES$8 = {
|
|
|
869
879
|
"4h": 240,
|
|
870
880
|
"6h": 360,
|
|
871
881
|
"8h": 480,
|
|
882
|
+
"1d": 1440,
|
|
883
|
+
"1w": 10080,
|
|
872
884
|
};
|
|
873
885
|
const MS_PER_MINUTE$6 = 60000;
|
|
874
886
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
@@ -2622,6 +2634,8 @@ const INTERVAL_MINUTES$7 = {
|
|
|
2622
2634
|
"4h": 240,
|
|
2623
2635
|
"6h": 360,
|
|
2624
2636
|
"8h": 480,
|
|
2637
|
+
"1d": 1440,
|
|
2638
|
+
"1w": 10080,
|
|
2625
2639
|
};
|
|
2626
2640
|
/**
|
|
2627
2641
|
* Aligns timestamp down to the nearest interval boundary.
|
|
@@ -3908,6 +3922,390 @@ const beginTime = (run) => (...args) => {
|
|
|
3908
3922
|
return fn();
|
|
3909
3923
|
};
|
|
3910
3924
|
|
|
3925
|
+
/**
|
|
3926
|
+
* Validates the common fields of ISignalDto that apply to both pending and scheduled signals.
|
|
3927
|
+
*
|
|
3928
|
+
* Checks:
|
|
3929
|
+
* - position is "long" or "short"
|
|
3930
|
+
* - priceOpen, priceTakeProfit, priceStopLoss are finite positive numbers
|
|
3931
|
+
* - price relationships are correct for position direction (TP/SL on correct sides of priceOpen)
|
|
3932
|
+
* - TP/SL distance constraints from GLOBAL_CONFIG
|
|
3933
|
+
* - minuteEstimatedTime is valid
|
|
3934
|
+
*
|
|
3935
|
+
* Does NOT check:
|
|
3936
|
+
* - currentPrice vs SL/TP (immediate close protection — handled by pending/scheduled validators)
|
|
3937
|
+
* - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled, scheduledAt, pendingAt
|
|
3938
|
+
*
|
|
3939
|
+
* @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
|
|
3940
|
+
*
|
|
3941
|
+
* @param signal - Signal DTO to validate
|
|
3942
|
+
* @returns Array of error strings (empty if valid)
|
|
3943
|
+
*/
|
|
3944
|
+
const validateCommonSignal = (signal) => {
|
|
3945
|
+
const errors = [];
|
|
3946
|
+
// Валидация position
|
|
3947
|
+
{
|
|
3948
|
+
if (signal.position === undefined || signal.position === null) {
|
|
3949
|
+
errors.push('position is required and must be "long" or "short"');
|
|
3950
|
+
}
|
|
3951
|
+
if (signal.position !== "long" && signal.position !== "short") {
|
|
3952
|
+
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
3956
|
+
{
|
|
3957
|
+
if (typeof signal.priceOpen !== "number") {
|
|
3958
|
+
errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
3959
|
+
}
|
|
3960
|
+
if (!isFinite(signal.priceOpen)) {
|
|
3961
|
+
errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
3962
|
+
}
|
|
3963
|
+
if (typeof signal.priceTakeProfit !== "number") {
|
|
3964
|
+
errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
3965
|
+
}
|
|
3966
|
+
if (!isFinite(signal.priceTakeProfit)) {
|
|
3967
|
+
errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
3968
|
+
}
|
|
3969
|
+
if (typeof signal.priceStopLoss !== "number") {
|
|
3970
|
+
errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
3971
|
+
}
|
|
3972
|
+
if (!isFinite(signal.priceStopLoss)) {
|
|
3973
|
+
errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
// Валидация цен (только если они конечные)
|
|
3977
|
+
{
|
|
3978
|
+
if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
|
|
3979
|
+
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
3980
|
+
}
|
|
3981
|
+
if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
|
|
3982
|
+
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
3983
|
+
}
|
|
3984
|
+
if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
|
|
3985
|
+
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
// Валидация для long позиции
|
|
3989
|
+
if (signal.position === "long") {
|
|
3990
|
+
// Проверка соотношения цен для long
|
|
3991
|
+
{
|
|
3992
|
+
if (signal.priceTakeProfit <= signal.priceOpen) {
|
|
3993
|
+
errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
|
|
3994
|
+
}
|
|
3995
|
+
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
3996
|
+
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
4000
|
+
{
|
|
4001
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4002
|
+
const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4003
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4004
|
+
errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
4005
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
4006
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
4011
|
+
{
|
|
4012
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4013
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
4014
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4015
|
+
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4016
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
4017
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
4022
|
+
{
|
|
4023
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4024
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
4025
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4026
|
+
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4027
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
4028
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
// Валидация для short позиции
|
|
4034
|
+
if (signal.position === "short") {
|
|
4035
|
+
// Проверка соотношения цен для short
|
|
4036
|
+
{
|
|
4037
|
+
if (signal.priceTakeProfit >= signal.priceOpen) {
|
|
4038
|
+
errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
|
|
4039
|
+
}
|
|
4040
|
+
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
4041
|
+
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
4045
|
+
{
|
|
4046
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4047
|
+
const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
|
|
4048
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4049
|
+
errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
4050
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
4051
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
4056
|
+
{
|
|
4057
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4058
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4059
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4060
|
+
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4061
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
4062
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
4067
|
+
{
|
|
4068
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4069
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4070
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4071
|
+
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4072
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
4073
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
// Валидация временных параметров
|
|
4079
|
+
{
|
|
4080
|
+
if (typeof signal.minuteEstimatedTime !== "number") {
|
|
4081
|
+
errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
|
|
4082
|
+
}
|
|
4083
|
+
if (signal.minuteEstimatedTime <= 0) {
|
|
4084
|
+
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
4085
|
+
}
|
|
4086
|
+
if (signal.minuteEstimatedTime === Infinity && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity) {
|
|
4087
|
+
errors.push(`minuteEstimatedTime cannot be Infinity when CC_MAX_SIGNAL_LIFETIME_MINUTES is not Infinity`);
|
|
4088
|
+
}
|
|
4089
|
+
if (signal.minuteEstimatedTime !== Infinity && !Number.isInteger(signal.minuteEstimatedTime)) {
|
|
4090
|
+
errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
// ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
|
|
4094
|
+
{
|
|
4095
|
+
if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity && signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
4096
|
+
const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
|
|
4097
|
+
const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
|
|
4098
|
+
errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
|
|
4099
|
+
`Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
|
|
4100
|
+
`Eternal signals block risk limits and prevent new trades.`);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
// Кидаем ошибку если есть проблемы
|
|
4104
|
+
if (errors.length > 0) {
|
|
4105
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
|
|
4109
|
+
/**
|
|
4110
|
+
* Validates a pending (immediately active) signal before it is opened.
|
|
4111
|
+
*
|
|
4112
|
+
* Checks:
|
|
4113
|
+
* - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled
|
|
4114
|
+
* - currentPrice is a finite positive number
|
|
4115
|
+
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
4116
|
+
* - currentPrice is between SL and TP — position would not be immediately closed on open
|
|
4117
|
+
* - scheduledAt and pendingAt are positive numbers
|
|
4118
|
+
*
|
|
4119
|
+
* @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
|
|
4120
|
+
*
|
|
4121
|
+
* @param signal - Pending signal row to validate
|
|
4122
|
+
* @param currentPrice - Current market price at the moment of signal creation
|
|
4123
|
+
* @throws {Error} If any validation check fails
|
|
4124
|
+
*/
|
|
4125
|
+
const validatePendingSignal = (signal, currentPrice) => {
|
|
4126
|
+
const errors = [];
|
|
4127
|
+
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
4128
|
+
{
|
|
4129
|
+
if (signal.id === undefined || signal.id === null || signal.id === '') {
|
|
4130
|
+
errors.push('id is required and must be a non-empty string');
|
|
4131
|
+
}
|
|
4132
|
+
if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
|
|
4133
|
+
errors.push('exchangeName is required');
|
|
4134
|
+
}
|
|
4135
|
+
if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
|
|
4136
|
+
errors.push('strategyName is required');
|
|
4137
|
+
}
|
|
4138
|
+
if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
|
|
4139
|
+
errors.push('symbol is required and must be a non-empty string');
|
|
4140
|
+
}
|
|
4141
|
+
if (signal._isScheduled === undefined || signal._isScheduled === null) {
|
|
4142
|
+
errors.push('_isScheduled is required');
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
4146
|
+
{
|
|
4147
|
+
if (typeof currentPrice !== "number") {
|
|
4148
|
+
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
4149
|
+
}
|
|
4150
|
+
if (!isFinite(currentPrice)) {
|
|
4151
|
+
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
4152
|
+
}
|
|
4153
|
+
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
4154
|
+
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
if (errors.length > 0) {
|
|
4158
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4159
|
+
}
|
|
4160
|
+
validateCommonSignal(signal);
|
|
4161
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
4162
|
+
if (signal.position === "long") {
|
|
4163
|
+
if (isFinite(currentPrice)) {
|
|
4164
|
+
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
4165
|
+
// SL < currentPrice < TP
|
|
4166
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
4167
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4168
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
4169
|
+
}
|
|
4170
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
4171
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4172
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
if (signal.position === "short") {
|
|
4177
|
+
if (isFinite(currentPrice)) {
|
|
4178
|
+
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
4179
|
+
// TP < currentPrice < SL
|
|
4180
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
4181
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4182
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
4183
|
+
}
|
|
4184
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
4185
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4186
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
// Валидация временных меток
|
|
4191
|
+
{
|
|
4192
|
+
if (typeof signal.scheduledAt !== "number") {
|
|
4193
|
+
errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
|
|
4194
|
+
}
|
|
4195
|
+
if (signal.scheduledAt <= 0) {
|
|
4196
|
+
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
4197
|
+
}
|
|
4198
|
+
if (typeof signal.pendingAt !== "number") {
|
|
4199
|
+
errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
|
|
4200
|
+
}
|
|
4201
|
+
if (signal.pendingAt <= 0) {
|
|
4202
|
+
errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
if (errors.length > 0) {
|
|
4206
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
|
|
4210
|
+
/**
|
|
4211
|
+
* Validates a scheduled signal before it is registered for activation.
|
|
4212
|
+
*
|
|
4213
|
+
* Checks:
|
|
4214
|
+
* - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled
|
|
4215
|
+
* - currentPrice is a finite positive number
|
|
4216
|
+
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
4217
|
+
* - priceOpen is between SL and TP — position would not be immediately closed upon activation
|
|
4218
|
+
* - scheduledAt is a positive number (pendingAt === 0 is allowed until activation)
|
|
4219
|
+
*
|
|
4220
|
+
* @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
|
|
4221
|
+
*
|
|
4222
|
+
* @param signal - Scheduled signal row to validate
|
|
4223
|
+
* @param currentPrice - Current market price at the moment of signal creation
|
|
4224
|
+
* @throws {Error} If any validation check fails
|
|
4225
|
+
*/
|
|
4226
|
+
const validateScheduledSignal = (signal, currentPrice) => {
|
|
4227
|
+
const errors = [];
|
|
4228
|
+
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
4229
|
+
{
|
|
4230
|
+
if (signal.id === undefined || signal.id === null || signal.id === '') {
|
|
4231
|
+
errors.push('id is required and must be a non-empty string');
|
|
4232
|
+
}
|
|
4233
|
+
if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
|
|
4234
|
+
errors.push('exchangeName is required');
|
|
4235
|
+
}
|
|
4236
|
+
if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
|
|
4237
|
+
errors.push('strategyName is required');
|
|
4238
|
+
}
|
|
4239
|
+
if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
|
|
4240
|
+
errors.push('symbol is required and must be a non-empty string');
|
|
4241
|
+
}
|
|
4242
|
+
if (signal._isScheduled === undefined || signal._isScheduled === null) {
|
|
4243
|
+
errors.push('_isScheduled is required');
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
4247
|
+
{
|
|
4248
|
+
if (typeof currentPrice !== "number") {
|
|
4249
|
+
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
4250
|
+
}
|
|
4251
|
+
if (!isFinite(currentPrice)) {
|
|
4252
|
+
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
4253
|
+
}
|
|
4254
|
+
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
4255
|
+
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
if (errors.length > 0) {
|
|
4259
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4260
|
+
}
|
|
4261
|
+
validateCommonSignal(signal);
|
|
4262
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
4263
|
+
if (signal.position === "long") {
|
|
4264
|
+
if (isFinite(signal.priceOpen)) {
|
|
4265
|
+
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
4266
|
+
// SL < priceOpen < TP
|
|
4267
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
4268
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4269
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
4270
|
+
}
|
|
4271
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
4272
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4273
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
if (signal.position === "short") {
|
|
4278
|
+
if (isFinite(signal.priceOpen)) {
|
|
4279
|
+
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
4280
|
+
// TP < priceOpen < SL
|
|
4281
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
4282
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4283
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
4284
|
+
}
|
|
4285
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
4286
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4287
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
// Валидация временных меток
|
|
4292
|
+
{
|
|
4293
|
+
if (typeof signal.scheduledAt !== "number") {
|
|
4294
|
+
errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
|
|
4295
|
+
}
|
|
4296
|
+
if (signal.scheduledAt <= 0) {
|
|
4297
|
+
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
4298
|
+
}
|
|
4299
|
+
if (typeof signal.pendingAt !== "number") {
|
|
4300
|
+
errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
|
|
4301
|
+
}
|
|
4302
|
+
// pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
|
|
4303
|
+
}
|
|
4304
|
+
if (errors.length > 0) {
|
|
4305
|
+
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4306
|
+
}
|
|
4307
|
+
};
|
|
4308
|
+
|
|
3911
4309
|
const INTERVAL_MINUTES$6 = {
|
|
3912
4310
|
"1m": 1,
|
|
3913
4311
|
"3m": 3,
|
|
@@ -4286,272 +4684,6 @@ const TO_PUBLIC_SIGNAL = (signal, currentPrice) => {
|
|
|
4286
4684
|
pnl: toProfitLossDto(signal, currentPrice),
|
|
4287
4685
|
};
|
|
4288
4686
|
};
|
|
4289
|
-
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
4290
|
-
const errors = [];
|
|
4291
|
-
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
4292
|
-
{
|
|
4293
|
-
if (signal.id === undefined || signal.id === null || signal.id === '') {
|
|
4294
|
-
errors.push('id is required and must be a non-empty string');
|
|
4295
|
-
}
|
|
4296
|
-
if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
|
|
4297
|
-
errors.push('exchangeName is required');
|
|
4298
|
-
}
|
|
4299
|
-
if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
|
|
4300
|
-
errors.push('strategyName is required');
|
|
4301
|
-
}
|
|
4302
|
-
if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
|
|
4303
|
-
errors.push('symbol is required and must be a non-empty string');
|
|
4304
|
-
}
|
|
4305
|
-
if (signal._isScheduled === undefined || signal._isScheduled === null) {
|
|
4306
|
-
errors.push('_isScheduled is required');
|
|
4307
|
-
}
|
|
4308
|
-
if (signal.position === undefined || signal.position === null) {
|
|
4309
|
-
errors.push('position is required and must be "long" or "short"');
|
|
4310
|
-
}
|
|
4311
|
-
if (signal.position !== "long" && signal.position !== "short") {
|
|
4312
|
-
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
4316
|
-
{
|
|
4317
|
-
if (typeof currentPrice !== "number") {
|
|
4318
|
-
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
4319
|
-
}
|
|
4320
|
-
if (!isFinite(currentPrice)) {
|
|
4321
|
-
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
4322
|
-
}
|
|
4323
|
-
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
4324
|
-
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
4325
|
-
}
|
|
4326
|
-
}
|
|
4327
|
-
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
4328
|
-
{
|
|
4329
|
-
if (typeof signal.priceOpen !== "number") {
|
|
4330
|
-
errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
4331
|
-
}
|
|
4332
|
-
if (!isFinite(signal.priceOpen)) {
|
|
4333
|
-
errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
4334
|
-
}
|
|
4335
|
-
if (typeof signal.priceTakeProfit !== "number") {
|
|
4336
|
-
errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
4337
|
-
}
|
|
4338
|
-
if (!isFinite(signal.priceTakeProfit)) {
|
|
4339
|
-
errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
4340
|
-
}
|
|
4341
|
-
if (typeof signal.priceStopLoss !== "number") {
|
|
4342
|
-
errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
4343
|
-
}
|
|
4344
|
-
if (!isFinite(signal.priceStopLoss)) {
|
|
4345
|
-
errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
4346
|
-
}
|
|
4347
|
-
}
|
|
4348
|
-
// Валидация цен (только если они конечные)
|
|
4349
|
-
{
|
|
4350
|
-
if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
|
|
4351
|
-
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
4352
|
-
}
|
|
4353
|
-
if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
|
|
4354
|
-
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
4355
|
-
}
|
|
4356
|
-
if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
|
|
4357
|
-
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
4358
|
-
}
|
|
4359
|
-
}
|
|
4360
|
-
// Валидация для long позиции
|
|
4361
|
-
if (signal.position === "long") {
|
|
4362
|
-
// Проверка соотношения цен для long
|
|
4363
|
-
{
|
|
4364
|
-
if (signal.priceTakeProfit <= signal.priceOpen) {
|
|
4365
|
-
errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
|
|
4366
|
-
}
|
|
4367
|
-
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
4368
|
-
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
4372
|
-
{
|
|
4373
|
-
if (!isScheduled && isFinite(currentPrice)) {
|
|
4374
|
-
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
4375
|
-
// SL < currentPrice < TP
|
|
4376
|
-
if (currentPrice <= signal.priceStopLoss) {
|
|
4377
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4378
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
4379
|
-
}
|
|
4380
|
-
if (currentPrice >= signal.priceTakeProfit) {
|
|
4381
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4382
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
4383
|
-
}
|
|
4384
|
-
}
|
|
4385
|
-
}
|
|
4386
|
-
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
4387
|
-
{
|
|
4388
|
-
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
4389
|
-
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
4390
|
-
// SL < priceOpen < TP
|
|
4391
|
-
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
4392
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4393
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
4394
|
-
}
|
|
4395
|
-
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
4396
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4397
|
-
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
4398
|
-
}
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
4402
|
-
{
|
|
4403
|
-
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4404
|
-
const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4405
|
-
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4406
|
-
errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
4407
|
-
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
4408
|
-
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
4409
|
-
}
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
4413
|
-
{
|
|
4414
|
-
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4415
|
-
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
4416
|
-
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4417
|
-
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4418
|
-
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
4419
|
-
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
}
|
|
4423
|
-
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
4424
|
-
{
|
|
4425
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4426
|
-
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
4427
|
-
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4428
|
-
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4429
|
-
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
4430
|
-
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4431
|
-
}
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
|
-
}
|
|
4435
|
-
// Валидация для short позиции
|
|
4436
|
-
if (signal.position === "short") {
|
|
4437
|
-
// Проверка соотношения цен для short
|
|
4438
|
-
{
|
|
4439
|
-
if (signal.priceTakeProfit >= signal.priceOpen) {
|
|
4440
|
-
errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
|
|
4441
|
-
}
|
|
4442
|
-
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
4443
|
-
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
4447
|
-
{
|
|
4448
|
-
if (!isScheduled && isFinite(currentPrice)) {
|
|
4449
|
-
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
4450
|
-
// TP < currentPrice < SL
|
|
4451
|
-
if (currentPrice >= signal.priceStopLoss) {
|
|
4452
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4453
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
4454
|
-
}
|
|
4455
|
-
if (currentPrice <= signal.priceTakeProfit) {
|
|
4456
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4457
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
|
-
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
4462
|
-
{
|
|
4463
|
-
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
4464
|
-
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
4465
|
-
// TP < priceOpen < SL
|
|
4466
|
-
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
4467
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
4468
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
4469
|
-
}
|
|
4470
|
-
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
4471
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
4472
|
-
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
4473
|
-
}
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
4477
|
-
{
|
|
4478
|
-
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4479
|
-
const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
|
|
4480
|
-
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
4481
|
-
errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
4482
|
-
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
4483
|
-
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
}
|
|
4487
|
-
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
4488
|
-
{
|
|
4489
|
-
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4490
|
-
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4491
|
-
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
4492
|
-
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4493
|
-
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
4494
|
-
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
4499
|
-
{
|
|
4500
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4501
|
-
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
4502
|
-
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
4503
|
-
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
4504
|
-
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
4505
|
-
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
4506
|
-
}
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
// Валидация временных параметров
|
|
4511
|
-
{
|
|
4512
|
-
if (typeof signal.minuteEstimatedTime !== "number") {
|
|
4513
|
-
errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
|
|
4514
|
-
}
|
|
4515
|
-
if (signal.minuteEstimatedTime <= 0) {
|
|
4516
|
-
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
4517
|
-
}
|
|
4518
|
-
if (signal.minuteEstimatedTime === Infinity && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity) {
|
|
4519
|
-
errors.push(`minuteEstimatedTime cannot be Infinity when CC_MAX_SIGNAL_LIFETIME_MINUTES is not Infinity`);
|
|
4520
|
-
}
|
|
4521
|
-
if (signal.minuteEstimatedTime !== Infinity && !Number.isInteger(signal.minuteEstimatedTime)) {
|
|
4522
|
-
errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
|
|
4523
|
-
}
|
|
4524
|
-
}
|
|
4525
|
-
// ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
|
|
4526
|
-
{
|
|
4527
|
-
if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity && signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
4528
|
-
const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
|
|
4529
|
-
const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
|
|
4530
|
-
errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
|
|
4531
|
-
`Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
|
|
4532
|
-
`Eternal signals block risk limits and prevent new trades.`);
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
// Валидация временных меток
|
|
4536
|
-
{
|
|
4537
|
-
if (typeof signal.scheduledAt !== "number") {
|
|
4538
|
-
errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
|
|
4539
|
-
}
|
|
4540
|
-
if (signal.scheduledAt <= 0) {
|
|
4541
|
-
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
4542
|
-
}
|
|
4543
|
-
if (typeof signal.pendingAt !== "number") {
|
|
4544
|
-
errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
|
|
4545
|
-
}
|
|
4546
|
-
if (signal.pendingAt <= 0 && !isScheduled) {
|
|
4547
|
-
errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
|
|
4548
|
-
}
|
|
4549
|
-
}
|
|
4550
|
-
// Кидаем ошибку если есть проблемы
|
|
4551
|
-
if (errors.length > 0) {
|
|
4552
|
-
throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
4553
|
-
}
|
|
4554
|
-
};
|
|
4555
4687
|
const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
4556
4688
|
if (self._isStopped) {
|
|
4557
4689
|
return null;
|
|
@@ -4617,7 +4749,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4617
4749
|
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4618
4750
|
};
|
|
4619
4751
|
// Валидируем сигнал перед возвратом
|
|
4620
|
-
|
|
4752
|
+
validatePendingSignal(signalRow, currentPrice);
|
|
4621
4753
|
return signalRow;
|
|
4622
4754
|
}
|
|
4623
4755
|
// ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
|
|
@@ -4642,7 +4774,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4642
4774
|
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4643
4775
|
};
|
|
4644
4776
|
// Валидируем сигнал перед возвратом
|
|
4645
|
-
|
|
4777
|
+
validateScheduledSignal(scheduledSignalRow, currentPrice);
|
|
4646
4778
|
return scheduledSignalRow;
|
|
4647
4779
|
}
|
|
4648
4780
|
const signalRow = {
|
|
@@ -4664,7 +4796,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4664
4796
|
_peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
|
|
4665
4797
|
};
|
|
4666
4798
|
// Валидируем сигнал перед возвратом
|
|
4667
|
-
|
|
4799
|
+
validatePendingSignal(signalRow, currentPrice);
|
|
4668
4800
|
return signalRow;
|
|
4669
4801
|
}, {
|
|
4670
4802
|
defaultValue: null,
|
|
@@ -4854,8 +4986,12 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
|
|
|
4854
4986
|
// CRITICAL: Larger percentShift absorbs smaller one
|
|
4855
4987
|
// For LONG: higher SL (closer to entry) absorbs lower one
|
|
4856
4988
|
// For SHORT: lower SL (closer to entry) absorbs higher one
|
|
4989
|
+
// When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
|
|
4857
4990
|
let shouldUpdate = false;
|
|
4858
|
-
if (
|
|
4991
|
+
if (GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
|
|
4992
|
+
shouldUpdate = true;
|
|
4993
|
+
}
|
|
4994
|
+
else if (signal.position === "long") {
|
|
4859
4995
|
// LONG: update only if new SL is higher (better protection)
|
|
4860
4996
|
shouldUpdate = newStopLoss > currentTrailingSL;
|
|
4861
4997
|
}
|
|
@@ -4936,8 +5072,12 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
|
|
|
4936
5072
|
// CRITICAL: Larger percentShift absorbs smaller one
|
|
4937
5073
|
// For LONG: lower TP (closer to entry) absorbs higher one
|
|
4938
5074
|
// For SHORT: higher TP (closer to entry) absorbs lower one
|
|
5075
|
+
// When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
|
|
4939
5076
|
let shouldUpdate = false;
|
|
4940
|
-
if (
|
|
5077
|
+
if (GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
|
|
5078
|
+
shouldUpdate = true;
|
|
5079
|
+
}
|
|
5080
|
+
else if (signal.position === "long") {
|
|
4941
5081
|
// LONG: update only if new TP is lower (closer to entry, more conservative)
|
|
4942
5082
|
shouldUpdate = newTakeProfit < currentTrailingTP;
|
|
4943
5083
|
}
|
|
@@ -6325,7 +6465,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
|
|
|
6325
6465
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
6326
6466
|
return result;
|
|
6327
6467
|
};
|
|
6328
|
-
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
6468
|
+
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
|
|
6329
6469
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
6330
6470
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
6331
6471
|
const bufferCandlesCount = candlesCount - 1;
|
|
@@ -6338,6 +6478,11 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
6338
6478
|
}
|
|
6339
6479
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
6340
6480
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
6481
|
+
// Если timestamp свечи вышел за frameEndTime — отменяем scheduled сигнал
|
|
6482
|
+
if (candle.timestamp > frameEndTime) {
|
|
6483
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
|
|
6484
|
+
return { outcome: "cancelled", result };
|
|
6485
|
+
}
|
|
6341
6486
|
// КРИТИЧНО: Проверяем был ли сигнал отменен пользователем через cancel()
|
|
6342
6487
|
if (self._cancelledSignal) {
|
|
6343
6488
|
// Сигнал был отменен через cancel() в onSchedulePing
|
|
@@ -6485,7 +6630,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
6485
6630
|
}
|
|
6486
6631
|
return { outcome: "pending" };
|
|
6487
6632
|
};
|
|
6488
|
-
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
6633
|
+
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEndTime) => {
|
|
6489
6634
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
6490
6635
|
const bufferCandlesCount = candlesCount - 1;
|
|
6491
6636
|
// КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
|
|
@@ -6502,6 +6647,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
6502
6647
|
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
6503
6648
|
const recentCandles = candles.slice(startIndex, i + 1);
|
|
6504
6649
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
6650
|
+
// Если timestamp свечи вышел за frameEndTime — закрываем pending сигнал по time_expired
|
|
6651
|
+
if (currentCandleTimestamp > frameEndTime) {
|
|
6652
|
+
const result = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, averagePrice, "time_expired", currentCandleTimestamp);
|
|
6653
|
+
if (!result) {
|
|
6654
|
+
throw new Error(`ClientStrategy backtest: frameEndTime time_expired close rejected by sync (signalId=${signal.id}).`);
|
|
6655
|
+
}
|
|
6656
|
+
return result;
|
|
6657
|
+
}
|
|
6505
6658
|
// КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
|
|
6506
6659
|
if (self._closedSignal) {
|
|
6507
6660
|
return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
|
|
@@ -7667,7 +7820,7 @@ class ClientStrategy {
|
|
|
7667
7820
|
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
|
|
7668
7821
|
* ```
|
|
7669
7822
|
*/
|
|
7670
|
-
async backtest(symbol, strategyName, candles) {
|
|
7823
|
+
async backtest(symbol, strategyName, candles, frameEndTime) {
|
|
7671
7824
|
this.params.logger.debug("ClientStrategy backtest", {
|
|
7672
7825
|
symbol,
|
|
7673
7826
|
strategyName,
|
|
@@ -7675,6 +7828,7 @@ class ClientStrategy {
|
|
|
7675
7828
|
candlesCount: candles.length,
|
|
7676
7829
|
hasScheduled: !!this._scheduledSignal,
|
|
7677
7830
|
hasPending: !!this._pendingSignal,
|
|
7831
|
+
frameEndTime,
|
|
7678
7832
|
});
|
|
7679
7833
|
if (!this.params.execution.context.backtest) {
|
|
7680
7834
|
throw new Error("ClientStrategy backtest: running in live context");
|
|
@@ -7793,7 +7947,7 @@ class ClientStrategy {
|
|
|
7793
7947
|
priceOpen: scheduled.priceOpen,
|
|
7794
7948
|
position: scheduled.position,
|
|
7795
7949
|
});
|
|
7796
|
-
const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
7950
|
+
const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles, frameEndTime);
|
|
7797
7951
|
if (scheduledResult.outcome === "cancelled") {
|
|
7798
7952
|
return scheduledResult.result;
|
|
7799
7953
|
}
|
|
@@ -7870,7 +8024,7 @@ class ClientStrategy {
|
|
|
7870
8024
|
if (candles.length < candlesCount) {
|
|
7871
8025
|
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
7872
8026
|
}
|
|
7873
|
-
return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
8027
|
+
return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles, frameEndTime);
|
|
7874
8028
|
}
|
|
7875
8029
|
/**
|
|
7876
8030
|
* Stops the strategy from generating new signals.
|
|
@@ -8665,12 +8819,15 @@ class ClientStrategy {
|
|
|
8665
8819
|
if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
|
|
8666
8820
|
return false;
|
|
8667
8821
|
// Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
|
|
8668
|
-
|
|
8669
|
-
if (
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8822
|
+
// When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
|
|
8823
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
|
|
8824
|
+
const currentTrailingSL = signal._trailingPriceStopLoss;
|
|
8825
|
+
if (currentTrailingSL !== undefined) {
|
|
8826
|
+
if (signal.position === "long" && newStopLoss <= currentTrailingSL)
|
|
8827
|
+
return false;
|
|
8828
|
+
if (signal.position === "short" && newStopLoss >= currentTrailingSL)
|
|
8829
|
+
return false;
|
|
8830
|
+
}
|
|
8674
8831
|
}
|
|
8675
8832
|
return true;
|
|
8676
8833
|
}
|
|
@@ -8912,12 +9069,15 @@ class ClientStrategy {
|
|
|
8912
9069
|
if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
|
|
8913
9070
|
return false;
|
|
8914
9071
|
// Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
|
|
8915
|
-
|
|
8916
|
-
if (
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
9072
|
+
// When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
|
|
9073
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
|
|
9074
|
+
const currentTrailingTP = signal._trailingPriceTakeProfit;
|
|
9075
|
+
if (currentTrailingTP !== undefined) {
|
|
9076
|
+
if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
|
|
9077
|
+
return false;
|
|
9078
|
+
if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
|
|
9079
|
+
return false;
|
|
9080
|
+
}
|
|
8921
9081
|
}
|
|
8922
9082
|
return true;
|
|
8923
9083
|
}
|
|
@@ -10263,17 +10423,18 @@ class StrategyConnectionService {
|
|
|
10263
10423
|
* @param candles - Array of historical candle data to backtest
|
|
10264
10424
|
* @returns Promise resolving to backtest result (signal or idle)
|
|
10265
10425
|
*/
|
|
10266
|
-
this.backtest = async (symbol, context, candles) => {
|
|
10426
|
+
this.backtest = async (symbol, context, candles, frameEndTime) => {
|
|
10267
10427
|
const backtest = this.executionContextService.context.backtest;
|
|
10268
10428
|
this.loggerService.log("strategyConnectionService backtest", {
|
|
10269
10429
|
symbol,
|
|
10270
10430
|
context,
|
|
10271
10431
|
candleCount: candles.length,
|
|
10432
|
+
frameEndTime,
|
|
10272
10433
|
backtest,
|
|
10273
10434
|
});
|
|
10274
10435
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
10275
10436
|
await strategy.waitForInit();
|
|
10276
|
-
const tick = await strategy.backtest(symbol, context.strategyName, candles);
|
|
10437
|
+
const tick = await strategy.backtest(symbol, context.strategyName, candles, frameEndTime);
|
|
10277
10438
|
{
|
|
10278
10439
|
await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
|
|
10279
10440
|
}
|
|
@@ -10940,7 +11101,8 @@ const INTERVAL_MINUTES$5 = {
|
|
|
10940
11101
|
"8h": 480,
|
|
10941
11102
|
"12h": 720,
|
|
10942
11103
|
"1d": 1440,
|
|
10943
|
-
"
|
|
11104
|
+
"1w": 10080,
|
|
11105
|
+
"1M": 43200,
|
|
10944
11106
|
};
|
|
10945
11107
|
/**
|
|
10946
11108
|
* Wrapper to call onTimeframe callback with error handling.
|
|
@@ -11359,6 +11521,8 @@ const INTERVAL_MINUTES$4 = {
|
|
|
11359
11521
|
"4h": 240,
|
|
11360
11522
|
"6h": 360,
|
|
11361
11523
|
"8h": 480,
|
|
11524
|
+
"1d": 1440,
|
|
11525
|
+
"1w": 10080,
|
|
11362
11526
|
};
|
|
11363
11527
|
/**
|
|
11364
11528
|
* Aligns timestamp down to the nearest interval boundary.
|
|
@@ -14208,17 +14372,18 @@ class StrategyCoreService {
|
|
|
14208
14372
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
14209
14373
|
* @returns Closed signal result with PNL
|
|
14210
14374
|
*/
|
|
14211
|
-
this.backtest = async (symbol, candles, when, backtest, context) => {
|
|
14375
|
+
this.backtest = async (symbol, candles, frameEndTime, when, backtest, context) => {
|
|
14212
14376
|
this.loggerService.log("strategyCoreService backtest", {
|
|
14213
14377
|
symbol,
|
|
14214
14378
|
candleCount: candles.length,
|
|
14215
14379
|
when,
|
|
14216
14380
|
backtest,
|
|
14217
14381
|
context,
|
|
14382
|
+
frameEndTime,
|
|
14218
14383
|
});
|
|
14219
14384
|
await this.validate(context);
|
|
14220
14385
|
return await ExecutionContextService.runInContext(async () => {
|
|
14221
|
-
return await this.strategyConnectionService.backtest(symbol, context, candles);
|
|
14386
|
+
return await this.strategyConnectionService.backtest(symbol, context, candles, frameEndTime);
|
|
14222
14387
|
}, {
|
|
14223
14388
|
symbol,
|
|
14224
14389
|
when,
|
|
@@ -16211,7 +16376,9 @@ const TICK_FN = async (self, symbol, when) => {
|
|
|
16211
16376
|
});
|
|
16212
16377
|
}
|
|
16213
16378
|
catch (error) {
|
|
16214
|
-
console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName}
|
|
16379
|
+
console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName} error=${getErrorMessage(error)}`, {
|
|
16380
|
+
error: errorData(error),
|
|
16381
|
+
});
|
|
16215
16382
|
self.loggerService.warn("backtestLogicPrivateService tick failed", {
|
|
16216
16383
|
symbol,
|
|
16217
16384
|
when: when.toISOString(),
|
|
@@ -16237,9 +16404,9 @@ const GET_CANDLES_FN = async (self, symbol, candlesNeeded, bufferStartTime, logM
|
|
|
16237
16404
|
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "GET_CANDLES_FN", message: getErrorMessage(error) };
|
|
16238
16405
|
}
|
|
16239
16406
|
};
|
|
16240
|
-
const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
|
|
16407
|
+
const BACKTEST_FN = async (self, symbol, candles, frameEndTime, when, context, logMeta) => {
|
|
16241
16408
|
try {
|
|
16242
|
-
return await self.strategyCoreService.backtest(symbol, candles, when, true, context);
|
|
16409
|
+
return await self.strategyCoreService.backtest(symbol, candles, frameEndTime, when, true, context);
|
|
16243
16410
|
}
|
|
16244
16411
|
catch (error) {
|
|
16245
16412
|
console.error(`backtestLogicPrivateService backtest failed symbol=${symbol} when=${when.toISOString()} strategyName=${context.strategyName} exchangeName=${context.exchangeName}`);
|
|
@@ -16252,7 +16419,29 @@ const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
|
|
|
16252
16419
|
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "BACKTEST_FN", message: getErrorMessage(error) };
|
|
16253
16420
|
}
|
|
16254
16421
|
};
|
|
16255
|
-
const
|
|
16422
|
+
const CLOSE_PENDING_FN = async (self, symbol, context, lastChunkCandles, frameEndTime, when, signalId) => {
|
|
16423
|
+
try {
|
|
16424
|
+
await self.strategyCoreService.closePending(true, symbol, context);
|
|
16425
|
+
}
|
|
16426
|
+
catch (error) {
|
|
16427
|
+
const message = `closePending failed: ${getErrorMessage(error)}`;
|
|
16428
|
+
console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
|
|
16429
|
+
await errorEmitter.next(error instanceof Error ? error : new Error(message));
|
|
16430
|
+
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
|
|
16431
|
+
}
|
|
16432
|
+
const result = await BACKTEST_FN(self, symbol, lastChunkCandles, frameEndTime, when, context, { signalId });
|
|
16433
|
+
if ("__error__" in result) {
|
|
16434
|
+
return result;
|
|
16435
|
+
}
|
|
16436
|
+
if (result.action === "active") {
|
|
16437
|
+
const message = `signal ${signalId} still active after closePending`;
|
|
16438
|
+
console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
|
|
16439
|
+
await errorEmitter.next(new Error(message));
|
|
16440
|
+
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
|
|
16441
|
+
}
|
|
16442
|
+
return result;
|
|
16443
|
+
};
|
|
16444
|
+
const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId, frameEndTime) => {
|
|
16256
16445
|
let backtestResult = initialResult;
|
|
16257
16446
|
const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
|
|
16258
16447
|
let lastChunkCandles = [];
|
|
@@ -16263,25 +16452,14 @@ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialRe
|
|
|
16263
16452
|
return chunkCandles;
|
|
16264
16453
|
}
|
|
16265
16454
|
if (!chunkCandles.length) {
|
|
16266
|
-
await self
|
|
16267
|
-
const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
|
|
16268
|
-
if ("__error__" in result) {
|
|
16269
|
-
return result;
|
|
16270
|
-
}
|
|
16271
|
-
if (result.action === "active") {
|
|
16272
|
-
const message = `signal ${signalId} still active after closePending`;
|
|
16273
|
-
console.error(`backtestLogicPrivateService RUN_INFINITY_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
|
|
16274
|
-
await errorEmitter.next(new Error(message));
|
|
16275
|
-
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_INFINITY_CHUNK_LOOP_FN", message };
|
|
16276
|
-
}
|
|
16277
|
-
return result;
|
|
16455
|
+
return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
|
|
16278
16456
|
}
|
|
16279
16457
|
self.loggerService.info("backtestLogicPrivateService candles fetched for infinity chunk", {
|
|
16280
16458
|
symbol,
|
|
16281
16459
|
signalId,
|
|
16282
16460
|
candlesCount: chunkCandles.length,
|
|
16283
16461
|
});
|
|
16284
|
-
const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
|
|
16462
|
+
const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
|
|
16285
16463
|
if ("__error__" in chunkResult) {
|
|
16286
16464
|
return chunkResult;
|
|
16287
16465
|
}
|
|
@@ -16326,7 +16504,7 @@ const EMIT_TIMEFRAME_PERFORMANCE_FN = async (self, symbol, timeframeStartTime, p
|
|
|
16326
16504
|
});
|
|
16327
16505
|
return currentTimestamp;
|
|
16328
16506
|
};
|
|
16329
|
-
const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
|
|
16507
|
+
const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
|
|
16330
16508
|
const signalStartTime = performance.now();
|
|
16331
16509
|
const signal = result.signal;
|
|
16332
16510
|
self.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
|
|
@@ -16346,6 +16524,10 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
|
|
|
16346
16524
|
console.error(`backtestLogicPrivateService scheduled signal: getCandles failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${candles.reason} message=${candles.message}`);
|
|
16347
16525
|
return candles;
|
|
16348
16526
|
}
|
|
16527
|
+
// No candles available for this scheduled signal — the frame ends before the signal
|
|
16528
|
+
// could be evaluated. Unlike pending (Infinity) signals that require CLOSE_PENDING_FN,
|
|
16529
|
+
// a scheduled signal that never activated needs no explicit cancellation: it simply
|
|
16530
|
+
// did not start. Returning "skip" moves the backtest to the next timeframe.
|
|
16349
16531
|
if (!candles.length) {
|
|
16350
16532
|
return { type: "skip" };
|
|
16351
16533
|
}
|
|
@@ -16387,7 +16569,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
|
|
|
16387
16569
|
});
|
|
16388
16570
|
}
|
|
16389
16571
|
try {
|
|
16390
|
-
const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
|
|
16572
|
+
const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
|
|
16391
16573
|
if ("__error__" in firstResult) {
|
|
16392
16574
|
console.error(`backtestLogicPrivateService scheduled signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
|
|
16393
16575
|
return firstResult;
|
|
@@ -16399,7 +16581,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
|
|
|
16399
16581
|
}
|
|
16400
16582
|
if (backtestResult.action === "active" && signal.minuteEstimatedTime === Infinity) {
|
|
16401
16583
|
const bufferMs = bufferMinutes * 60000;
|
|
16402
|
-
const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id);
|
|
16584
|
+
const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id, frameEndTime);
|
|
16403
16585
|
if ("__error__" in chunkResult) {
|
|
16404
16586
|
console.error(`backtestLogicPrivateService scheduled signal: infinity chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
|
|
16405
16587
|
return chunkResult;
|
|
@@ -16429,7 +16611,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
|
|
|
16429
16611
|
yield backtestResult;
|
|
16430
16612
|
return { type: "closed", previousEventTimestamp: newTimestamp, closeTimestamp: backtestResult.closeTimestamp, shouldStop };
|
|
16431
16613
|
};
|
|
16432
|
-
const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId) => {
|
|
16614
|
+
const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId, frameEndTime) => {
|
|
16433
16615
|
const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
|
|
16434
16616
|
let chunkStart = bufferStartTime;
|
|
16435
16617
|
let lastChunkCandles = [];
|
|
@@ -16450,29 +16632,14 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
|
|
|
16450
16632
|
await errorEmitter.next(new Error(message));
|
|
16451
16633
|
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
|
|
16452
16634
|
}
|
|
16453
|
-
await self
|
|
16454
|
-
const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
|
|
16455
|
-
if ("__error__" in result) {
|
|
16456
|
-
return result;
|
|
16457
|
-
}
|
|
16458
|
-
if (result.action === "active") {
|
|
16459
|
-
const message = `signal ${signalId} still active after closePending`;
|
|
16460
|
-
console.error(`backtestLogicPrivateService RUN_OPENED_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
|
|
16461
|
-
self.loggerService.warn("backtestLogicPrivateService opened infinity: signal still active after closePending", {
|
|
16462
|
-
symbol,
|
|
16463
|
-
signalId,
|
|
16464
|
-
});
|
|
16465
|
-
await errorEmitter.next(new Error(message));
|
|
16466
|
-
return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
|
|
16467
|
-
}
|
|
16468
|
-
return result;
|
|
16635
|
+
return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
|
|
16469
16636
|
}
|
|
16470
16637
|
self.loggerService.info("backtestLogicPrivateService candles fetched", {
|
|
16471
16638
|
symbol,
|
|
16472
16639
|
signalId,
|
|
16473
16640
|
candlesCount: chunkCandles.length,
|
|
16474
16641
|
});
|
|
16475
|
-
const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
|
|
16642
|
+
const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
|
|
16476
16643
|
if ("__error__" in chunkResult) {
|
|
16477
16644
|
return chunkResult;
|
|
16478
16645
|
}
|
|
@@ -16483,7 +16650,7 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
|
|
|
16483
16650
|
chunkStart = new Date(chunkResult._backtestLastTimestamp + 60000 - bufferMs);
|
|
16484
16651
|
}
|
|
16485
16652
|
};
|
|
16486
|
-
const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
|
|
16653
|
+
const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
|
|
16487
16654
|
const signalStartTime = performance.now();
|
|
16488
16655
|
const signal = result.signal;
|
|
16489
16656
|
self.loggerService.info("backtestLogicPrivateService signal opened", {
|
|
@@ -16514,7 +16681,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
|
|
|
16514
16681
|
signalId: signal.id,
|
|
16515
16682
|
candlesCount: candles.length,
|
|
16516
16683
|
});
|
|
16517
|
-
const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
|
|
16684
|
+
const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
|
|
16518
16685
|
if ("__error__" in firstResult) {
|
|
16519
16686
|
console.error(`backtestLogicPrivateService opened signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
|
|
16520
16687
|
return firstResult;
|
|
@@ -16523,7 +16690,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
|
|
|
16523
16690
|
}
|
|
16524
16691
|
else {
|
|
16525
16692
|
const bufferMs = bufferMinutes * 60000;
|
|
16526
|
-
const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id);
|
|
16693
|
+
const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id, frameEndTime);
|
|
16527
16694
|
if ("__error__" in chunkResult) {
|
|
16528
16695
|
console.error(`backtestLogicPrivateService opened signal: chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
|
|
16529
16696
|
return chunkResult;
|
|
@@ -16589,86 +16756,103 @@ class BacktestLogicPrivateService {
|
|
|
16589
16756
|
symbol,
|
|
16590
16757
|
});
|
|
16591
16758
|
const backtestStartTime = performance.now();
|
|
16759
|
+
let _fatalError = null;
|
|
16760
|
+
let previousEventTimestamp = null;
|
|
16592
16761
|
const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
|
|
16593
16762
|
const totalFrames = timeframes.length;
|
|
16763
|
+
let frameEndTime = timeframes[totalFrames - 1].getTime();
|
|
16594
16764
|
let i = 0;
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
break;
|
|
16602
|
-
}
|
|
16603
|
-
const result = await TICK_FN(this, symbol, when);
|
|
16604
|
-
if ("__error__" in result) {
|
|
16605
|
-
break;
|
|
16606
|
-
}
|
|
16607
|
-
if (result.action === "idle" &&
|
|
16608
|
-
await and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
|
|
16609
|
-
strategyName: this.methodContextService.context.strategyName,
|
|
16610
|
-
exchangeName: this.methodContextService.context.exchangeName,
|
|
16611
|
-
frameName: this.methodContextService.context.frameName,
|
|
16612
|
-
}))) {
|
|
16613
|
-
this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
|
|
16614
|
-
symbol,
|
|
16615
|
-
when: when.toISOString(),
|
|
16616
|
-
processedFrames: i,
|
|
16617
|
-
totalFrames,
|
|
16618
|
-
});
|
|
16619
|
-
break;
|
|
16620
|
-
}
|
|
16621
|
-
if (result.action === "scheduled") {
|
|
16622
|
-
yield result;
|
|
16623
|
-
const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
|
|
16624
|
-
if (r.type === "error") {
|
|
16765
|
+
try {
|
|
16766
|
+
while (i < timeframes.length) {
|
|
16767
|
+
const timeframeStartTime = performance.now();
|
|
16768
|
+
const when = timeframes[i];
|
|
16769
|
+
await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
|
|
16770
|
+
if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
|
|
16625
16771
|
break;
|
|
16626
16772
|
}
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
}
|
|
16632
|
-
if (r.shouldStop) {
|
|
16633
|
-
break;
|
|
16634
|
-
}
|
|
16773
|
+
const result = await TICK_FN(this, symbol, when);
|
|
16774
|
+
if ("__error__" in result) {
|
|
16775
|
+
_fatalError = new Error(`[${result.reason}] ${result.message}`);
|
|
16776
|
+
break;
|
|
16635
16777
|
}
|
|
16636
|
-
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16778
|
+
if (result.action === "idle" &&
|
|
16779
|
+
await and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
|
|
16780
|
+
strategyName: this.methodContextService.context.strategyName,
|
|
16781
|
+
exchangeName: this.methodContextService.context.exchangeName,
|
|
16782
|
+
frameName: this.methodContextService.context.frameName,
|
|
16783
|
+
}))) {
|
|
16784
|
+
this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
|
|
16785
|
+
symbol,
|
|
16786
|
+
when: when.toISOString(),
|
|
16787
|
+
processedFrames: i,
|
|
16788
|
+
totalFrames,
|
|
16789
|
+
});
|
|
16641
16790
|
break;
|
|
16642
16791
|
}
|
|
16643
|
-
if (
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16792
|
+
if (result.action === "scheduled") {
|
|
16793
|
+
yield result;
|
|
16794
|
+
const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
|
|
16795
|
+
if (r.type === "error") {
|
|
16796
|
+
_fatalError = new Error(`[${r.reason}] ${r.message}`);
|
|
16797
|
+
break;
|
|
16647
16798
|
}
|
|
16648
|
-
if (r.
|
|
16799
|
+
if (r.type === "closed") {
|
|
16800
|
+
previousEventTimestamp = r.previousEventTimestamp;
|
|
16801
|
+
while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
|
|
16802
|
+
i++;
|
|
16803
|
+
}
|
|
16804
|
+
if (r.shouldStop) {
|
|
16805
|
+
break;
|
|
16806
|
+
}
|
|
16807
|
+
}
|
|
16808
|
+
}
|
|
16809
|
+
if (result.action === "opened") {
|
|
16810
|
+
yield result;
|
|
16811
|
+
const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
|
|
16812
|
+
if (r.type === "error") {
|
|
16813
|
+
_fatalError = new Error(`[${r.reason}] ${r.message}`);
|
|
16649
16814
|
break;
|
|
16650
16815
|
}
|
|
16816
|
+
if (r.type === "closed") {
|
|
16817
|
+
previousEventTimestamp = r.previousEventTimestamp;
|
|
16818
|
+
while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
|
|
16819
|
+
i++;
|
|
16820
|
+
}
|
|
16821
|
+
if (r.shouldStop) {
|
|
16822
|
+
break;
|
|
16823
|
+
}
|
|
16824
|
+
}
|
|
16651
16825
|
}
|
|
16826
|
+
previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
|
|
16827
|
+
i++;
|
|
16652
16828
|
}
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
16829
|
+
// Emit final progress event (100%)
|
|
16830
|
+
await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
|
|
16831
|
+
// Track total backtest duration
|
|
16832
|
+
const backtestEndTime = performance.now();
|
|
16833
|
+
const currentTimestamp = Date.now();
|
|
16834
|
+
await performanceEmitter.next({
|
|
16835
|
+
timestamp: currentTimestamp,
|
|
16836
|
+
previousTimestamp: previousEventTimestamp,
|
|
16837
|
+
metricType: "backtest_total",
|
|
16838
|
+
duration: backtestEndTime - backtestStartTime,
|
|
16839
|
+
strategyName: this.methodContextService.context.strategyName,
|
|
16840
|
+
exchangeName: this.methodContextService.context.exchangeName,
|
|
16841
|
+
frameName: this.methodContextService.context.frameName,
|
|
16842
|
+
symbol,
|
|
16843
|
+
backtest: true,
|
|
16844
|
+
});
|
|
16845
|
+
}
|
|
16846
|
+
catch (error) {
|
|
16847
|
+
_fatalError = error;
|
|
16848
|
+
}
|
|
16849
|
+
finally {
|
|
16850
|
+
if (_fatalError !== null) {
|
|
16851
|
+
console.error(`[BacktestLogicPrivateService] Fatal error — backtest sequence broken for symbol=${symbol} ` +
|
|
16852
|
+
`strategy=${this.methodContextService.context.strategyName}`, _fatalError);
|
|
16853
|
+
process.exit(-1);
|
|
16854
|
+
}
|
|
16855
|
+
}
|
|
16672
16856
|
}
|
|
16673
16857
|
}
|
|
16674
16858
|
|
|
@@ -16685,6 +16869,8 @@ const INTERVAL_MINUTES$3 = {
|
|
|
16685
16869
|
"4h": 240,
|
|
16686
16870
|
"6h": 360,
|
|
16687
16871
|
"8h": 480,
|
|
16872
|
+
"1d": 1440,
|
|
16873
|
+
"1w": 10080,
|
|
16688
16874
|
};
|
|
16689
16875
|
const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
|
|
16690
16876
|
const tickSubject = new Subject();
|
|
@@ -31521,6 +31707,8 @@ const INTERVAL_MINUTES$2 = {
|
|
|
31521
31707
|
"4h": 240,
|
|
31522
31708
|
"6h": 360,
|
|
31523
31709
|
"8h": 480,
|
|
31710
|
+
"1d": 1440,
|
|
31711
|
+
"1w": 10080,
|
|
31524
31712
|
};
|
|
31525
31713
|
/**
|
|
31526
31714
|
* Aligns timestamp down to the nearest interval boundary.
|
|
@@ -32270,6 +32458,8 @@ const INTERVAL_MINUTES$1 = {
|
|
|
32270
32458
|
"4h": 240,
|
|
32271
32459
|
"6h": 360,
|
|
32272
32460
|
"8h": 480,
|
|
32461
|
+
"1d": 1440,
|
|
32462
|
+
"1w": 10080,
|
|
32273
32463
|
};
|
|
32274
32464
|
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
32275
32465
|
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
@@ -33324,54 +33514,144 @@ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommi
|
|
|
33324
33514
|
const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
|
|
33325
33515
|
const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
|
|
33326
33516
|
const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
|
|
33517
|
+
/**
|
|
33518
|
+
* Wrapper around a `Partial<IBroker>` adapter instance.
|
|
33519
|
+
*
|
|
33520
|
+
* Implements the full `IBroker` interface but guards every method call —
|
|
33521
|
+
* if the underlying adapter does not implement a given method, an error is thrown.
|
|
33522
|
+
* `waitForInit` is the only exception: it is silently skipped when not implemented.
|
|
33523
|
+
*
|
|
33524
|
+
* Created internally by `BrokerAdapter.useBrokerAdapter` and stored as
|
|
33525
|
+
* `_brokerInstance`. All `BrokerAdapter.commit*` methods delegate here
|
|
33526
|
+
* after backtest-mode and enable-state checks pass.
|
|
33527
|
+
*/
|
|
33327
33528
|
class BrokerProxy {
|
|
33328
33529
|
constructor(_instance) {
|
|
33329
33530
|
this._instance = _instance;
|
|
33531
|
+
/**
|
|
33532
|
+
* Calls `waitForInit` on the underlying adapter exactly once (singleshot).
|
|
33533
|
+
* If the adapter does not implement `waitForInit`, the call is silently skipped.
|
|
33534
|
+
*
|
|
33535
|
+
* @returns Resolves when initialization is complete (or immediately if not implemented).
|
|
33536
|
+
*/
|
|
33330
33537
|
this.waitForInit = singleshot(async () => {
|
|
33331
33538
|
if (this._instance.waitForInit) {
|
|
33332
33539
|
await this._instance.waitForInit();
|
|
33540
|
+
return;
|
|
33333
33541
|
}
|
|
33334
33542
|
});
|
|
33335
33543
|
}
|
|
33544
|
+
/**
|
|
33545
|
+
* Forwards a signal-open event to the underlying adapter.
|
|
33546
|
+
* Throws if the adapter does not implement `onSignalOpenCommit`.
|
|
33547
|
+
*
|
|
33548
|
+
* @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag.
|
|
33549
|
+
* @throws {Error} If the adapter does not implement `onSignalOpenCommit`.
|
|
33550
|
+
*/
|
|
33336
33551
|
async onSignalOpenCommit(payload) {
|
|
33337
33552
|
if (this._instance.onSignalOpenCommit) {
|
|
33338
33553
|
await this._instance.onSignalOpenCommit(payload);
|
|
33554
|
+
return;
|
|
33339
33555
|
}
|
|
33556
|
+
throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
|
|
33340
33557
|
}
|
|
33558
|
+
/**
|
|
33559
|
+
* Forwards a signal-close event to the underlying adapter.
|
|
33560
|
+
* Throws if the adapter does not implement `onSignalCloseCommit`.
|
|
33561
|
+
*
|
|
33562
|
+
* @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag.
|
|
33563
|
+
* @throws {Error} If the adapter does not implement `onSignalCloseCommit`.
|
|
33564
|
+
*/
|
|
33341
33565
|
async onSignalCloseCommit(payload) {
|
|
33342
33566
|
if (this._instance.onSignalCloseCommit) {
|
|
33343
33567
|
await this._instance.onSignalCloseCommit(payload);
|
|
33568
|
+
return;
|
|
33344
33569
|
}
|
|
33570
|
+
throw new Error("BrokerProxy onSignalCloseCommit is not implemented");
|
|
33345
33571
|
}
|
|
33572
|
+
/**
|
|
33573
|
+
* Forwards a partial-profit close event to the underlying adapter.
|
|
33574
|
+
* Throws if the adapter does not implement `onPartialProfitCommit`.
|
|
33575
|
+
*
|
|
33576
|
+
* @param payload - Partial profit details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
|
|
33577
|
+
* @throws {Error} If the adapter does not implement `onPartialProfitCommit`.
|
|
33578
|
+
*/
|
|
33346
33579
|
async onPartialProfitCommit(payload) {
|
|
33347
33580
|
if (this._instance.onPartialProfitCommit) {
|
|
33348
33581
|
await this._instance.onPartialProfitCommit(payload);
|
|
33582
|
+
return;
|
|
33349
33583
|
}
|
|
33584
|
+
throw new Error("BrokerProxy onPartialProfitCommit is not implemented");
|
|
33350
33585
|
}
|
|
33586
|
+
/**
|
|
33587
|
+
* Forwards a partial-loss close event to the underlying adapter.
|
|
33588
|
+
* Throws if the adapter does not implement `onPartialLossCommit`.
|
|
33589
|
+
*
|
|
33590
|
+
* @param payload - Partial loss details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
|
|
33591
|
+
* @throws {Error} If the adapter does not implement `onPartialLossCommit`.
|
|
33592
|
+
*/
|
|
33351
33593
|
async onPartialLossCommit(payload) {
|
|
33352
33594
|
if (this._instance.onPartialLossCommit) {
|
|
33353
33595
|
await this._instance.onPartialLossCommit(payload);
|
|
33596
|
+
return;
|
|
33354
33597
|
}
|
|
33598
|
+
throw new Error("BrokerProxy onPartialLossCommit is not implemented");
|
|
33355
33599
|
}
|
|
33600
|
+
/**
|
|
33601
|
+
* Forwards a trailing stop-loss update event to the underlying adapter.
|
|
33602
|
+
* Throws if the adapter does not implement `onTrailingStopCommit`.
|
|
33603
|
+
*
|
|
33604
|
+
* @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag.
|
|
33605
|
+
* @throws {Error} If the adapter does not implement `onTrailingStopCommit`.
|
|
33606
|
+
*/
|
|
33356
33607
|
async onTrailingStopCommit(payload) {
|
|
33357
33608
|
if (this._instance.onTrailingStopCommit) {
|
|
33358
33609
|
await this._instance.onTrailingStopCommit(payload);
|
|
33610
|
+
return;
|
|
33359
33611
|
}
|
|
33612
|
+
throw new Error("BrokerProxy onTrailingStopCommit is not implemented");
|
|
33360
33613
|
}
|
|
33614
|
+
/**
|
|
33615
|
+
* Forwards a trailing take-profit update event to the underlying adapter.
|
|
33616
|
+
* Throws if the adapter does not implement `onTrailingTakeCommit`.
|
|
33617
|
+
*
|
|
33618
|
+
* @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag.
|
|
33619
|
+
* @throws {Error} If the adapter does not implement `onTrailingTakeCommit`.
|
|
33620
|
+
*/
|
|
33361
33621
|
async onTrailingTakeCommit(payload) {
|
|
33362
33622
|
if (this._instance.onTrailingTakeCommit) {
|
|
33363
33623
|
await this._instance.onTrailingTakeCommit(payload);
|
|
33624
|
+
return;
|
|
33364
33625
|
}
|
|
33626
|
+
throw new Error("BrokerProxy onTrailingTakeCommit is not implemented");
|
|
33365
33627
|
}
|
|
33628
|
+
/**
|
|
33629
|
+
* Forwards a breakeven event to the underlying adapter.
|
|
33630
|
+
* Throws if the adapter does not implement `onBreakevenCommit`.
|
|
33631
|
+
*
|
|
33632
|
+
* @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice (= effectivePriceOpen), newTakeProfitPrice, context, backtest flag.
|
|
33633
|
+
* @throws {Error} If the adapter does not implement `onBreakevenCommit`.
|
|
33634
|
+
*/
|
|
33366
33635
|
async onBreakevenCommit(payload) {
|
|
33367
33636
|
if (this._instance.onBreakevenCommit) {
|
|
33368
33637
|
await this._instance.onBreakevenCommit(payload);
|
|
33638
|
+
return;
|
|
33369
33639
|
}
|
|
33640
|
+
throw new Error("BrokerProxy onBreakevenCommit is not implemented");
|
|
33370
33641
|
}
|
|
33642
|
+
/**
|
|
33643
|
+
* Forwards a DCA average-buy entry event to the underlying adapter.
|
|
33644
|
+
* Throws if the adapter does not implement `onAverageBuyCommit`.
|
|
33645
|
+
*
|
|
33646
|
+
* @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag.
|
|
33647
|
+
* @throws {Error} If the adapter does not implement `onAverageBuyCommit`.
|
|
33648
|
+
*/
|
|
33371
33649
|
async onAverageBuyCommit(payload) {
|
|
33372
33650
|
if (this._instance.onAverageBuyCommit) {
|
|
33373
33651
|
await this._instance.onAverageBuyCommit(payload);
|
|
33652
|
+
return;
|
|
33374
33653
|
}
|
|
33654
|
+
throw new Error("BrokerProxy onAverageBuyCommit is not implemented");
|
|
33375
33655
|
}
|
|
33376
33656
|
}
|
|
33377
33657
|
/**
|
|
@@ -43143,7 +43423,7 @@ class MemoryLocalInstance {
|
|
|
43143
43423
|
* @param value - Value to store and index
|
|
43144
43424
|
* @param index - Optional BM25 index string; defaults to JSON.stringify(value)
|
|
43145
43425
|
*/
|
|
43146
|
-
async writeMemory(memoryId, value,
|
|
43426
|
+
async writeMemory(memoryId, value, description) {
|
|
43147
43427
|
bt.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_WRITE, {
|
|
43148
43428
|
signalId: this.signalId,
|
|
43149
43429
|
bucketName: this.bucketName,
|
|
@@ -43152,7 +43432,7 @@ class MemoryLocalInstance {
|
|
|
43152
43432
|
this._index.upsert({
|
|
43153
43433
|
id: memoryId,
|
|
43154
43434
|
content: value,
|
|
43155
|
-
index:
|
|
43435
|
+
index: description,
|
|
43156
43436
|
priority: Date.now(),
|
|
43157
43437
|
});
|
|
43158
43438
|
}
|
|
@@ -43451,7 +43731,7 @@ class MemoryAdapter {
|
|
|
43451
43731
|
* @param dto.value - Value to store
|
|
43452
43732
|
* @param dto.signalId - Signal identifier
|
|
43453
43733
|
* @param dto.bucketName - Bucket name
|
|
43454
|
-
* @param dto.
|
|
43734
|
+
* @param dto.description - Optional BM25 index string; defaults to JSON.stringify(value)
|
|
43455
43735
|
*/
|
|
43456
43736
|
this.writeMemory = async (dto) => {
|
|
43457
43737
|
if (!this.enable.hasValue()) {
|
|
@@ -43466,7 +43746,7 @@ class MemoryAdapter {
|
|
|
43466
43746
|
const isInitial = !this.getInstance.has(key);
|
|
43467
43747
|
const instance = this.getInstance(dto.signalId, dto.bucketName);
|
|
43468
43748
|
await instance.waitForInit(isInitial);
|
|
43469
|
-
return await instance.writeMemory(dto.memoryId, dto.value, dto.
|
|
43749
|
+
return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
|
|
43470
43750
|
};
|
|
43471
43751
|
/**
|
|
43472
43752
|
* Search memory using BM25 full-text scoring.
|
|
@@ -43617,7 +43897,7 @@ const REMOVE_MEMORY_METHOD_NAME = "memory.removeMemory";
|
|
|
43617
43897
|
* ```
|
|
43618
43898
|
*/
|
|
43619
43899
|
async function writeMemory(dto) {
|
|
43620
|
-
const { bucketName, memoryId, value } = dto;
|
|
43900
|
+
const { bucketName, memoryId, value, description } = dto;
|
|
43621
43901
|
bt.loggerService.info(WRITE_MEMORY_METHOD_NAME, {
|
|
43622
43902
|
bucketName,
|
|
43623
43903
|
memoryId,
|
|
@@ -43641,6 +43921,7 @@ async function writeMemory(dto) {
|
|
|
43641
43921
|
value,
|
|
43642
43922
|
signalId: signal.id,
|
|
43643
43923
|
bucketName,
|
|
43924
|
+
description,
|
|
43644
43925
|
});
|
|
43645
43926
|
}
|
|
43646
43927
|
/**
|
|
@@ -44084,7 +44365,7 @@ class DumpMemoryInstance {
|
|
|
44084
44365
|
bucketName: this.bucketName,
|
|
44085
44366
|
signalId: this.signalId,
|
|
44086
44367
|
value: { messages },
|
|
44087
|
-
|
|
44368
|
+
description,
|
|
44088
44369
|
});
|
|
44089
44370
|
}
|
|
44090
44371
|
/**
|
|
@@ -44105,7 +44386,7 @@ class DumpMemoryInstance {
|
|
|
44105
44386
|
bucketName: this.bucketName,
|
|
44106
44387
|
signalId: this.signalId,
|
|
44107
44388
|
value: record,
|
|
44108
|
-
|
|
44389
|
+
description,
|
|
44109
44390
|
});
|
|
44110
44391
|
}
|
|
44111
44392
|
/**
|
|
@@ -44127,7 +44408,7 @@ class DumpMemoryInstance {
|
|
|
44127
44408
|
bucketName: this.bucketName,
|
|
44128
44409
|
signalId: this.signalId,
|
|
44129
44410
|
value: { rows },
|
|
44130
|
-
|
|
44411
|
+
description,
|
|
44131
44412
|
});
|
|
44132
44413
|
}
|
|
44133
44414
|
/**
|
|
@@ -44148,7 +44429,7 @@ class DumpMemoryInstance {
|
|
|
44148
44429
|
bucketName: this.bucketName,
|
|
44149
44430
|
signalId: this.signalId,
|
|
44150
44431
|
value: { content },
|
|
44151
|
-
|
|
44432
|
+
description,
|
|
44152
44433
|
});
|
|
44153
44434
|
}
|
|
44154
44435
|
/**
|
|
@@ -44169,7 +44450,7 @@ class DumpMemoryInstance {
|
|
|
44169
44450
|
bucketName: this.bucketName,
|
|
44170
44451
|
signalId: this.signalId,
|
|
44171
44452
|
value: { content },
|
|
44172
|
-
|
|
44453
|
+
description,
|
|
44173
44454
|
});
|
|
44174
44455
|
}
|
|
44175
44456
|
/**
|
|
@@ -44191,7 +44472,7 @@ class DumpMemoryInstance {
|
|
|
44191
44472
|
bucketName: this.bucketName,
|
|
44192
44473
|
signalId: this.signalId,
|
|
44193
44474
|
value: json,
|
|
44194
|
-
|
|
44475
|
+
description,
|
|
44195
44476
|
});
|
|
44196
44477
|
}
|
|
44197
44478
|
/** Releases resources held by this instance. */
|
|
@@ -50614,6 +50895,8 @@ const INTERVAL_MINUTES = {
|
|
|
50614
50895
|
"4h": 240,
|
|
50615
50896
|
"6h": 360,
|
|
50616
50897
|
"8h": 480,
|
|
50898
|
+
"1d": 1440,
|
|
50899
|
+
"1w": 10080,
|
|
50617
50900
|
};
|
|
50618
50901
|
/**
|
|
50619
50902
|
* Aligns timestamp down to the nearest interval boundary.
|
|
@@ -51688,4 +51971,117 @@ const percentValue = (yesterdayValue, todayValue) => {
|
|
|
51688
51971
|
return yesterdayValue / todayValue - 1;
|
|
51689
51972
|
};
|
|
51690
51973
|
|
|
51691
|
-
|
|
51974
|
+
/**
|
|
51975
|
+
* Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
|
|
51976
|
+
*
|
|
51977
|
+
* When priceOpen is provided:
|
|
51978
|
+
* - If currentPrice already reached priceOpen (shouldActivateImmediately) →
|
|
51979
|
+
* validates as pending: currentPrice must be between SL and TP
|
|
51980
|
+
* - Otherwise → validates as scheduled: priceOpen must be between SL and TP
|
|
51981
|
+
*
|
|
51982
|
+
* When priceOpen is absent:
|
|
51983
|
+
* - Validates as pending: currentPrice must be between SL and TP
|
|
51984
|
+
*
|
|
51985
|
+
* Checks:
|
|
51986
|
+
* - currentPrice is a finite positive number
|
|
51987
|
+
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
51988
|
+
* - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
|
|
51989
|
+
*
|
|
51990
|
+
* @param signal - Signal DTO returned by getSignal
|
|
51991
|
+
* @param currentPrice - Current market price at the moment of signal creation
|
|
51992
|
+
* @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
|
|
51993
|
+
*/
|
|
51994
|
+
const validateSignal = (signal, currentPrice) => {
|
|
51995
|
+
const errors = [];
|
|
51996
|
+
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
51997
|
+
{
|
|
51998
|
+
if (typeof currentPrice !== "number") {
|
|
51999
|
+
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
52000
|
+
}
|
|
52001
|
+
if (!isFinite(currentPrice)) {
|
|
52002
|
+
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
52003
|
+
}
|
|
52004
|
+
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
52005
|
+
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
52006
|
+
}
|
|
52007
|
+
}
|
|
52008
|
+
if (errors.length > 0) {
|
|
52009
|
+
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
52010
|
+
return false;
|
|
52011
|
+
}
|
|
52012
|
+
try {
|
|
52013
|
+
validateCommonSignal(signal);
|
|
52014
|
+
}
|
|
52015
|
+
catch (error) {
|
|
52016
|
+
console.error(getErrorMessage(error));
|
|
52017
|
+
return false;
|
|
52018
|
+
}
|
|
52019
|
+
// Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
|
|
52020
|
+
// - нет priceOpen → pending (открывается по currentPrice)
|
|
52021
|
+
// - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
|
|
52022
|
+
// - priceOpen задан и ещё не достигнут → scheduled
|
|
52023
|
+
const hasPriceOpen = signal.priceOpen !== undefined;
|
|
52024
|
+
const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
|
|
52025
|
+
(signal.position === "short" && currentPrice >= signal.priceOpen));
|
|
52026
|
+
const isScheduled = hasPriceOpen && !shouldActivateImmediately;
|
|
52027
|
+
if (isScheduled) {
|
|
52028
|
+
// Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
|
|
52029
|
+
if (signal.position === "long") {
|
|
52030
|
+
if (isFinite(signal.priceOpen)) {
|
|
52031
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
52032
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
52033
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
52034
|
+
}
|
|
52035
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
52036
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
52037
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
52038
|
+
}
|
|
52039
|
+
}
|
|
52040
|
+
}
|
|
52041
|
+
if (signal.position === "short") {
|
|
52042
|
+
if (isFinite(signal.priceOpen)) {
|
|
52043
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
52044
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
52045
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
52046
|
+
}
|
|
52047
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
52048
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
52049
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
52050
|
+
}
|
|
52051
|
+
}
|
|
52052
|
+
}
|
|
52053
|
+
}
|
|
52054
|
+
else {
|
|
52055
|
+
// Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
|
|
52056
|
+
if (signal.position === "long") {
|
|
52057
|
+
if (isFinite(currentPrice)) {
|
|
52058
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
52059
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
52060
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
52061
|
+
}
|
|
52062
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
52063
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
52064
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
52065
|
+
}
|
|
52066
|
+
}
|
|
52067
|
+
}
|
|
52068
|
+
if (signal.position === "short") {
|
|
52069
|
+
if (isFinite(currentPrice)) {
|
|
52070
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
52071
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
52072
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
52073
|
+
}
|
|
52074
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
52075
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
52076
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
52077
|
+
}
|
|
52078
|
+
}
|
|
52079
|
+
}
|
|
52080
|
+
}
|
|
52081
|
+
if (errors.length > 0) {
|
|
52082
|
+
console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
|
|
52083
|
+
}
|
|
52084
|
+
return !errors.length;
|
|
52085
|
+
};
|
|
52086
|
+
|
|
52087
|
+
export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, Memory, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistMemoryAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, searchMemory, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, warmCandles, writeMemory };
|