Prevent torch forecast fee churn exits
This commit is contained in:
@@ -901,6 +901,9 @@ 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,
|
||||||
}
|
}
|
||||||
|
hold_seconds = (utc_now() - position.opened_at).total_seconds()
|
||||||
|
diagnostics["hold_seconds"] = hold_seconds
|
||||||
|
diagnostics["min_hold_seconds"] = settings.min_hold_seconds
|
||||||
if effective_stop_loss is not None and 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:
|
||||||
@@ -928,12 +931,33 @@ def _torch_forecast_exit_signal(
|
|||||||
)
|
)
|
||||||
return Signal(position.symbol, "SELL", 0.78, "torch_forecast: no valid PyTorch forecast to hold", diagnostics)
|
return Signal(position.symbol, "SELL", 0.78, "torch_forecast: no valid PyTorch forecast to hold", diagnostics)
|
||||||
if bool(forecast.get("block_entry", False)) or expected_return <= 0.0 or probability_up <= 0.50:
|
if bool(forecast.get("block_entry", False)) or expected_return <= 0.0 or probability_up <= 0.50:
|
||||||
|
if hold_seconds < settings.min_hold_seconds:
|
||||||
|
diagnostics["forecast_exit_blocked_by_min_hold"] = True
|
||||||
|
return Signal(
|
||||||
|
position.symbol,
|
||||||
|
"HOLD",
|
||||||
|
0.46,
|
||||||
|
"torch_forecast: minimum hold protects against fee churn",
|
||||||
|
diagnostics,
|
||||||
|
)
|
||||||
|
forecast_exit = _forecast_exit_signal(
|
||||||
|
forecast=forecast,
|
||||||
|
position=position,
|
||||||
|
price=price,
|
||||||
|
estimated_exit_net_percent=estimated_exit_net_percent,
|
||||||
|
stop_loss_percent=stop_loss_percent,
|
||||||
|
min_edge_percent=min_edge,
|
||||||
|
)
|
||||||
|
if forecast_exit is not None:
|
||||||
|
action, confidence, reason = forecast_exit
|
||||||
|
return Signal(position.symbol, action, confidence, reason, diagnostics)
|
||||||
|
diagnostics["forecast_exit_blocked_by_cost"] = True
|
||||||
return Signal(
|
return Signal(
|
||||||
position.symbol,
|
position.symbol,
|
||||||
"SELL",
|
"HOLD",
|
||||||
0.86,
|
0.44,
|
||||||
(
|
(
|
||||||
"torch_forecast: PyTorch forecast turned negative; "
|
"torch_forecast: forecast weakened, but exit is not worth fees; "
|
||||||
f"p_up={probability_up:.3f}, expected={expected_return:.4f}%"
|
f"p_up={probability_up:.3f}, expected={expected_return:.4f}%"
|
||||||
),
|
),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
|||||||
+72
-2
@@ -826,7 +826,18 @@ def test_torch_forecast_rebound_fallback_can_be_disabled(make_settings, tmp_path
|
|||||||
def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_path) -> None:
|
def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(tmp_path, strategy_mode="torch_forecast", stop_loss_percent=0.04)
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", stop_loss_percent=0.04)
|
||||||
strategy = SpotStrategy(settings)
|
strategy = SpotStrategy(settings)
|
||||||
position = Position(1, "SOLUSDT", 1, 100, 100, 0.1, 96, 120, 103)
|
position = Position(
|
||||||
|
1,
|
||||||
|
"SOLUSDT",
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
0.1,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
103,
|
||||||
|
opened_at=utc_now() - timedelta(seconds=600),
|
||||||
|
)
|
||||||
ticker = Ticker("SOLUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)
|
ticker = Ticker("SOLUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)
|
||||||
|
|
||||||
signal = strategy.exit_signal(
|
signal = strategy.exit_signal(
|
||||||
@@ -844,7 +855,66 @@ def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_pa
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert signal.action == "SELL"
|
assert signal.action == "SELL"
|
||||||
assert "torch_forecast" in signal.reason
|
assert "прогноз" in signal.reason
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_holds_negative_forecast_during_min_hold(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
position = Position(1, "SOLUSDT", 1, 100, 100, 0.1, 96, 120, 103)
|
||||||
|
ticker = Ticker("SOLUSDT", 101, 100.99, 101.01, 10_000_000, 1000, 1.0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
_trend_entry_candles(),
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": -0.08,
|
||||||
|
"probability_up": 0.43,
|
||||||
|
"skill": 0.18,
|
||||||
|
"block_entry": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["forecast_exit_blocked_by_min_hold"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_holds_fee_churn_exit_after_min_hold(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
position = Position(
|
||||||
|
1,
|
||||||
|
"BTCUSDT",
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
0.1,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
100.1,
|
||||||
|
opened_at=utc_now() - timedelta(seconds=600),
|
||||||
|
)
|
||||||
|
ticker = Ticker("BTCUSDT", 99.99, 99.98, 100.0, 10_000_000, 1000, 1.0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
_trend_entry_candles(),
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": -0.01,
|
||||||
|
"probability_up": 0.499,
|
||||||
|
"skill": 0.18,
|
||||||
|
"block_entry": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["forecast_exit_blocked_by_cost"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user