backtest-kit 5.10.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/build/index.cjs CHANGED
@@ -575,6 +575,16 @@ const GLOBAL_CONFIG = {
575
575
  * Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
576
576
  */
577
577
  CC_ENABLE_PPPL_EVERYWHERE: false,
578
+ /**
579
+ * Enables trailing logic (Trailing Take / Trailing Stop) without requiring absorption conditions.
580
+ * Allows trailing mechanisms to be activated regardless of whether absorption has been detected.
581
+ *
582
+ * This can lead to earlier or more frequent trailing activation, improving reactivity to price movement,
583
+ * but may increase sensitivity to noise and result in premature exits.
584
+ *
585
+ * Default: false (trailing logic is applied only when absorption conditions are met)
586
+ */
587
+ CC_ENABLE_TRAILING_EVERYWHERE: false,
578
588
  /**
579
589
  * Cost of entering a position (in USD).
580
590
  * This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
@@ -889,6 +899,8 @@ const INTERVAL_MINUTES$8 = {
889
899
  "4h": 240,
890
900
  "6h": 360,
891
901
  "8h": 480,
902
+ "1d": 1440,
903
+ "1w": 10080,
892
904
  };
893
905
  const MS_PER_MINUTE$6 = 60000;
894
906
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
@@ -2642,6 +2654,8 @@ const INTERVAL_MINUTES$7 = {
2642
2654
  "4h": 240,
2643
2655
  "6h": 360,
2644
2656
  "8h": 480,
2657
+ "1d": 1440,
2658
+ "1w": 10080,
2645
2659
  };
2646
2660
  /**
2647
2661
  * Aligns timestamp down to the nearest interval boundary.
@@ -3928,6 +3942,390 @@ const beginTime = (run) => (...args) => {
3928
3942
  return fn();
3929
3943
  };
3930
3944
 
3945
+ /**
3946
+ * Validates the common fields of ISignalDto that apply to both pending and scheduled signals.
3947
+ *
3948
+ * Checks:
3949
+ * - position is "long" or "short"
3950
+ * - priceOpen, priceTakeProfit, priceStopLoss are finite positive numbers
3951
+ * - price relationships are correct for position direction (TP/SL on correct sides of priceOpen)
3952
+ * - TP/SL distance constraints from GLOBAL_CONFIG
3953
+ * - minuteEstimatedTime is valid
3954
+ *
3955
+ * Does NOT check:
3956
+ * - currentPrice vs SL/TP (immediate close protection — handled by pending/scheduled validators)
3957
+ * - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled, scheduledAt, pendingAt
3958
+ *
3959
+ * @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
3960
+ *
3961
+ * @param signal - Signal DTO to validate
3962
+ * @returns Array of error strings (empty if valid)
3963
+ */
3964
+ const validateCommonSignal = (signal) => {
3965
+ const errors = [];
3966
+ // Валидация position
3967
+ {
3968
+ if (signal.position === undefined || signal.position === null) {
3969
+ errors.push('position is required and must be "long" or "short"');
3970
+ }
3971
+ if (signal.position !== "long" && signal.position !== "short") {
3972
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
3973
+ }
3974
+ }
3975
+ // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
3976
+ {
3977
+ if (typeof signal.priceOpen !== "number") {
3978
+ errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
3979
+ }
3980
+ if (!isFinite(signal.priceOpen)) {
3981
+ errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
3982
+ }
3983
+ if (typeof signal.priceTakeProfit !== "number") {
3984
+ errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
3985
+ }
3986
+ if (!isFinite(signal.priceTakeProfit)) {
3987
+ errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
3988
+ }
3989
+ if (typeof signal.priceStopLoss !== "number") {
3990
+ errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
3991
+ }
3992
+ if (!isFinite(signal.priceStopLoss)) {
3993
+ errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
3994
+ }
3995
+ }
3996
+ // Валидация цен (только если они конечные)
3997
+ {
3998
+ if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
3999
+ errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
4000
+ }
4001
+ if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
4002
+ errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
4003
+ }
4004
+ if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
4005
+ errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
4006
+ }
4007
+ }
4008
+ // Валидация для long позиции
4009
+ if (signal.position === "long") {
4010
+ // Проверка соотношения цен для long
4011
+ {
4012
+ if (signal.priceTakeProfit <= signal.priceOpen) {
4013
+ errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
4014
+ }
4015
+ if (signal.priceStopLoss >= signal.priceOpen) {
4016
+ errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
4017
+ }
4018
+ }
4019
+ // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
4020
+ {
4021
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4022
+ const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
4023
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4024
+ errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
4025
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
4026
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
4027
+ }
4028
+ }
4029
+ }
4030
+ // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
4031
+ {
4032
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4033
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
4034
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4035
+ errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4036
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
4037
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4038
+ }
4039
+ }
4040
+ }
4041
+ // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
4042
+ {
4043
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4044
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
4045
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4046
+ errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4047
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
4048
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4049
+ }
4050
+ }
4051
+ }
4052
+ }
4053
+ // Валидация для short позиции
4054
+ if (signal.position === "short") {
4055
+ // Проверка соотношения цен для short
4056
+ {
4057
+ if (signal.priceTakeProfit >= signal.priceOpen) {
4058
+ errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
4059
+ }
4060
+ if (signal.priceStopLoss <= signal.priceOpen) {
4061
+ errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
4062
+ }
4063
+ }
4064
+ // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
4065
+ {
4066
+ if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4067
+ const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
4068
+ if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4069
+ errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
4070
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
4071
+ `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
4072
+ }
4073
+ }
4074
+ }
4075
+ // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
4076
+ {
4077
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4078
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
4079
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4080
+ errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4081
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
4082
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4083
+ }
4084
+ }
4085
+ }
4086
+ // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
4087
+ {
4088
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4089
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
4090
+ if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4091
+ errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4092
+ `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
4093
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4094
+ }
4095
+ }
4096
+ }
4097
+ }
4098
+ // Валидация временных параметров
4099
+ {
4100
+ if (typeof signal.minuteEstimatedTime !== "number") {
4101
+ errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
4102
+ }
4103
+ if (signal.minuteEstimatedTime <= 0) {
4104
+ errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
4105
+ }
4106
+ if (signal.minuteEstimatedTime === Infinity && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity) {
4107
+ errors.push(`minuteEstimatedTime cannot be Infinity when CC_MAX_SIGNAL_LIFETIME_MINUTES is not Infinity`);
4108
+ }
4109
+ if (signal.minuteEstimatedTime !== Infinity && !Number.isInteger(signal.minuteEstimatedTime)) {
4110
+ errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
4111
+ }
4112
+ }
4113
+ // ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
4114
+ {
4115
+ if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity && signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
4116
+ const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
4117
+ const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
4118
+ errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
4119
+ `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
4120
+ `Eternal signals block risk limits and prevent new trades.`);
4121
+ }
4122
+ }
4123
+ // Кидаем ошибку если есть проблемы
4124
+ if (errors.length > 0) {
4125
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4126
+ }
4127
+ };
4128
+
4129
+ /**
4130
+ * Validates a pending (immediately active) signal before it is opened.
4131
+ *
4132
+ * Checks:
4133
+ * - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled
4134
+ * - currentPrice is a finite positive number
4135
+ * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
4136
+ * - currentPrice is between SL and TP — position would not be immediately closed on open
4137
+ * - scheduledAt and pendingAt are positive numbers
4138
+ *
4139
+ * @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
4140
+ *
4141
+ * @param signal - Pending signal row to validate
4142
+ * @param currentPrice - Current market price at the moment of signal creation
4143
+ * @throws {Error} If any validation check fails
4144
+ */
4145
+ const validatePendingSignal = (signal, currentPrice) => {
4146
+ const errors = [];
4147
+ // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
4148
+ {
4149
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
4150
+ errors.push('id is required and must be a non-empty string');
4151
+ }
4152
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
4153
+ errors.push('exchangeName is required');
4154
+ }
4155
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
4156
+ errors.push('strategyName is required');
4157
+ }
4158
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
4159
+ errors.push('symbol is required and must be a non-empty string');
4160
+ }
4161
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
4162
+ errors.push('_isScheduled is required');
4163
+ }
4164
+ }
4165
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
4166
+ {
4167
+ if (typeof currentPrice !== "number") {
4168
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
4169
+ }
4170
+ if (!isFinite(currentPrice)) {
4171
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
4172
+ }
4173
+ if (isFinite(currentPrice) && currentPrice <= 0) {
4174
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
4175
+ }
4176
+ }
4177
+ if (errors.length > 0) {
4178
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4179
+ }
4180
+ validateCommonSignal(signal);
4181
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
4182
+ if (signal.position === "long") {
4183
+ if (isFinite(currentPrice)) {
4184
+ // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
4185
+ // SL < currentPrice < TP
4186
+ if (currentPrice <= signal.priceStopLoss) {
4187
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
4188
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
4189
+ }
4190
+ if (currentPrice >= signal.priceTakeProfit) {
4191
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
4192
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
4193
+ }
4194
+ }
4195
+ }
4196
+ if (signal.position === "short") {
4197
+ if (isFinite(currentPrice)) {
4198
+ // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
4199
+ // TP < currentPrice < SL
4200
+ if (currentPrice >= signal.priceStopLoss) {
4201
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
4202
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
4203
+ }
4204
+ if (currentPrice <= signal.priceTakeProfit) {
4205
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
4206
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
4207
+ }
4208
+ }
4209
+ }
4210
+ // Валидация временных меток
4211
+ {
4212
+ if (typeof signal.scheduledAt !== "number") {
4213
+ errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
4214
+ }
4215
+ if (signal.scheduledAt <= 0) {
4216
+ errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
4217
+ }
4218
+ if (typeof signal.pendingAt !== "number") {
4219
+ errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
4220
+ }
4221
+ if (signal.pendingAt <= 0) {
4222
+ errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
4223
+ }
4224
+ }
4225
+ if (errors.length > 0) {
4226
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4227
+ }
4228
+ };
4229
+
4230
+ /**
4231
+ * Validates a scheduled signal before it is registered for activation.
4232
+ *
4233
+ * Checks:
4234
+ * - ISignalRow-specific fields: id, exchangeName, strategyName, symbol, _isScheduled
4235
+ * - currentPrice is a finite positive number
4236
+ * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
4237
+ * - priceOpen is between SL and TP — position would not be immediately closed upon activation
4238
+ * - scheduledAt is a positive number (pendingAt === 0 is allowed until activation)
4239
+ *
4240
+ * @deprecated This is an internal code exported for unit tests only. Use `validateSignal` in Strategy::getSignal
4241
+ *
4242
+ * @param signal - Scheduled signal row to validate
4243
+ * @param currentPrice - Current market price at the moment of signal creation
4244
+ * @throws {Error} If any validation check fails
4245
+ */
4246
+ const validateScheduledSignal = (signal, currentPrice) => {
4247
+ const errors = [];
4248
+ // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
4249
+ {
4250
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
4251
+ errors.push('id is required and must be a non-empty string');
4252
+ }
4253
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
4254
+ errors.push('exchangeName is required');
4255
+ }
4256
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
4257
+ errors.push('strategyName is required');
4258
+ }
4259
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
4260
+ errors.push('symbol is required and must be a non-empty string');
4261
+ }
4262
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
4263
+ errors.push('_isScheduled is required');
4264
+ }
4265
+ }
4266
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
4267
+ {
4268
+ if (typeof currentPrice !== "number") {
4269
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
4270
+ }
4271
+ if (!isFinite(currentPrice)) {
4272
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
4273
+ }
4274
+ if (isFinite(currentPrice) && currentPrice <= 0) {
4275
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
4276
+ }
4277
+ }
4278
+ if (errors.length > 0) {
4279
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4280
+ }
4281
+ validateCommonSignal(signal);
4282
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
4283
+ if (signal.position === "long") {
4284
+ if (isFinite(signal.priceOpen)) {
4285
+ // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
4286
+ // SL < priceOpen < TP
4287
+ if (signal.priceOpen <= signal.priceStopLoss) {
4288
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
4289
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
4290
+ }
4291
+ if (signal.priceOpen >= signal.priceTakeProfit) {
4292
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
4293
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
4294
+ }
4295
+ }
4296
+ }
4297
+ if (signal.position === "short") {
4298
+ if (isFinite(signal.priceOpen)) {
4299
+ // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
4300
+ // TP < priceOpen < SL
4301
+ if (signal.priceOpen >= signal.priceStopLoss) {
4302
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
4303
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
4304
+ }
4305
+ if (signal.priceOpen <= signal.priceTakeProfit) {
4306
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
4307
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
4308
+ }
4309
+ }
4310
+ }
4311
+ // Валидация временных меток
4312
+ {
4313
+ if (typeof signal.scheduledAt !== "number") {
4314
+ errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
4315
+ }
4316
+ if (signal.scheduledAt <= 0) {
4317
+ errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
4318
+ }
4319
+ if (typeof signal.pendingAt !== "number") {
4320
+ errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
4321
+ }
4322
+ // pendingAt === 0 is allowed for scheduled signals (set to SCHEDULED_SIGNAL_PENDING_MOCK until activation)
4323
+ }
4324
+ if (errors.length > 0) {
4325
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4326
+ }
4327
+ };
4328
+
3931
4329
  const INTERVAL_MINUTES$6 = {
3932
4330
  "1m": 1,
3933
4331
  "3m": 3,
@@ -4306,272 +4704,6 @@ const TO_PUBLIC_SIGNAL = (signal, currentPrice) => {
4306
4704
  pnl: toProfitLossDto(signal, currentPrice),
4307
4705
  };
4308
4706
  };
4309
- const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
4310
- const errors = [];
4311
- // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
4312
- {
4313
- if (signal.id === undefined || signal.id === null || signal.id === '') {
4314
- errors.push('id is required and must be a non-empty string');
4315
- }
4316
- if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
4317
- errors.push('exchangeName is required');
4318
- }
4319
- if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
4320
- errors.push('strategyName is required');
4321
- }
4322
- if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
4323
- errors.push('symbol is required and must be a non-empty string');
4324
- }
4325
- if (signal._isScheduled === undefined || signal._isScheduled === null) {
4326
- errors.push('_isScheduled is required');
4327
- }
4328
- if (signal.position === undefined || signal.position === null) {
4329
- errors.push('position is required and must be "long" or "short"');
4330
- }
4331
- if (signal.position !== "long" && signal.position !== "short") {
4332
- errors.push(`position must be "long" or "short", got "${signal.position}"`);
4333
- }
4334
- }
4335
- // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
4336
- {
4337
- if (typeof currentPrice !== "number") {
4338
- errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
4339
- }
4340
- if (!isFinite(currentPrice)) {
4341
- errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
4342
- }
4343
- if (isFinite(currentPrice) && currentPrice <= 0) {
4344
- errors.push(`currentPrice must be positive, got ${currentPrice}`);
4345
- }
4346
- }
4347
- // ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
4348
- {
4349
- if (typeof signal.priceOpen !== "number") {
4350
- errors.push(`priceOpen must be a number type, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
4351
- }
4352
- if (!isFinite(signal.priceOpen)) {
4353
- errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
4354
- }
4355
- if (typeof signal.priceTakeProfit !== "number") {
4356
- errors.push(`priceTakeProfit must be a number type, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
4357
- }
4358
- if (!isFinite(signal.priceTakeProfit)) {
4359
- errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
4360
- }
4361
- if (typeof signal.priceStopLoss !== "number") {
4362
- errors.push(`priceStopLoss must be a number type, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
4363
- }
4364
- if (!isFinite(signal.priceStopLoss)) {
4365
- errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
4366
- }
4367
- }
4368
- // Валидация цен (только если они конечные)
4369
- {
4370
- if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
4371
- errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
4372
- }
4373
- if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
4374
- errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
4375
- }
4376
- if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
4377
- errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
4378
- }
4379
- }
4380
- // Валидация для long позиции
4381
- if (signal.position === "long") {
4382
- // Проверка соотношения цен для long
4383
- {
4384
- if (signal.priceTakeProfit <= signal.priceOpen) {
4385
- errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
4386
- }
4387
- if (signal.priceStopLoss >= signal.priceOpen) {
4388
- errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
4389
- }
4390
- }
4391
- // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
4392
- {
4393
- if (!isScheduled && isFinite(currentPrice)) {
4394
- // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
4395
- // SL < currentPrice < TP
4396
- if (currentPrice <= signal.priceStopLoss) {
4397
- errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
4398
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
4399
- }
4400
- if (currentPrice >= signal.priceTakeProfit) {
4401
- errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
4402
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
4403
- }
4404
- }
4405
- }
4406
- // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
4407
- {
4408
- if (isScheduled && isFinite(signal.priceOpen)) {
4409
- // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
4410
- // SL < priceOpen < TP
4411
- if (signal.priceOpen <= signal.priceStopLoss) {
4412
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
4413
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
4414
- }
4415
- if (signal.priceOpen >= signal.priceTakeProfit) {
4416
- errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
4417
- `Signal would close immediately on activation. This is logically impossible for LONG position.`);
4418
- }
4419
- }
4420
- }
4421
- // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
4422
- {
4423
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4424
- const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
4425
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4426
- errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
4427
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
4428
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
4429
- }
4430
- }
4431
- }
4432
- // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
4433
- {
4434
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4435
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
4436
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4437
- errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4438
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
4439
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4440
- }
4441
- }
4442
- }
4443
- // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
4444
- {
4445
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4446
- const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
4447
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4448
- errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4449
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
4450
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4451
- }
4452
- }
4453
- }
4454
- }
4455
- // Валидация для short позиции
4456
- if (signal.position === "short") {
4457
- // Проверка соотношения цен для short
4458
- {
4459
- if (signal.priceTakeProfit >= signal.priceOpen) {
4460
- errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
4461
- }
4462
- if (signal.priceStopLoss <= signal.priceOpen) {
4463
- errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
4464
- }
4465
- }
4466
- // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
4467
- {
4468
- if (!isScheduled && isFinite(currentPrice)) {
4469
- // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
4470
- // TP < currentPrice < SL
4471
- if (currentPrice >= signal.priceStopLoss) {
4472
- errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
4473
- `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
4474
- }
4475
- if (currentPrice <= signal.priceTakeProfit) {
4476
- errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
4477
- `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
4478
- }
4479
- }
4480
- }
4481
- // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
4482
- {
4483
- if (isScheduled && isFinite(signal.priceOpen)) {
4484
- // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
4485
- // TP < priceOpen < SL
4486
- if (signal.priceOpen >= signal.priceStopLoss) {
4487
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
4488
- `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
4489
- }
4490
- if (signal.priceOpen <= signal.priceTakeProfit) {
4491
- errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
4492
- `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
4493
- }
4494
- }
4495
- }
4496
- // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
4497
- {
4498
- if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4499
- const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
4500
- if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
4501
- errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
4502
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
4503
- `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
4504
- }
4505
- }
4506
- }
4507
- // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
4508
- {
4509
- if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4510
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
4511
- if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
4512
- errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4513
- `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
4514
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4515
- }
4516
- }
4517
- }
4518
- // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
4519
- {
4520
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4521
- const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
4522
- if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
4523
- errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
4524
- `Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
4525
- `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
4526
- }
4527
- }
4528
- }
4529
- }
4530
- // Валидация временных параметров
4531
- {
4532
- if (typeof signal.minuteEstimatedTime !== "number") {
4533
- errors.push(`minuteEstimatedTime must be a number type, got ${signal.minuteEstimatedTime} (${typeof signal.minuteEstimatedTime})`);
4534
- }
4535
- if (signal.minuteEstimatedTime <= 0) {
4536
- errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
4537
- }
4538
- if (signal.minuteEstimatedTime === Infinity && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity) {
4539
- errors.push(`minuteEstimatedTime cannot be Infinity when CC_MAX_SIGNAL_LIFETIME_MINUTES is not Infinity`);
4540
- }
4541
- if (signal.minuteEstimatedTime !== Infinity && !Number.isInteger(signal.minuteEstimatedTime)) {
4542
- errors.push(`minuteEstimatedTime must be an integer (whole number), got ${signal.minuteEstimatedTime}`);
4543
- }
4544
- }
4545
- // ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
4546
- {
4547
- if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES !== Infinity && signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
4548
- const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
4549
- const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
4550
- errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
4551
- `Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
4552
- `Eternal signals block risk limits and prevent new trades.`);
4553
- }
4554
- }
4555
- // Валидация временных меток
4556
- {
4557
- if (typeof signal.scheduledAt !== "number") {
4558
- errors.push(`scheduledAt must be a number type, got ${signal.scheduledAt} (${typeof signal.scheduledAt})`);
4559
- }
4560
- if (signal.scheduledAt <= 0) {
4561
- errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
4562
- }
4563
- if (typeof signal.pendingAt !== "number") {
4564
- errors.push(`pendingAt must be a number type, got ${signal.pendingAt} (${typeof signal.pendingAt})`);
4565
- }
4566
- if (signal.pendingAt <= 0 && !isScheduled) {
4567
- errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
4568
- }
4569
- }
4570
- // Кидаем ошибку если есть проблемы
4571
- if (errors.length > 0) {
4572
- throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
4573
- }
4574
- };
4575
4707
  const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4576
4708
  if (self._isStopped) {
4577
4709
  return null;
@@ -4637,7 +4769,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4637
4769
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4638
4770
  };
4639
4771
  // Валидируем сигнал перед возвратом
4640
- VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
4772
+ validatePendingSignal(signalRow, currentPrice);
4641
4773
  return signalRow;
4642
4774
  }
4643
4775
  // ОЖИДАНИЕ АКТИВАЦИИ: создаем scheduled signal (risk check при активации)
@@ -4662,7 +4794,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4662
4794
  _peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4663
4795
  };
4664
4796
  // Валидируем сигнал перед возвратом
4665
- VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
4797
+ validateScheduledSignal(scheduledSignalRow, currentPrice);
4666
4798
  return scheduledSignalRow;
4667
4799
  }
4668
4800
  const signalRow = {
@@ -4684,7 +4816,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4684
4816
  _peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0 },
4685
4817
  };
4686
4818
  // Валидируем сигнал перед возвратом
4687
- VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
4819
+ validatePendingSignal(signalRow, currentPrice);
4688
4820
  return signalRow;
4689
4821
  }, {
4690
4822
  defaultValue: null,
@@ -4874,8 +5006,12 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
4874
5006
  // CRITICAL: Larger percentShift absorbs smaller one
4875
5007
  // For LONG: higher SL (closer to entry) absorbs lower one
4876
5008
  // For SHORT: lower SL (closer to entry) absorbs higher one
5009
+ // When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
4877
5010
  let shouldUpdate = false;
4878
- if (signal.position === "long") {
5011
+ if (GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
5012
+ shouldUpdate = true;
5013
+ }
5014
+ else if (signal.position === "long") {
4879
5015
  // LONG: update only if new SL is higher (better protection)
4880
5016
  shouldUpdate = newStopLoss > currentTrailingSL;
4881
5017
  }
@@ -4956,8 +5092,12 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
4956
5092
  // CRITICAL: Larger percentShift absorbs smaller one
4957
5093
  // For LONG: lower TP (closer to entry) absorbs higher one
4958
5094
  // For SHORT: higher TP (closer to entry) absorbs lower one
5095
+ // When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
4959
5096
  let shouldUpdate = false;
4960
- if (signal.position === "long") {
5097
+ if (GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
5098
+ shouldUpdate = true;
5099
+ }
5100
+ else if (signal.position === "long") {
4961
5101
  // LONG: update only if new TP is lower (closer to entry, more conservative)
4962
5102
  shouldUpdate = newTakeProfit < currentTrailingTP;
4963
5103
  }
@@ -8699,12 +8839,15 @@ class ClientStrategy {
8699
8839
  if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
8700
8840
  return false;
8701
8841
  // Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
8702
- const currentTrailingSL = signal._trailingPriceStopLoss;
8703
- if (currentTrailingSL !== undefined) {
8704
- if (signal.position === "long" && newStopLoss <= currentTrailingSL)
8705
- return false;
8706
- if (signal.position === "short" && newStopLoss >= currentTrailingSL)
8707
- return false;
8842
+ // When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
8843
+ if (!GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
8844
+ const currentTrailingSL = signal._trailingPriceStopLoss;
8845
+ if (currentTrailingSL !== undefined) {
8846
+ if (signal.position === "long" && newStopLoss <= currentTrailingSL)
8847
+ return false;
8848
+ if (signal.position === "short" && newStopLoss >= currentTrailingSL)
8849
+ return false;
8850
+ }
8708
8851
  }
8709
8852
  return true;
8710
8853
  }
@@ -8946,12 +9089,15 @@ class ClientStrategy {
8946
9089
  if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
8947
9090
  return false;
8948
9091
  // Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
8949
- const currentTrailingTP = signal._trailingPriceTakeProfit;
8950
- if (currentTrailingTP !== undefined) {
8951
- if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
8952
- return false;
8953
- if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
8954
- return false;
9092
+ // When CC_ENABLE_TRAILING_EVERYWHERE is true, absorption check is skipped
9093
+ if (!GLOBAL_CONFIG.CC_ENABLE_TRAILING_EVERYWHERE) {
9094
+ const currentTrailingTP = signal._trailingPriceTakeProfit;
9095
+ if (currentTrailingTP !== undefined) {
9096
+ if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
9097
+ return false;
9098
+ if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
9099
+ return false;
9100
+ }
8955
9101
  }
8956
9102
  return true;
8957
9103
  }
@@ -10975,7 +11121,8 @@ const INTERVAL_MINUTES$5 = {
10975
11121
  "8h": 480,
10976
11122
  "12h": 720,
10977
11123
  "1d": 1440,
10978
- "3d": 4320,
11124
+ "1w": 10080,
11125
+ "1M": 43200,
10979
11126
  };
10980
11127
  /**
10981
11128
  * Wrapper to call onTimeframe callback with error handling.
@@ -11394,6 +11541,8 @@ const INTERVAL_MINUTES$4 = {
11394
11541
  "4h": 240,
11395
11542
  "6h": 360,
11396
11543
  "8h": 480,
11544
+ "1d": 1440,
11545
+ "1w": 10080,
11397
11546
  };
11398
11547
  /**
11399
11548
  * Aligns timestamp down to the nearest interval boundary.
@@ -16740,6 +16889,8 @@ const INTERVAL_MINUTES$3 = {
16740
16889
  "4h": 240,
16741
16890
  "6h": 360,
16742
16891
  "8h": 480,
16892
+ "1d": 1440,
16893
+ "1w": 10080,
16743
16894
  };
16744
16895
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
16745
16896
  const tickSubject = new functoolsKit.Subject();
@@ -31576,6 +31727,8 @@ const INTERVAL_MINUTES$2 = {
31576
31727
  "4h": 240,
31577
31728
  "6h": 360,
31578
31729
  "8h": 480,
31730
+ "1d": 1440,
31731
+ "1w": 10080,
31579
31732
  };
31580
31733
  /**
31581
31734
  * Aligns timestamp down to the nearest interval boundary.
@@ -32325,6 +32478,8 @@ const INTERVAL_MINUTES$1 = {
32325
32478
  "4h": 240,
32326
32479
  "6h": 360,
32327
32480
  "8h": 480,
32481
+ "1d": 1440,
32482
+ "1w": 10080,
32328
32483
  };
32329
32484
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
32330
32485
  const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
@@ -50760,6 +50915,8 @@ const INTERVAL_MINUTES = {
50760
50915
  "4h": 240,
50761
50916
  "6h": 360,
50762
50917
  "8h": 480,
50918
+ "1d": 1440,
50919
+ "1w": 10080,
50763
50920
  };
50764
50921
  /**
50765
50922
  * Aligns timestamp down to the nearest interval boundary.
@@ -51834,6 +51991,119 @@ const percentValue = (yesterdayValue, todayValue) => {
51834
51991
  return yesterdayValue / todayValue - 1;
51835
51992
  };
51836
51993
 
51994
+ /**
51995
+ * Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
51996
+ *
51997
+ * When priceOpen is provided:
51998
+ * - If currentPrice already reached priceOpen (shouldActivateImmediately) →
51999
+ * validates as pending: currentPrice must be between SL and TP
52000
+ * - Otherwise → validates as scheduled: priceOpen must be between SL and TP
52001
+ *
52002
+ * When priceOpen is absent:
52003
+ * - Validates as pending: currentPrice must be between SL and TP
52004
+ *
52005
+ * Checks:
52006
+ * - currentPrice is a finite positive number
52007
+ * - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
52008
+ * - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
52009
+ *
52010
+ * @param signal - Signal DTO returned by getSignal
52011
+ * @param currentPrice - Current market price at the moment of signal creation
52012
+ * @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
52013
+ */
52014
+ const validateSignal = (signal, currentPrice) => {
52015
+ const errors = [];
52016
+ // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
52017
+ {
52018
+ if (typeof currentPrice !== "number") {
52019
+ errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
52020
+ }
52021
+ if (!isFinite(currentPrice)) {
52022
+ errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
52023
+ }
52024
+ if (isFinite(currentPrice) && currentPrice <= 0) {
52025
+ errors.push(`currentPrice must be positive, got ${currentPrice}`);
52026
+ }
52027
+ }
52028
+ if (errors.length > 0) {
52029
+ console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
52030
+ return false;
52031
+ }
52032
+ try {
52033
+ validateCommonSignal(signal);
52034
+ }
52035
+ catch (error) {
52036
+ console.error(functoolsKit.getErrorMessage(error));
52037
+ return false;
52038
+ }
52039
+ // Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
52040
+ // - нет priceOpen → pending (открывается по currentPrice)
52041
+ // - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
52042
+ // - priceOpen задан и ещё не достигнут → scheduled
52043
+ const hasPriceOpen = signal.priceOpen !== undefined;
52044
+ const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
52045
+ (signal.position === "short" && currentPrice >= signal.priceOpen));
52046
+ const isScheduled = hasPriceOpen && !shouldActivateImmediately;
52047
+ if (isScheduled) {
52048
+ // Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
52049
+ if (signal.position === "long") {
52050
+ if (isFinite(signal.priceOpen)) {
52051
+ if (signal.priceOpen <= signal.priceStopLoss) {
52052
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
52053
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
52054
+ }
52055
+ if (signal.priceOpen >= signal.priceTakeProfit) {
52056
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
52057
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
52058
+ }
52059
+ }
52060
+ }
52061
+ if (signal.position === "short") {
52062
+ if (isFinite(signal.priceOpen)) {
52063
+ if (signal.priceOpen >= signal.priceStopLoss) {
52064
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
52065
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
52066
+ }
52067
+ if (signal.priceOpen <= signal.priceTakeProfit) {
52068
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
52069
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
52070
+ }
52071
+ }
52072
+ }
52073
+ }
52074
+ else {
52075
+ // Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
52076
+ if (signal.position === "long") {
52077
+ if (isFinite(currentPrice)) {
52078
+ if (currentPrice <= signal.priceStopLoss) {
52079
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
52080
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
52081
+ }
52082
+ if (currentPrice >= signal.priceTakeProfit) {
52083
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
52084
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
52085
+ }
52086
+ }
52087
+ }
52088
+ if (signal.position === "short") {
52089
+ if (isFinite(currentPrice)) {
52090
+ if (currentPrice >= signal.priceStopLoss) {
52091
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
52092
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
52093
+ }
52094
+ if (currentPrice <= signal.priceTakeProfit) {
52095
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
52096
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
52097
+ }
52098
+ }
52099
+ }
52100
+ }
52101
+ if (errors.length > 0) {
52102
+ console.error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
52103
+ }
52104
+ return !errors.length;
52105
+ };
52106
+
51837
52107
  exports.ActionBase = ActionBase;
51838
52108
  exports.Backtest = Backtest;
51839
52109
  exports.Breakeven = Breakeven;
@@ -52039,6 +52309,10 @@ exports.toProfitLossDto = toProfitLossDto;
52039
52309
  exports.tpPercentShiftToPrice = tpPercentShiftToPrice;
52040
52310
  exports.tpPriceToPercentShift = tpPriceToPercentShift;
52041
52311
  exports.validate = validate;
52312
+ exports.validateCommonSignal = validateCommonSignal;
52313
+ exports.validatePendingSignal = validatePendingSignal;
52314
+ exports.validateScheduledSignal = validateScheduledSignal;
52315
+ exports.validateSignal = validateSignal;
52042
52316
  exports.waitForCandle = waitForCandle;
52043
52317
  exports.warmCandles = warmCandles;
52044
52318
  exports.writeMemory = writeMemory;