backtest-kit 5.10.0 → 6.0.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/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
- VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
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
- VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
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
- VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
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 (signal.position === "long") {
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 (signal.position === "long") {
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
  }
@@ -8679,12 +8819,15 @@ class ClientStrategy {
8679
8819
  if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
8680
8820
  return false;
8681
8821
  // Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
8682
- const currentTrailingSL = signal._trailingPriceStopLoss;
8683
- if (currentTrailingSL !== undefined) {
8684
- if (signal.position === "long" && newStopLoss <= currentTrailingSL)
8685
- return false;
8686
- if (signal.position === "short" && newStopLoss >= currentTrailingSL)
8687
- return false;
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
+ }
8688
8831
  }
8689
8832
  return true;
8690
8833
  }
@@ -8926,12 +9069,15 @@ class ClientStrategy {
8926
9069
  if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
8927
9070
  return false;
8928
9071
  // Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
8929
- const currentTrailingTP = signal._trailingPriceTakeProfit;
8930
- if (currentTrailingTP !== undefined) {
8931
- if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
8932
- return false;
8933
- if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
8934
- return false;
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
+ }
8935
9081
  }
8936
9082
  return true;
8937
9083
  }
@@ -10955,7 +11101,8 @@ const INTERVAL_MINUTES$5 = {
10955
11101
  "8h": 480,
10956
11102
  "12h": 720,
10957
11103
  "1d": 1440,
10958
- "3d": 4320,
11104
+ "1w": 10080,
11105
+ "1M": 43200,
10959
11106
  };
10960
11107
  /**
10961
11108
  * Wrapper to call onTimeframe callback with error handling.
@@ -11374,6 +11521,8 @@ const INTERVAL_MINUTES$4 = {
11374
11521
  "4h": 240,
11375
11522
  "6h": 360,
11376
11523
  "8h": 480,
11524
+ "1d": 1440,
11525
+ "1w": 10080,
11377
11526
  };
11378
11527
  /**
11379
11528
  * Aligns timestamp down to the nearest interval boundary.
@@ -16720,6 +16869,8 @@ const INTERVAL_MINUTES$3 = {
16720
16869
  "4h": 240,
16721
16870
  "6h": 360,
16722
16871
  "8h": 480,
16872
+ "1d": 1440,
16873
+ "1w": 10080,
16723
16874
  };
16724
16875
  const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
16725
16876
  const tickSubject = new Subject();
@@ -31556,6 +31707,8 @@ const INTERVAL_MINUTES$2 = {
31556
31707
  "4h": 240,
31557
31708
  "6h": 360,
31558
31709
  "8h": 480,
31710
+ "1d": 1440,
31711
+ "1w": 10080,
31559
31712
  };
31560
31713
  /**
31561
31714
  * Aligns timestamp down to the nearest interval boundary.
@@ -32305,6 +32458,8 @@ const INTERVAL_MINUTES$1 = {
32305
32458
  "4h": 240,
32306
32459
  "6h": 360,
32307
32460
  "8h": 480,
32461
+ "1d": 1440,
32462
+ "1w": 10080,
32308
32463
  };
32309
32464
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
32310
32465
  const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
@@ -50740,6 +50895,8 @@ const INTERVAL_MINUTES = {
50740
50895
  "4h": 240,
50741
50896
  "6h": 360,
50742
50897
  "8h": 480,
50898
+ "1d": 1440,
50899
+ "1w": 10080,
50743
50900
  };
50744
50901
  /**
50745
50902
  * Aligns timestamp down to the nearest interval boundary.
@@ -51814,4 +51971,117 @@ const percentValue = (yesterdayValue, todayValue) => {
51814
51971
  return yesterdayValue / todayValue - 1;
51815
51972
  };
51816
51973
 
51817
- 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, waitForCandle, warmCandles, writeMemory };
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 };