backtest-kit 5.9.0 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }
@@ -6345,7 +6485,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
6345
6485
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
6346
6486
  return result;
6347
6487
  };
6348
- const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
6488
+ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
6349
6489
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6350
6490
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
6351
6491
  const bufferCandlesCount = candlesCount - 1;
@@ -6358,6 +6498,11 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6358
6498
  }
6359
6499
  const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
6360
6500
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6501
+ // Если timestamp свечи вышел за frameEndTime — отменяем scheduled сигнал
6502
+ if (candle.timestamp > frameEndTime) {
6503
+ const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
6504
+ return { outcome: "cancelled", result };
6505
+ }
6361
6506
  // КРИТИЧНО: Проверяем был ли сигнал отменен пользователем через cancel()
6362
6507
  if (self._cancelledSignal) {
6363
6508
  // Сигнал был отменен через cancel() в onSchedulePing
@@ -6505,7 +6650,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6505
6650
  }
6506
6651
  return { outcome: "pending" };
6507
6652
  };
6508
- const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6653
+ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEndTime) => {
6509
6654
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
6510
6655
  const bufferCandlesCount = candlesCount - 1;
6511
6656
  // КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
@@ -6522,6 +6667,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6522
6667
  const startIndex = Math.max(0, i - (candlesCount - 1));
6523
6668
  const recentCandles = candles.slice(startIndex, i + 1);
6524
6669
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6670
+ // Если timestamp свечи вышел за frameEndTime — закрываем pending сигнал по time_expired
6671
+ if (currentCandleTimestamp > frameEndTime) {
6672
+ const result = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, averagePrice, "time_expired", currentCandleTimestamp);
6673
+ if (!result) {
6674
+ throw new Error(`ClientStrategy backtest: frameEndTime time_expired close rejected by sync (signalId=${signal.id}).`);
6675
+ }
6676
+ return result;
6677
+ }
6525
6678
  // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
