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