Allow disabling stop loss exits
This commit is contained in:
@@ -74,6 +74,7 @@ TIME_SERIES_PROBE_MIN_PROBABILITY_UP=0.55
|
|||||||
TIME_SERIES_PROBE_SIZE_MULTIPLIER=0.40
|
TIME_SERIES_PROBE_SIZE_MULTIPLIER=0.40
|
||||||
TIME_SERIES_REBOUND_FALLBACK_ENABLED=true
|
TIME_SERIES_REBOUND_FALLBACK_ENABLED=true
|
||||||
STOP_LOSS_PERCENT=0.04
|
STOP_LOSS_PERCENT=0.04
|
||||||
|
STOP_LOSS_EXIT_ENABLED=false
|
||||||
TAKE_PROFIT_PERCENT=0.035
|
TAKE_PROFIT_PERCENT=0.035
|
||||||
TRAILING_STOP_PERCENT=0.015
|
TRAILING_STOP_PERCENT=0.015
|
||||||
MIN_HOLD_SECONDS=180
|
MIN_HOLD_SECONDS=180
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class Settings:
|
|||||||
time_series_probe_size_multiplier: float
|
time_series_probe_size_multiplier: float
|
||||||
time_series_rebound_fallback_enabled: bool
|
time_series_rebound_fallback_enabled: bool
|
||||||
stop_loss_percent: float
|
stop_loss_percent: float
|
||||||
|
stop_loss_exit_enabled: bool
|
||||||
take_profit_percent: float
|
take_profit_percent: float
|
||||||
trailing_stop_percent: float
|
trailing_stop_percent: float
|
||||||
min_hold_seconds: int
|
min_hold_seconds: int
|
||||||
@@ -289,6 +290,7 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
|
|||||||
time_series_probe_size_multiplier=_float_env("TIME_SERIES_PROBE_SIZE_MULTIPLIER", 0.40),
|
time_series_probe_size_multiplier=_float_env("TIME_SERIES_PROBE_SIZE_MULTIPLIER", 0.40),
|
||||||
time_series_rebound_fallback_enabled=_bool_env("TIME_SERIES_REBOUND_FALLBACK_ENABLED", True),
|
time_series_rebound_fallback_enabled=_bool_env("TIME_SERIES_REBOUND_FALLBACK_ENABLED", True),
|
||||||
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04),
|
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04),
|
||||||
|
stop_loss_exit_enabled=_bool_env("STOP_LOSS_EXIT_ENABLED", True),
|
||||||
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
|
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
|
||||||
trailing_stop_percent=_float_env("TRAILING_STOP_PERCENT", 0.015),
|
trailing_stop_percent=_float_env("TRAILING_STOP_PERCENT", 0.015),
|
||||||
min_hold_seconds=_int_env("MIN_HOLD_SECONDS", 180),
|
min_hold_seconds=_int_env("MIN_HOLD_SECONDS", 180),
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
|
|||||||
"time_series_rebound_fallback_enabled": settings.time_series_rebound_fallback_enabled,
|
"time_series_rebound_fallback_enabled": settings.time_series_rebound_fallback_enabled,
|
||||||
"time_series_model_artifact": _time_series_model_artifact(settings),
|
"time_series_model_artifact": _time_series_model_artifact(settings),
|
||||||
"stop_loss_percent": settings.stop_loss_percent,
|
"stop_loss_percent": settings.stop_loss_percent,
|
||||||
|
"stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
|
||||||
"take_profit_percent": settings.take_profit_percent,
|
"take_profit_percent": settings.take_profit_percent,
|
||||||
"trailing_stop_percent": settings.trailing_stop_percent,
|
"trailing_stop_percent": settings.trailing_stop_percent,
|
||||||
"min_hold_seconds": settings.min_hold_seconds,
|
"min_hold_seconds": settings.min_hold_seconds,
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ def _neutral_rules(settings: Settings, reason: str | None = None) -> dict[str, A
|
|||||||
"ema_exit_mode": "normal",
|
"ema_exit_mode": "normal",
|
||||||
"rsi_exit_mode": "normal",
|
"rsi_exit_mode": "normal",
|
||||||
"stop_loss_percent": settings.stop_loss_percent,
|
"stop_loss_percent": settings.stop_loss_percent,
|
||||||
|
"stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
|
||||||
"take_profit_percent": settings.take_profit_percent,
|
"take_profit_percent": settings.take_profit_percent,
|
||||||
"trailing_stop_percent": settings.trailing_stop_percent,
|
"trailing_stop_percent": settings.trailing_stop_percent,
|
||||||
"symbol_threshold_adjustments": {},
|
"symbol_threshold_adjustments": {},
|
||||||
|
|||||||
+41
-14
@@ -320,7 +320,8 @@ class SpotStrategy:
|
|||||||
diagnostics = {
|
diagnostics = {
|
||||||
"price": price,
|
"price": price,
|
||||||
"entry_price": position.entry_price,
|
"entry_price": position.entry_price,
|
||||||
"stop_loss": position.stop_loss,
|
"stop_loss": position.stop_loss if self.settings.stop_loss_exit_enabled else None,
|
||||||
|
"stop_loss_exit_enabled": self.settings.stop_loss_exit_enabled,
|
||||||
"take_profit": position.take_profit,
|
"take_profit": position.take_profit,
|
||||||
"highest_price": position.highest_price,
|
"highest_price": position.highest_price,
|
||||||
"trailing_stop": trailing,
|
"trailing_stop": trailing,
|
||||||
@@ -328,7 +329,7 @@ class SpotStrategy:
|
|||||||
"ema_20": latest.ema_20,
|
"ema_20": latest.ema_20,
|
||||||
"ema_50": latest.ema_50,
|
"ema_50": latest.ema_50,
|
||||||
}
|
}
|
||||||
if price <= position.stop_loss:
|
if self.settings.stop_loss_exit_enabled and price <= position.stop_loss:
|
||||||
return Signal(position.symbol, "SELL", 1.0, "сработал стоп-лосс", diagnostics)
|
return Signal(position.symbol, "SELL", 1.0, "сработал стоп-лосс", diagnostics)
|
||||||
if price >= position.take_profit:
|
if price >= position.take_profit:
|
||||||
return Signal(position.symbol, "SELL", 0.96, "сработал тейк-профит", diagnostics)
|
return Signal(position.symbol, "SELL", 0.96, "сработал тейк-профит", diagnostics)
|
||||||
@@ -389,7 +390,7 @@ class SpotStrategy:
|
|||||||
trailing_percent = _adaptive_percent(
|
trailing_percent = _adaptive_percent(
|
||||||
adaptive, "trailing_stop_percent", self.settings.trailing_stop_percent, 0.003, 0.08
|
adaptive, "trailing_stop_percent", self.settings.trailing_stop_percent, 0.003, 0.08
|
||||||
)
|
)
|
||||||
effective_stop_loss = max(position.stop_loss, position.entry_price * (1 - stop_loss_percent))
|
effective_stop_loss = _effective_stop_loss(self.settings, position, stop_loss_percent)
|
||||||
effective_take_profit = position.entry_price * (1 + take_profit_percent)
|
effective_take_profit = position.entry_price * (1 + take_profit_percent)
|
||||||
trailing = position.trailing_stop(trailing_percent)
|
trailing = position.trailing_stop(trailing_percent)
|
||||||
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, self.settings)
|
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, self.settings)
|
||||||
@@ -397,6 +398,7 @@ class SpotStrategy:
|
|||||||
"price": price,
|
"price": price,
|
||||||
"entry_price": position.entry_price,
|
"entry_price": position.entry_price,
|
||||||
"stop_loss": effective_stop_loss,
|
"stop_loss": effective_stop_loss,
|
||||||
|
"stop_loss_exit_enabled": self.settings.stop_loss_exit_enabled,
|
||||||
"take_profit": effective_take_profit,
|
"take_profit": effective_take_profit,
|
||||||
"highest_price": position.highest_price,
|
"highest_price": position.highest_price,
|
||||||
"trailing_stop": trailing,
|
"trailing_stop": trailing,
|
||||||
@@ -408,7 +410,7 @@ class SpotStrategy:
|
|||||||
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
||||||
"min_exit_profit_percent": float(adaptive.get("min_exit_profit_percent", 0.0) or 0.0),
|
"min_exit_profit_percent": float(adaptive.get("min_exit_profit_percent", 0.0) or 0.0),
|
||||||
}
|
}
|
||||||
if price <= effective_stop_loss:
|
if effective_stop_loss is not None and price <= effective_stop_loss:
|
||||||
return Signal(position.symbol, "SELL", 1.0, "сработал стоп-лосс", diagnostics)
|
return Signal(position.symbol, "SELL", 1.0, "сработал стоп-лосс", diagnostics)
|
||||||
if price >= effective_take_profit:
|
if price >= effective_take_profit:
|
||||||
return Signal(position.symbol, "SELL", 0.96, "сработал тейк-профит", diagnostics)
|
return Signal(position.symbol, "SELL", 0.96, "сработал тейк-профит", diagnostics)
|
||||||
@@ -580,11 +582,9 @@ def _trend_macd_exit_signal(
|
|||||||
previous = candles[-2]
|
previous = candles[-2]
|
||||||
price = ticker.last_price
|
price = ticker.last_price
|
||||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||||
effective_stop_loss = max(position.stop_loss, position.entry_price * (1 - stop_loss_percent))
|
effective_stop_loss = _effective_stop_loss(settings, position, stop_loss_percent)
|
||||||
atr_multiplier = _clamp(settings.atr_trailing_multiplier, 0.5, 10.0)
|
atr_multiplier = _clamp(settings.atr_trailing_multiplier, 0.5, 10.0)
|
||||||
atr_trailing_stop = None
|
atr_trailing_stop = _atr_trailing_stop(settings, position, latest.atr_14, atr_multiplier, effective_stop_loss)
|
||||||
if latest.atr_14 is not None and position.highest_price > position.entry_price:
|
|
||||||
atr_trailing_stop = max(effective_stop_loss, position.highest_price - latest.atr_14 * atr_multiplier)
|
|
||||||
macd_cross_down = _macd_crossed_down(previous, latest)
|
macd_cross_down = _macd_crossed_down(previous, latest)
|
||||||
close_below_ema50 = latest.ema_50 is not None and latest.close < latest.ema_50
|
close_below_ema50 = latest.ema_50 is not None and latest.close < latest.ema_50
|
||||||
diagnostics = {
|
diagnostics = {
|
||||||
@@ -592,6 +592,7 @@ def _trend_macd_exit_signal(
|
|||||||
"price": price,
|
"price": price,
|
||||||
"entry_price": position.entry_price,
|
"entry_price": position.entry_price,
|
||||||
"stop_loss": effective_stop_loss,
|
"stop_loss": effective_stop_loss,
|
||||||
|
"stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
|
||||||
"atr_trailing_stop": atr_trailing_stop,
|
"atr_trailing_stop": atr_trailing_stop,
|
||||||
"atr_trailing_multiplier": atr_multiplier,
|
"atr_trailing_multiplier": atr_multiplier,
|
||||||
"highest_price": position.highest_price,
|
"highest_price": position.highest_price,
|
||||||
@@ -603,7 +604,7 @@ def _trend_macd_exit_signal(
|
|||||||
"macd_cross_down": macd_cross_down,
|
"macd_cross_down": macd_cross_down,
|
||||||
"close_below_ema50": close_below_ema50,
|
"close_below_ema50": close_below_ema50,
|
||||||
}
|
}
|
||||||
if price <= effective_stop_loss:
|
if effective_stop_loss is not None and price <= effective_stop_loss:
|
||||||
return Signal(position.symbol, "SELL", 1.0, "trend_macd: сработал стоп-лосс", diagnostics)
|
return Signal(position.symbol, "SELL", 1.0, "trend_macd: сработал стоп-лосс", diagnostics)
|
||||||
if atr_trailing_stop is not None and price <= atr_trailing_stop:
|
if atr_trailing_stop is not None and price <= atr_trailing_stop:
|
||||||
return Signal(position.symbol, "SELL", 0.94, "trend_macd: сработал ATR trailing stop", diagnostics)
|
return Signal(position.symbol, "SELL", 0.94, "trend_macd: сработал ATR trailing stop", diagnostics)
|
||||||
@@ -859,11 +860,15 @@ def _torch_forecast_exit_signal(
|
|||||||
latest = candles[-1] if candles else None
|
latest = candles[-1] if candles else None
|
||||||
price = ticker.last_price
|
price = ticker.last_price
|
||||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||||
effective_stop_loss = max(position.stop_loss, position.entry_price * (1 - stop_loss_percent))
|
effective_stop_loss = _effective_stop_loss(settings, position, stop_loss_percent)
|
||||||
atr_multiplier = _clamp(settings.atr_trailing_multiplier, 0.5, 10.0)
|
atr_multiplier = _clamp(settings.atr_trailing_multiplier, 0.5, 10.0)
|
||||||
atr_trailing_stop = None
|
atr_trailing_stop = _atr_trailing_stop(
|
||||||
if latest and latest.atr_14 is not None and position.highest_price > position.entry_price:
|
settings,
|
||||||
atr_trailing_stop = max(effective_stop_loss, position.highest_price - latest.atr_14 * atr_multiplier)
|
position,
|
||||||
|
latest.atr_14 if latest else None,
|
||||||
|
atr_multiplier,
|
||||||
|
effective_stop_loss,
|
||||||
|
)
|
||||||
|
|
||||||
expected_return = _safe_float(forecast.get("expected_return_percent"), 0.0)
|
expected_return = _safe_float(forecast.get("expected_return_percent"), 0.0)
|
||||||
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
||||||
@@ -879,6 +884,7 @@ def _torch_forecast_exit_signal(
|
|||||||
"price": price,
|
"price": price,
|
||||||
"entry_price": position.entry_price,
|
"entry_price": position.entry_price,
|
||||||
"stop_loss": effective_stop_loss,
|
"stop_loss": effective_stop_loss,
|
||||||
|
"stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
|
||||||
"take_profit": position.take_profit,
|
"take_profit": position.take_profit,
|
||||||
"atr_trailing_stop": atr_trailing_stop,
|
"atr_trailing_stop": atr_trailing_stop,
|
||||||
"atr_trailing_multiplier": atr_multiplier,
|
"atr_trailing_multiplier": atr_multiplier,
|
||||||
@@ -895,7 +901,7 @@ def _torch_forecast_exit_signal(
|
|||||||
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
||||||
"atr_14": latest.atr_14 if latest else None,
|
"atr_14": latest.atr_14 if latest else None,
|
||||||
}
|
}
|
||||||
if price <= effective_stop_loss:
|
if effective_stop_loss is not None and price <= effective_stop_loss:
|
||||||
return Signal(position.symbol, "SELL", 1.0, "torch_forecast: stop-loss hit", diagnostics)
|
return Signal(position.symbol, "SELL", 1.0, "torch_forecast: stop-loss hit", diagnostics)
|
||||||
if price >= position.take_profit:
|
if price >= position.take_profit:
|
||||||
return Signal(position.symbol, "SELL", 0.96, "torch_forecast: take-profit hit", diagnostics)
|
return Signal(position.symbol, "SELL", 0.96, "torch_forecast: take-profit hit", diagnostics)
|
||||||
@@ -1565,6 +1571,27 @@ def _adaptive_percent(adaptive: dict, key: str, default: float, low: float, high
|
|||||||
return _clamp(_safe_float(adaptive.get(key), default), low, high)
|
return _clamp(_safe_float(adaptive.get(key), default), low, high)
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_stop_loss(settings: Settings, position: Position, stop_loss_percent: float) -> float | None:
|
||||||
|
if not settings.stop_loss_exit_enabled:
|
||||||
|
return None
|
||||||
|
return max(position.stop_loss, position.entry_price * (1 - stop_loss_percent))
|
||||||
|
|
||||||
|
|
||||||
|
def _atr_trailing_stop(
|
||||||
|
settings: Settings,
|
||||||
|
position: Position,
|
||||||
|
atr: float | None,
|
||||||
|
atr_multiplier: float,
|
||||||
|
effective_stop_loss: float | None,
|
||||||
|
) -> float | None:
|
||||||
|
if atr is None or atr <= 0 or position.highest_price <= position.entry_price:
|
||||||
|
return None
|
||||||
|
raw_stop = position.highest_price - atr * atr_multiplier
|
||||||
|
if settings.stop_loss_exit_enabled and effective_stop_loss is not None:
|
||||||
|
return max(effective_stop_loss, raw_stop)
|
||||||
|
return raw_stop if raw_stop > position.entry_price else None
|
||||||
|
|
||||||
|
|
||||||
def _estimated_exit_net_percent(position: Position, price: float, settings: Settings) -> float:
|
def _estimated_exit_net_percent(position: Position, price: float, settings: Settings) -> float:
|
||||||
if position.entry_price <= 0:
|
if position.entry_price <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ def make_settings():
|
|||||||
time_series_probe_size_multiplier=0.40,
|
time_series_probe_size_multiplier=0.40,
|
||||||
time_series_rebound_fallback_enabled=True,
|
time_series_rebound_fallback_enabled=True,
|
||||||
stop_loss_percent=0.02,
|
stop_loss_percent=0.02,
|
||||||
|
stop_loss_exit_enabled=True,
|
||||||
take_profit_percent=0.035,
|
take_profit_percent=0.035,
|
||||||
trailing_stop_percent=0.015,
|
trailing_stop_percent=0.015,
|
||||||
min_hold_seconds=180,
|
min_hold_seconds=180,
|
||||||
|
|||||||
@@ -233,6 +233,90 @@ def test_trend_macd_exits_on_atr_trailing_stop(make_settings, tmp_path) -> None:
|
|||||||
assert "ATR trailing" in signal.reason
|
assert "ATR trailing" in signal.reason
|
||||||
|
|
||||||
|
|
||||||
|
def test_trend_macd_holds_below_stop_when_stop_loss_exit_disabled(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(
|
||||||
|
tmp_path,
|
||||||
|
strategy_mode="trend_macd",
|
||||||
|
stop_loss_exit_enabled=False,
|
||||||
|
atr_trailing_multiplier=2.2,
|
||||||
|
)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
candles = _trend_entry_candles(close=99.0, ema50=95.0, macd_cross_up=False)
|
||||||
|
candles[-2].macd = 0.1
|
||||||
|
candles[-2].macd_signal = 0.0
|
||||||
|
candles[-1].macd = 0.1
|
||||||
|
candles[-1].macd_signal = 0.0
|
||||||
|
candles[-1].atr_14 = 1.0
|
||||||
|
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 100.5)
|
||||||
|
ticker = Ticker("BTCUSDT", 95.5, 95.49, 95.51, 1_000_000, 100, 0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(position, candles, ticker)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["stop_loss"] is None
|
||||||
|
assert signal.diagnostics["stop_loss_exit_enabled"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_atr_trailing_without_stop_loss_waits_for_profit_stop(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(
|
||||||
|
tmp_path,
|
||||||
|
strategy_mode="torch_forecast",
|
||||||
|
stop_loss_exit_enabled=False,
|
||||||
|
atr_trailing_multiplier=2.2,
|
||||||
|
)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
candles = _trend_entry_candles(close=99.0, ema50=95.0, macd_cross_up=False)
|
||||||
|
candles[-1].atr_14 = 1.0
|
||||||
|
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 101.0)
|
||||||
|
ticker = Ticker("BTCUSDT", 98.7, 98.69, 98.71, 1_000_000, 100, 0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
candles,
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": 0.4,
|
||||||
|
"probability_up": 0.62,
|
||||||
|
"skill": 0.18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["atr_trailing_stop"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_atr_trailing_without_stop_loss_can_lock_profit(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(
|
||||||
|
tmp_path,
|
||||||
|
strategy_mode="torch_forecast",
|
||||||
|
stop_loss_exit_enabled=False,
|
||||||
|
atr_trailing_multiplier=2.2,
|
||||||
|
)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
candles = _trend_entry_candles(close=104.0, ema50=95.0, macd_cross_up=False)
|
||||||
|
candles[-1].atr_14 = 1.0
|
||||||
|
position = Position(1, "BTCUSDT", 1, 100, 100, 0.1, 96, 120, 104.0)
|
||||||
|
ticker = Ticker("BTCUSDT", 101.7, 101.69, 101.71, 1_000_000, 100, 0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
candles,
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": 0.4,
|
||||||
|
"probability_up": 0.62,
|
||||||
|
"skill": 0.18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "SELL"
|
||||||
|
assert "ATR trailing" in signal.reason
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_buys_only_from_positive_torch_edge(make_settings, tmp_path) -> None:
|
def test_torch_forecast_buys_only_from_positive_torch_edge(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(
|
settings = make_settings(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ def _full_backtest(
|
|||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
max_hold = max(12, horizon * 8)
|
max_hold = max(12, horizon * 8)
|
||||||
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
|
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
|
||||||
|
stop_loss_exit_enabled = bool(getattr(settings, "stop_loss_exit_enabled", True))
|
||||||
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
|
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
|
||||||
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
|
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
|
||||||
position = positions.get(record.symbol)
|
position = positions.get(record.symbol)
|
||||||
@@ -576,10 +577,15 @@ def _full_backtest(
|
|||||||
position["highest"] = max(position["highest"], record.close)
|
position["highest"] = max(position["highest"], record.close)
|
||||||
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
|
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
|
||||||
held = record.index - int(position["entry_index"])
|
held = record.index - int(position["entry_index"])
|
||||||
atr_stop = (
|
atr_stop_level = (
|
||||||
record.close <= position["highest"] - record.atr * atr_multiplier
|
position["highest"] - record.atr * atr_multiplier
|
||||||
if record.atr > 0 and position["highest"] > position["entry_price"]
|
if record.atr > 0 and position["highest"] > position["entry_price"]
|
||||||
else False
|
else None
|
||||||
|
)
|
||||||
|
atr_stop = bool(
|
||||||
|
atr_stop_level is not None
|
||||||
|
and record.close <= atr_stop_level
|
||||||
|
and (stop_loss_exit_enabled or atr_stop_level > position["entry_price"])
|
||||||
)
|
)
|
||||||
weak_forecast = (
|
weak_forecast = (
|
||||||
record.expected_percent < thresholds.edge
|
record.expected_percent < thresholds.edge
|
||||||
@@ -587,7 +593,7 @@ def _full_backtest(
|
|||||||
or record.skill <= 0.0
|
or record.skill <= 0.0
|
||||||
)
|
)
|
||||||
exit_reason = ""
|
exit_reason = ""
|
||||||
if net_percent <= -stop_loss_percent:
|
if stop_loss_exit_enabled and net_percent <= -stop_loss_percent:
|
||||||
exit_reason = "stop_loss"
|
exit_reason = "stop_loss"
|
||||||
elif atr_stop:
|
elif atr_stop:
|
||||||
exit_reason = "atr_trailing_stop"
|
exit_reason = "atr_trailing_stop"
|
||||||
@@ -663,6 +669,7 @@ def _benchmark_backtest(
|
|||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
max_hold = max(12, horizon * 8)
|
max_hold = max(12, horizon * 8)
|
||||||
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
|
stop_loss_percent = max(0.003, min(0.08, float(settings.stop_loss_percent))) * 100.0
|
||||||
|
stop_loss_exit_enabled = bool(getattr(settings, "stop_loss_exit_enabled", True))
|
||||||
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
|
atr_multiplier = max(0.5, min(10.0, float(settings.atr_trailing_multiplier)))
|
||||||
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
|
for record in sorted(records, key=lambda item: (item.timestamp, item.symbol)):
|
||||||
position = positions.get(record.symbol)
|
position = positions.get(record.symbol)
|
||||||
@@ -670,13 +677,18 @@ def _benchmark_backtest(
|
|||||||
position["highest"] = max(position["highest"], record.close)
|
position["highest"] = max(position["highest"], record.close)
|
||||||
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
|
net_percent = _net_percent(position["entry_price"], record.close, round_trip_cost)
|
||||||
held = record.index - int(position["entry_index"])
|
held = record.index - int(position["entry_index"])
|
||||||
atr_stop = (
|
atr_stop_level = (
|
||||||
record.close <= position["highest"] - record.atr * atr_multiplier
|
position["highest"] - record.atr * atr_multiplier
|
||||||
if record.atr > 0 and position["highest"] > position["entry_price"]
|
if record.atr > 0 and position["highest"] > position["entry_price"]
|
||||||
else False
|
else None
|
||||||
|
)
|
||||||
|
atr_stop = bool(
|
||||||
|
atr_stop_level is not None
|
||||||
|
and record.close <= atr_stop_level
|
||||||
|
and (stop_loss_exit_enabled or atr_stop_level > position["entry_price"])
|
||||||
)
|
)
|
||||||
exit_reason = ""
|
exit_reason = ""
|
||||||
if net_percent <= -stop_loss_percent:
|
if stop_loss_exit_enabled and net_percent <= -stop_loss_percent:
|
||||||
exit_reason = "stop_loss"
|
exit_reason = "stop_loss"
|
||||||
elif atr_stop:
|
elif atr_stop:
|
||||||
exit_reason = "atr_trailing_stop"
|
exit_reason = "atr_trailing_stop"
|
||||||
|
|||||||
Reference in New Issue
Block a user