6526
6679
  if (self._closedSignal) {
6527
6680
  return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
@@ -7687,7 +7840,7 @@ class ClientStrategy {
7687
7840
  * console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
7688
7841
  * ```
7689
7842
  */
7690
- async backtest(symbol, strategyName, candles) {
7843
+ async backtest(symbol, strategyName, candles, frameEndTime) {
7691
7844
  this.params.logger.debug("ClientStrategy backtest", {
7692
7845
  symbol,
7693
7846
  strategyName,
@@ -7695,6 +7848,7 @@ class ClientStrategy {
7695
7848
  candlesCount: candles.length,
7696
7849
  hasScheduled: !!this._scheduledSignal,
7697
7850
  hasPending: !!this._pendingSignal,
7851
+ frameEndTime,
7698
7852
  });
7699
7853
  if (!this.params.execution.context.backtest) {
7700
7854
  throw new Error("ClientStrategy backtest: running in live context");
@@ -7813,7 +7967,7 @@ class ClientStrategy {
7813
7967
  priceOpen: scheduled.priceOpen,
7814
7968
  position: scheduled.position,
7815
7969
  });
7816
- const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7970
+ const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles, frameEndTime);
7817
7971
  if (scheduledResult.outcome === "cancelled") {
7818
7972
  return scheduledResult.result;
7819
7973
  }
@@ -7890,7 +8044,7 @@ class ClientStrategy {
7890
8044
  if (candles.length < candlesCount) {
7891
8045
  this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
7892
8046
  }
7893
- return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
8047
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles, frameEndTime);
7894
8048
  }
7895
8049
  /**
7896
8050
  * Stops the strategy from generating new signals.
@@ -8685,12 +8839,15 @@ class ClientStrategy {
8685
8839
  if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
8686
8840
  return false;
8687
8841
  // Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
8688
- const currentTrailingSL = signal._trailingPriceStopLoss;
8689
- if (currentTrailingSL !== undefined) {
8690
- if (signal.position === "long" && newStopLoss <= currentTrailingSL)
8691
- return false;
8692
- if (signal.position === "short" && newStopLoss >= currentTrailingSL)
8693
- 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
+ }
8694
8851
  }
8695
8852
  return true;
8696
8853
  }
@@ -8932,12 +9089,15 @@ class ClientStrategy {
8932
9089
  if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
8933
9090
  return false;
8934
9091
  // Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
8935
- const currentTrailingTP = signal._trailingPriceTakeProfit;
8936
- if (currentTrailingTP !== undefined) {
8937
- if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
8938
- return false;
8939
- if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
8940
- 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
+ }
8941
9101
  }
8942
9102
  return true;
8943
9103
  }
@@ -10283,17 +10443,18 @@ class StrategyConnectionService {
10283
10443
  * @param candles - Array of historical candle data to backtest
10284
10444
  * @returns Promise resolving to backtest result (signal or idle)
10285
10445
  */
10286
- this.backtest = async (symbol, context, candles) => {
10446
+ this.backtest = async (symbol, context, candles, frameEndTime) => {
10287
10447
  const backtest = this.executionContextService.context.backtest;
10288
10448
  this.loggerService.log("strategyConnectionService backtest", {
10289
10449
  symbol,
10290
10450
  context,
10291
10451
  candleCount: candles.length,
10452
+ frameEndTime,
10292
10453
  backtest,
10293
10454
  });
10294
10455
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10295
10456
  await strategy.waitForInit();
10296
- const tick = await strategy.backtest(symbol, context.strategyName, candles);
10457
+ const tick = await strategy.backtest(symbol, context.strategyName, candles, frameEndTime);
10297
10458
  {
10298
10459
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
10299
10460
  }
@@ -10960,7 +11121,8 @@ const INTERVAL_MINUTES$5 = {
10960
11121
  "8h": 480,
10961
11122
  "12h": 720,
10962
11123
  "1d": 1440,
10963
- "3d": 4320,
11124
+ "1w": 10080,
11125
+ "1M": 43200,
10964
11126
  };
10965
11127
  /**
10966
11128
  * Wrapper to call onTimeframe callback with error handling.
@@ -11379,6 +11541,8 @@ const INTERVAL_MINUTES$4 = {
11379
11541
  "4h": 240,
11380
11542
  "6h": 360,
11381
11543
  "8h": 480,
11544
+ "1d": 1440,
11545
+ "1w": 10080,
11382
11546
  };
11383
11547
  /**
11384
11548
  * Aligns timestamp down to the nearest interval boundary.
@@ -14228,17 +14392,18 @@ class StrategyCoreService {
14228
14392
  * @param context - Execution context with strategyName, exchangeName, frameName
14229
14393
  * @returns Closed signal result with PNL
14230
14394
  */
14231
- this.backtest = async (symbol, candles, when, backtest, context) => {
14395
+ this.backtest = async (symbol, candles, frameEndTime, when, backtest, context) => {
14232
14396
  this.loggerService.log("strategyCoreService backtest", {
14233
14397
  symbol,
14234
14398
  candleCount: candles.length,
14235
14399
  when,
14236
14400
  backtest,
14237
14401
  context,
14402
+ frameEndTime,
14238
14403
  });
14239
14404
  await this.validate(context);
14240
14405
  return await ExecutionContextService.runInContext(async () => {
14241
- return await this.strategyConnectionService.backtest(symbol, context, candles);
14406
+ return await this.strategyConnectionService.backtest(symbol, context, candles, frameEndTime);
14242
14407
  }, {
14243
14408
  symbol,
14244
14409
  when,
@@ -16231,7 +16396,9 @@ const TICK_FN = async (self, symbol, when) => {
16231
16396
  });
16232
16397
  }
16233
16398
  catch (error) {
16234
- console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName}`);
16399
+ console.error(`backtestLogicPrivateService tick failed symbol=${symbol} when=${when.toISOString()} strategyName=${self.methodContextService.context.strategyName} exchangeName=${self.methodContextService.context.exchangeName} error=${functoolsKit.getErrorMessage(error)}`, {
16400
+ error: functoolsKit.errorData(error),
16401
+ });
16235
16402
  self.loggerService.warn("backtestLogicPrivateService tick failed", {
16236
16403
  symbol,
16237
16404
  when: when.toISOString(),
@@ -16257,9 +16424,9 @@ const GET_CANDLES_FN = async (self, symbol, candlesNeeded, bufferStartTime, logM
16257
16424
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "GET_CANDLES_FN", message: functoolsKit.getErrorMessage(error) };
16258
16425
  }
16259
16426
  };
16260
- const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16427
+ const BACKTEST_FN = async (self, symbol, candles, frameEndTime, when, context, logMeta) => {
16261
16428
  try {
16262
- return await self.strategyCoreService.backtest(symbol, candles, when, true, context);
16429
+ return await self.strategyCoreService.backtest(symbol, candles, frameEndTime, when, true, context);
16263
16430
  }
16264
16431
  catch (error) {
16265
16432
  console.error(`backtestLogicPrivateService backtest failed symbol=${symbol} when=${when.toISOString()} strategyName=${context.strategyName} exchangeName=${context.exchangeName}`);
@@ -16272,7 +16439,29 @@ const BACKTEST_FN = async (self, symbol, candles, when, context, logMeta) => {
16272
16439
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "BACKTEST_FN", message: functoolsKit.getErrorMessage(error) };
16273
16440
  }
16274
16441
  };
16275
- const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId) => {
16442
+ const CLOSE_PENDING_FN = async (self, symbol, context, lastChunkCandles, frameEndTime, when, signalId) => {
16443
+ try {
16444
+ await self.strategyCoreService.closePending(true, symbol, context);
16445
+ }
16446
+ catch (error) {
16447
+ const message = `closePending failed: ${functoolsKit.getErrorMessage(error)}`;
16448
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16449
+ await errorEmitter.next(error instanceof Error ? error : new Error(message));
16450
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16451
+ }
16452
+ const result = await BACKTEST_FN(self, symbol, lastChunkCandles, frameEndTime, when, context, { signalId });
16453
+ if ("__error__" in result) {
16454
+ return result;
16455
+ }
16456
+ if (result.action === "active") {
16457
+ const message = `signal ${signalId} still active after closePending`;
16458
+ console.error(`backtestLogicPrivateService CLOSE_PENDING_FN: ${message} symbol=${symbol}`);
16459
+ await errorEmitter.next(new Error(message));
16460
+ return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "CLOSE_PENDING_FN", message };
16461
+ }
16462
+ return result;
16463
+ };
16464
+ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialResult, bufferMs, signalId, frameEndTime) => {
16276
16465
  let backtestResult = initialResult;
16277
16466
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16278
16467
  let lastChunkCandles = [];
@@ -16283,25 +16472,14 @@ const RUN_INFINITY_CHUNK_LOOP_FN = async (self, symbol, when, context, initialRe
16283
16472
  return chunkCandles;
16284
16473
  }
16285
16474
  if (!chunkCandles.length) {
16286
- await self.strategyCoreService.closePending(true, symbol, context);
16287
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16288
- if ("__error__" in result) {
16289
- return result;
16290
- }
16291
- if (result.action === "active") {
16292
- const message = `signal ${signalId} still active after closePending`;
16293
- console.error(`backtestLogicPrivateService RUN_INFINITY_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16294
- await errorEmitter.next(new Error(message));
16295
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_INFINITY_CHUNK_LOOP_FN", message };
16296
- }
16297
- return result;
16475
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16298
16476
  }
16299
16477
  self.loggerService.info("backtestLogicPrivateService candles fetched for infinity chunk", {
16300
16478
  symbol,
16301
16479
  signalId,
16302
16480
  candlesCount: chunkCandles.length,
16303
16481
  });
16304
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16482
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16305
16483
  if ("__error__" in chunkResult) {
16306
16484
  return chunkResult;
16307
16485
  }
@@ -16346,7 +16524,7 @@ const EMIT_TIMEFRAME_PERFORMANCE_FN = async (self, symbol, timeframeStartTime, p
16346
16524
  });
16347
16525
  return currentTimestamp;
16348
16526
  };
16349
- const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16527
+ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16350
16528
  const signalStartTime = performance.now();
16351
16529
  const signal = result.signal;
16352
16530
  self.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
@@ -16366,6 +16544,10 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16366
16544
  console.error(`backtestLogicPrivateService scheduled signal: getCandles failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${candles.reason} message=${candles.message}`);
16367
16545
  return candles;
16368
16546
  }
16547
+ // No candles available for this scheduled signal — the frame ends before the signal
16548
+ // could be evaluated. Unlike pending (Infinity) signals that require CLOSE_PENDING_FN,
16549
+ // a scheduled signal that never activated needs no explicit cancellation: it simply
16550
+ // did not start. Returning "skip" moves the backtest to the next timeframe.
16369
16551
  if (!candles.length) {
16370
16552
  return { type: "skip" };
16371
16553
  }
@@ -16407,7 +16589,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16407
16589
  });
16408
16590
  }
16409
16591
  try {
16410
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16592
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16411
16593
  if ("__error__" in firstResult) {
16412
16594
  console.error(`backtestLogicPrivateService scheduled signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16413
16595
  return firstResult;
@@ -16419,7 +16601,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16419
16601
  }
16420
16602
  if (backtestResult.action === "active" && signal.minuteEstimatedTime === Infinity) {
16421
16603
  const bufferMs = bufferMinutes * 60000;
16422
- const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id);
16604
+ const chunkResult = await RUN_INFINITY_CHUNK_LOOP_FN(self, symbol, when, context, backtestResult, bufferMs, signal.id, frameEndTime);
16423
16605
  if ("__error__" in chunkResult) {
16424
16606
  console.error(`backtestLogicPrivateService scheduled signal: infinity chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16425
16607
  return chunkResult;
@@ -16449,7 +16631,7 @@ const PROCESS_SCHEDULED_SIGNAL_FN = async function* (self, symbol, when, result,
16449
16631
  yield backtestResult;
16450
16632
  return { type: "closed", previousEventTimestamp: newTimestamp, closeTimestamp: backtestResult.closeTimestamp, shouldStop };
16451
16633
  };
16452
- const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId) => {
16634
+ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStartTime, bufferMs, signalId, frameEndTime) => {
16453
16635
  const CHUNK = GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST;
16454
16636
  let chunkStart = bufferStartTime;
16455
16637
  let lastChunkCandles = [];
@@ -16470,29 +16652,14 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16470
16652
  await errorEmitter.next(new Error(message));
16471
16653
  return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16472
16654
  }
16473
- await self.strategyCoreService.closePending(true, symbol, context);
16474
- const result = await BACKTEST_FN(self, symbol, lastChunkCandles, when, context, { signalId });
16475
- if ("__error__" in result) {
16476
- return result;
16477
- }
16478
- if (result.action === "active") {
16479
- const message = `signal ${signalId} still active after closePending`;
16480
- console.error(`backtestLogicPrivateService RUN_OPENED_CHUNK_LOOP_FN: ${message} symbol=${symbol}`);
16481
- self.loggerService.warn("backtestLogicPrivateService opened infinity: signal still active after closePending", {
16482
- symbol,
16483
- signalId,
16484
- });
16485
- await errorEmitter.next(new Error(message));
16486
- return { type: "error", __error__: SYMBOL_FN_ERROR, reason: "RUN_OPENED_CHUNK_LOOP_FN", message };
16487
- }
16488
- return result;
16655
+ return await CLOSE_PENDING_FN(self, symbol, context, lastChunkCandles, frameEndTime, when, signalId);
16489
16656
  }
16490
16657
  self.loggerService.info("backtestLogicPrivateService candles fetched", {
16491
16658
  symbol,
16492
16659
  signalId,
16493
16660
  candlesCount: chunkCandles.length,
16494
16661
  });
16495
- const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, when, context, { signalId });
16662
+ const chunkResult = await BACKTEST_FN(self, symbol, chunkCandles, frameEndTime, when, context, { signalId });
16496
16663
  if ("__error__" in chunkResult) {
16497
16664
  return chunkResult;
16498
16665
  }
@@ -16503,7 +16670,7 @@ const RUN_OPENED_CHUNK_LOOP_FN = async (self, symbol, when, context, bufferStart
16503
16670
  chunkStart = new Date(chunkResult._backtestLastTimestamp + 60000 - bufferMs);
16504
16671
  }
16505
16672
  };
16506
- const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp) {
16673
+ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, previousEventTimestamp, frameEndTime) {
16507
16674
  const signalStartTime = performance.now();
16508
16675
  const signal = result.signal;
16509
16676
  self.loggerService.info("backtestLogicPrivateService signal opened", {
@@ -16534,7 +16701,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16534
16701
  signalId: signal.id,
16535
16702
  candlesCount: candles.length,
16536
16703
  });
16537
- const firstResult = await BACKTEST_FN(self, symbol, candles, when, context, { signalId: signal.id });
16704
+ const firstResult = await BACKTEST_FN(self, symbol, candles, frameEndTime, when, context, { signalId: signal.id });
16538
16705
  if ("__error__" in firstResult) {
16539
16706
  console.error(`backtestLogicPrivateService opened signal: backtest failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${firstResult.reason} message=${firstResult.message}`);
16540
16707
  return firstResult;
@@ -16543,7 +16710,7 @@ const PROCESS_OPENED_SIGNAL_FN = async function* (self, symbol, when, result, pr
16543
16710
  }
16544
16711
  else {
16545
16712
  const bufferMs = bufferMinutes * 60000;
16546
- const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id);
16713
+ const chunkResult = await RUN_OPENED_CHUNK_LOOP_FN(self, symbol, when, context, bufferStartTime, bufferMs, signal.id, frameEndTime);
16547
16714
  if ("__error__" in chunkResult) {
16548
16715
  console.error(`backtestLogicPrivateService opened signal: chunk loop failed, stopping backtest symbol=${symbol} signalId=${signal.id} reason=${chunkResult.reason} message=${chunkResult.message}`);
16549
16716
  return chunkResult;
@@ -16609,86 +16776,103 @@ class BacktestLogicPrivateService {
16609
16776
  symbol,
16610
16777
  });
16611
16778
  const backtestStartTime = performance.now();
16779
+ let _fatalError = null;
16780
+ let previousEventTimestamp = null;
16612
16781
  const timeframes = await this.frameCoreService.getTimeframe(symbol, this.methodContextService.context.frameName);
16613
16782
  const totalFrames = timeframes.length;
16783
+ let frameEndTime = timeframes[totalFrames - 1].getTime();
16614
16784
  let i = 0;
16615
- let previousEventTimestamp = null;
16616
- while (i < timeframes.length) {
16617
- const timeframeStartTime = performance.now();
16618
- const when = timeframes[i];
16619
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16620
- if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16621
- break;
16622
- }
16623
- const result = await TICK_FN(this, symbol, when);
16624
- if ("__error__" in result) {
16625
- break;
16626
- }
16627
- if (result.action === "idle" &&
16628
- await functoolsKit.and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16629
- strategyName: this.methodContextService.context.strategyName,
16630
- exchangeName: this.methodContextService.context.exchangeName,
16631
- frameName: this.methodContextService.context.frameName,
16632
- }))) {
16633
- this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16634
- symbol,
16635
- when: when.toISOString(),
16636
- processedFrames: i,
16637
- totalFrames,
16638
- });
16639
- break;
16640
- }
16641
- if (result.action === "scheduled") {
16642
- yield result;
16643
- const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16644
- if (r.type === "error") {
16785
+ try {
16786
+ while (i < timeframes.length) {
16787
+ const timeframeStartTime = performance.now();
16788
+ const when = timeframes[i];
16789
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, i);
16790
+ if (await CHECK_STOPPED_FN(this, symbol, "before tick", { when: when.toISOString(), processedFrames: i, totalFrames })) {
16645
16791
  break;
16646
16792
  }
16647
- if (r.type === "closed") {
16648
- previousEventTimestamp = r.previousEventTimestamp;
16649
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16650
- i++;
16651
- }
16652
- if (r.shouldStop) {
16653
- break;
16654
- }
16793
+ const result = await TICK_FN(this, symbol, when);
16794
+ if ("__error__" in result) {
16795
+ _fatalError = new Error(`[${result.reason}] ${result.message}`);
16796
+ break;
16655
16797
  }
16656
- }
16657
- if (result.action === "opened") {
16658
- yield result;
16659
- const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp);
16660
- if (r.type === "error") {
16798
+ if (result.action === "idle" &&
16799
+ await functoolsKit.and(Promise.resolve(true), this.strategyCoreService.getStopped(true, symbol, {
16800
+ strategyName: this.methodContextService.context.strategyName,
16801
+ exchangeName: this.methodContextService.context.exchangeName,
16802
+ frameName: this.methodContextService.context.frameName,
16803
+ }))) {
16804
+ this.loggerService.info("backtestLogicPrivateService stopped by user request (idle state)", {
16805
+ symbol,
16806
+ when: when.toISOString(),
16807
+ processedFrames: i,
16808
+ totalFrames,
16809
+ });
16661
16810
  break;
16662
16811
  }
16663
- if (r.type === "closed") {
16664
- previousEventTimestamp = r.previousEventTimestamp;
16665
- while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16666
- i++;
16812
+ if (result.action === "scheduled") {
16813
+ yield result;
16814
+ const r = yield* PROCESS_SCHEDULED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16815
+ if (r.type === "error") {
16816
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16817
+ break;
16667
16818
  }
16668
- if (r.shouldStop) {
16819
+ if (r.type === "closed") {
16820
+ previousEventTimestamp = r.previousEventTimestamp;
16821
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16822
+ i++;
16823
+ }
16824
+ if (r.shouldStop) {
16825
+ break;
16826
+ }
16827
+ }
16828
+ }
16829
+ if (result.action === "opened") {
16830
+ yield result;
16831
+ const r = yield* PROCESS_OPENED_SIGNAL_FN(this, symbol, when, result, previousEventTimestamp, frameEndTime);
16832
+ if (r.type === "error") {
16833
+ _fatalError = new Error(`[${r.reason}] ${r.message}`);
16669
16834
  break;
16670
16835
  }
16836
+ if (r.type === "closed") {
16837
+ previousEventTimestamp = r.previousEventTimestamp;
16838
+ while (i < timeframes.length && timeframes[i].getTime() < r.closeTimestamp) {
16839
+ i++;
16840
+ }
16841
+ if (r.shouldStop) {
16842
+ break;
16843
+ }
16844
+ }
16671
16845
  }
16846
+ previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16847
+ i++;
16672
16848
  }
16673
- previousEventTimestamp = await EMIT_TIMEFRAME_PERFORMANCE_FN(this, symbol, timeframeStartTime, previousEventTimestamp);
16674
- i++;
16675
- }
16676
- // Emit final progress event (100%)
16677
- await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16678
- // Track total backtest duration
16679
- const backtestEndTime = performance.now();
16680
- const currentTimestamp = Date.now();
16681
- await performanceEmitter.next({
16682
- timestamp: currentTimestamp,
16683
- previousTimestamp: previousEventTimestamp,
16684
- metricType: "backtest_total",
16685
- duration: backtestEndTime - backtestStartTime,
16686
- strategyName: this.methodContextService.context.strategyName,
16687
- exchangeName: this.methodContextService.context.exchangeName,
16688
- frameName: this.methodContextService.context.frameName,
16689
- symbol,
16690
- backtest: true,
16691
- });
16849
+ // Emit final progress event (100%)
16850
+ await EMIT_PROGRESS_FN(this, symbol, totalFrames, totalFrames);
16851
+ // Track total backtest duration
16852
+ const backtestEndTime = performance.now();
16853
+ const currentTimestamp = Date.now();
16854
+ await performanceEmitter.next({
16855
+ timestamp: currentTimestamp,
16856
+ previousTimestamp: previousEventTimestamp,
16857
+ metricType: "backtest_total",
16858
+ duration: backtestEndTime - backtestStartTime,
16859
+ strategyName: this.methodContextService.context.strategyName,
16860
+ exchangeName: this.methodContextService.context.exchangeName,
16861
+ frameName: this.methodContextService.context.frameName,
16862
+ symbol,
16863
+ backtest: true,
16864
+ });
16865
+ }
16866
+ catch (error) {
16867
+ _fatalError = error;
16868
+ }
16869
+ finally {
16870
+ if (_fatalError !== null) {
16871
+ console.error(`[BacktestLogicPrivateService] Fatal error — backtest sequence broken for symbol=${symbol} ` +
16872
+ `strategy=${this.methodContextService.context.strategyName}`, _fatalError);
16873
+ process.exit(-1);
16874
+ }
16875
+ }
16692
16876
  }
16693
16877
  }
16694
16878
 
@@ -16705,6 +16889,8 @@ const INTERVAL_MINUTES$3 = {
16705
16889
  "4h": 240,
16706
16890
  "6h": 360,
16707
16891
  "8h": 480,
16892
+ "1d": 1440,
16893
+ "1w": 10080,
16708
16894
  };
16709
16895
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
16710
16896
  const tickSubject = new functoolsKit.Subject();
@@ -31541,6 +31727,8 @@ const INTERVAL_MINUTES$2 = {
31541
31727
  "4h": 240,
31542
31728
  "6h": 360,
31543
31729
  "8h": 480,
31730
+ "1d": 1440,
31731
+ "1w": 10080,
31544
31732
  };
31545
31733
  /**
31546
31734
  * Aligns timestamp down to the nearest interval boundary.
@@ -32290,6 +32478,8 @@ const INTERVAL_MINUTES$1 = {
32290
32478
  "4h": 240,
32291
32479
  "6h": 360,
32292
32480
  "8h": 480,
32481
+ "1d": 1440,
32482
+ "1w": 10080,
32293
32483
  };
32294
32484
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
32295
32485
  const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
@@ -33344,54 +33534,144 @@ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommi
33344
33534
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
33345
33535
  const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
33346
33536
  const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
33537
+ /**
33538
+ * Wrapper around a `Partial<IBroker>` adapter instance.
33539
+ *
33540
+ * Implements the full `IBroker` interface but guards every method call —
33541
+ * if the underlying adapter does not implement a given method, an error is thrown.
33542
+ * `waitForInit` is the only exception: it is silently skipped when not implemented.
33543
+ *
33544
+ * Created internally by `BrokerAdapter.useBrokerAdapter` and stored as
33545
+ * `_brokerInstance`. All `BrokerAdapter.commit*` methods delegate here
33546
+ * after backtest-mode and enable-state checks pass.
33547
+ */
33347
33548
  class BrokerProxy {
33348
33549
  constructor(_instance) {
33349
33550
  this._instance = _instance;
33551
+ /**
33552
+ * Calls `waitForInit` on the underlying adapter exactly once (singleshot).
33553
+ * If the adapter does not implement `waitForInit`, the call is silently skipped.
33554
+ *
33555
+ * @returns Resolves when initialization is complete (or immediately if not implemented).
33556
+ */
33350
33557
  this.waitForInit = functoolsKit.singleshot(async () => {
33351
33558
  if (this._instance.waitForInit) {
33352
33559
  await this._instance.waitForInit();
33560
+ return;
33353
33561
  }
33354
33562
  });
33355
33563
  }
33564
+ /**
33565
+ * Forwards a signal-open event to the underlying adapter.
33566
+ * Throws if the adapter does not implement `onSignalOpenCommit`.
33567
+ *
33568
+ * @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag.
33569
+ * @throws {Error} If the adapter does not implement `onSignalOpenCommit`.
33570
+ */
33356
33571
  async onSignalOpenCommit(payload) {
33357
33572
  if (this._instance.onSignalOpenCommit) {
33358
33573
  await this._instance.onSignalOpenCommit(payload);
33574
+ return;
33359
33575
  }
33576
+ throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
33360
33577
  }
33578
+ /**
33579
+ * Forwards a signal-close event to the underlying adapter.
33580
+ * Throws if the adapter does not implement `onSignalCloseCommit`.
33581
+ *
33582
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag.
33583
+ * @throws {Error} If the adapter does not implement `onSignalCloseCommit`.
33584
+ */
33361
33585
  async onSignalCloseCommit(payload) {
33362
33586
  if (this._instance.onSignalCloseCommit) {
33363
33587
  await this._instance.onSignalCloseCommit(payload);
33588
+ return;
33364
33589
  }
33590
+ throw new Error("BrokerProxy onSignalCloseCommit is not implemented");
33365
33591
  }
33592
+ /**
33593
+ * Forwards a partial-profit close event to the underlying adapter.
33594
+ * Throws if the adapter does not implement `onPartialProfitCommit`.
33595
+ *
33596
+ * @param payload - Partial profit details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33597
+ * @throws {Error} If the adapter does not implement `onPartialProfitCommit`.
33598
+ */
33366
33599
  async onPartialProfitCommit(payload) {
33367
33600
  if (this._instance.onPartialProfitCommit) {
33368
33601
  await this._instance.onPartialProfitCommit(payload);
33602
+ return;
33369
33603
  }
33604
+ throw new Error("BrokerProxy onPartialProfitCommit is not implemented");
33370
33605
  }
33606
+ /**
33607
+ * Forwards a partial-loss close event to the underlying adapter.
33608
+ * Throws if the adapter does not implement `onPartialLossCommit`.
33609
+ *
33610
+ * @param payload - Partial loss details: symbol, percentToClose, cost, currentPrice, context, backtest flag.
33611
+ * @throws {Error} If the adapter does not implement `onPartialLossCommit`.
33612
+ */
33371
33613
  async onPartialLossCommit(payload) {
33372
33614
  if (this._instance.onPartialLossCommit) {
33373
33615
  await this._instance.onPartialLossCommit(payload);
33616
+ return;
33374
33617
  }
33618
+ throw new Error("BrokerProxy onPartialLossCommit is not implemented");
33375
33619
  }
33620
+ /**
33621
+ * Forwards a trailing stop-loss update event to the underlying adapter.
33622
+ * Throws if the adapter does not implement `onTrailingStopCommit`.
33623
+ *
33624
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag.
33625
+ * @throws {Error} If the adapter does not implement `onTrailingStopCommit`.
33626
+ */
33376
33627
  async onTrailingStopCommit(payload) {
33377
33628
  if (this._instance.onTrailingStopCommit) {
33378
33629
  await this._instance.onTrailingStopCommit(payload);
33630
+ return;
33379
33631
  }
33632
+ throw new Error("BrokerProxy onTrailingStopCommit is not implemented");
33380
33633
  }
33634
+ /**
33635
+ * Forwards a trailing take-profit update event to the underlying adapter.
33636
+ * Throws if the adapter does not implement `onTrailingTakeCommit`.
33637
+ *
33638
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag.
33639
+ * @throws {Error} If the adapter does not implement `onTrailingTakeCommit`.
33640
+ */
33381
33641
  async onTrailingTakeCommit(payload) {
33382
33642
  if (this._instance.onTrailingTakeCommit) {
33383
33643
  await this._instance.onTrailingTakeCommit(payload);
33644
+ return;
33384
33645
  }
33646
+ throw new Error("BrokerProxy onTrailingTakeCommit is not implemented");
33385
33647
  }
33648
+ /**
33649
+ * Forwards a breakeven event to the underlying adapter.
33650
+ * Throws if the adapter does not implement `onBreakevenCommit`.
33651
+ *
33652
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice (= effectivePriceOpen), newTakeProfitPrice, context, backtest flag.
33653
+ * @throws {Error} If the adapter does not implement `onBreakevenCommit`.
33654
+ */
33386
33655
  async onBreakevenCommit(payload) {
33387
33656
  if (this._instance.onBreakevenCommit) {
33388
33657
  await this._instance.onBreakevenCommit(payload);
33658
+ return;
33389
33659
  }
33660
+ throw new Error("BrokerProxy onBreakevenCommit is not implemented");
33390
33661
  }
33662
+ /**
33663
+ * Forwards a DCA average-buy entry event to the underlying adapter.
33664
+ * Throws if the adapter does not implement `onAverageBuyCommit`.
33665
+ *
33666
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag.
33667
+ * @throws {Error} If the adapter does not implement `onAverageBuyCommit`.
33668
+ */
33391
33669
  async onAverageBuyCommit(payload) {
33392
33670
  if (this._instance.onAverageBuyCommit) {
33393
33671
  await this._instance.onAverageBuyCommit(payload);
33672
+ return;
33394
33673
  }
33674
+ throw new Error("BrokerProxy onAverageBuyCommit is not implemented");
33395
33675
  }
33396
33676
  }
33397
33677
  /**
@@ -43163,7 +43443,7 @@ class MemoryLocalInstance {
43163
43443
  * @param value - Value to store and index
43164
43444
  * @param index - Optional BM25 index string; defaults to JSON.stringify(value)
43165
43445
  */
43166
- async writeMemory(memoryId, value, index) {
43446
+ async writeMemory(memoryId, value, description) {
43167
43447
  bt.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_WRITE, {
43168
43448
  signalId: this.signalId,
43169
43449
  bucketName: this.bucketName,
@@ -43172,7 +43452,7 @@ class MemoryLocalInstance {
43172
43452
  this._index.upsert({
43173
43453
  id: memoryId,
43174
43454
  content: value,
43175
- index: index ?? JSON.stringify(value),
43455
+ index: description,
43176
43456
  priority: Date.now(),
43177
43457
  });
43178
43458
  }
@@ -43471,7 +43751,7 @@ class MemoryAdapter {
43471
43751
  * @param dto.value - Value to store
43472
43752
  * @param dto.signalId - Signal identifier
43473
43753
  * @param dto.bucketName - Bucket name
43474
- * @param dto.index - Optional BM25 index string; defaults to JSON.stringify(value)
43754
+ * @param dto.description - Optional BM25 index string; defaults to JSON.stringify(value)
43475
43755
  */
43476
43756
  this.writeMemory = async (dto) => {
43477
43757
  if (!this.enable.hasValue()) {
@@ -43486,7 +43766,7 @@ class MemoryAdapter {
43486
43766
  const isInitial = !this.getInstance.has(key);
43487
43767
  const instance = this.getInstance(dto.signalId, dto.bucketName);
43488
43768
  await instance.waitForInit(isInitial);
43489
- return await instance.writeMemory(dto.memoryId, dto.value, dto.index);
43769
+ return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
43490
43770
  };
43491
43771
  /**
43492
43772
  * Search memory using BM25 full-text scoring.
@@ -43637,7 +43917,7 @@ const REMOVE_MEMORY_METHOD_NAME = "memory.removeMemory";
43637
43917
  * ```
43638
43918
  */
43639
43919
  async function writeMemory(dto) {
43640
- const { bucketName, memoryId, value } = dto;
43920
+ const { bucketName, memoryId, value, description } = dto;
43641
43921
  bt.loggerService.info(WRITE_MEMORY_METHOD_NAME, {
43642
43922
  bucketName,
43643
43923
  memoryId,
@@ -43661,6 +43941,7 @@ async function writeMemory(dto) {
43661
43941
  value,
43662
43942
  signalId: signal.id,
43663
43943
  bucketName,
43944
+ description,
43664
43945
  });
43665
43946
  }
43666
43947
  /**
@@ -44104,7 +44385,7 @@ class DumpMemoryInstance {
44104
44385
  bucketName: this.bucketName,
44105
44386
  signalId: this.signalId,
44106
44387
  value: { messages },
44107
- index: description,
44388
+ description,
44108
44389
  });
44109
44390
  }
44110
44391
  /**
@@ -44125,7 +44406,7 @@ class DumpMemoryInstance {
44125
44406
  bucketName: this.bucketName,
44126
44407
  signalId: this.signalId,
44127
44408
  value: record,
44128
- index: description,
44409
+ description,
44129
44410
  });
44130
44411
  }
44131
44412
  /**
@@ -44147,7 +44428,7 @@ class DumpMemoryInstance {
44147
44428
  bucketName: this.bucketName,
44148
44429
  signalId: this.signalId,
44149
44430
  value: { rows },
44150
- index: description,
44431
+ description,
44151
44432
  });
44152
44433
  }
44153
44434
  /**
@@ -44168,7 +44449,7 @@ class DumpMemoryInstance {
44168
44449
  bucketName: this.bucketName,
44169
44450
  signalId: this.signalId,
44170
44451
  value: { content },
44171
- index: description,
44452
+ description,
44172
44453
  });
44173
44454
  }
44174
44455
  /**
@@ -44189,7 +44470,7 @@ class DumpMemoryInstance {
44189
44470
  bucketName: this.bucketName,
44190
44471
  signalId: this.signalId,
44191
44472
  value: { content },
44192
- index: description,
44473
+ description,
44193
44474
  });
44194
44475
  }
44195
44476
  /**
@@ -44211,7 +44492,7 @@ class DumpMemoryInstance {
44211
44492
  bucketName: this.bucketName,
44212
44493
  signalId: this.signalId,
44213
44494
  value: json,
44214
- index: description,
44495
+ description,
44215
44496
  });
44216
44497
  }
44217
44498
  /** Releases resources held by this instance. */
@@ -50634,6 +50915,8 @@ const INTERVAL_MINUTES = {
50634
50915
  "4h": 240,
50635
50916
  "6h": 360,
50636
50917
  "8h": 480,
50918
+ "1d": 1440,
50919
+ "1w": 10080,
50637
50920
  };
50638
50921
  /**
50639
50922
  * Aligns timestamp down to the nearest interval boundary.
@@ -51708,6 +51991,119 @@ const percentValue = (yesterdayValue, todayValue) => {
51708
51991
  return yesterdayValue / todayValue - 1;
51709
51992
  };
51710
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
+
51711
52107
  exports.ActionBase = ActionBase;
51712
52108
  exports.Backtest = Backtest;
51713
52109
  exports.Breakeven = Breakeven;
@@ -51913,6 +52309,10 @@ exports.toProfitLossDto = toProfitLossDto;
51913
52309
  exports.tpPercentShiftToPrice = tpPercentShiftToPrice;
51914
52310
  exports.tpPriceToPercentShift = tpPriceToPercentShift;
51915
52311
  exports.validate = validate;
52312
+ exports.validateCommonSignal = validateCommonSignal;
52313
+ exports.validatePendingSignal = validatePendingSignal;
52314
+ exports.validateScheduledSignal = validateScheduledSignal;
52315
+ exports.validateSignal = validateSignal;
51916
52316
  exports.waitForCandle = waitForCandle;
51917
52317
  exports.warmCandles = warmCandles;
51918
52318
  exports.writeMemory = writeMemory;