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