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