Train Torch model for 12 spot pairs
This commit is contained in:
@@ -284,6 +284,50 @@ def test_torch_forecast_blocks_without_valid_torch_model(make_settings, tmp_path
|
||||
assert signal.diagnostics["checks"]["torch_model_ok"] is False
|
||||
|
||||
|
||||
def test_torch_forecast_allows_additional_entries_until_symbol_limit(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
strategy_mode="torch_forecast",
|
||||
min_position_usdt=1,
|
||||
max_symbol_exposure_usdt=3,
|
||||
max_positions_per_symbol=3,
|
||||
max_position_usdt=25,
|
||||
stop_loss_percent=0.04,
|
||||
risk_per_trade_percent=0.01,
|
||||
)
|
||||
strategy = SpotStrategy(settings)
|
||||
ticker = Ticker("BTCUSDT", 105, 104.99, 105.01, 10_000_000, 1000, 1.0)
|
||||
forecast = {
|
||||
"usable": True,
|
||||
"model": "torch_gru",
|
||||
"expected_return_percent": 0.36,
|
||||
"probability_up": 0.66,
|
||||
"skill": 0.22,
|
||||
"block_entry": False,
|
||||
}
|
||||
|
||||
additional = strategy.entry_signal(
|
||||
"BTCUSDT",
|
||||
[],
|
||||
ticker,
|
||||
open_positions_for_symbol=1,
|
||||
forecast=forecast,
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
capped = strategy.entry_signal(
|
||||
"BTCUSDT",
|
||||
[],
|
||||
ticker,
|
||||
open_positions_for_symbol=3,
|
||||
forecast=forecast,
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
|
||||
assert additional.action == "BUY"
|
||||
assert capped.action == "HOLD"
|
||||
assert "symbol position limit" in capped.reason
|
||||
|
||||
|
||||
def test_torch_forecast_blocks_failed_quality_gate(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
@@ -392,6 +436,190 @@ def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp
|
||||
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_overlay_buys_stabilized_drop(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
strategy_mode="torch_forecast",
|
||||
rebound_trading_enabled=True,
|
||||
rebound_entry_confidence=0.55,
|
||||
rebound_min_probability=0.55,
|
||||
rebound_max_position_usdt=6.0,
|
||||
time_series_min_edge_percent=0.10,
|
||||
time_series_probe_min_probability_up=0.55,
|
||||
max_position_usdt=25,
|
||||
stop_loss_percent=0.04,
|
||||
risk_per_trade_percent=0.01,
|
||||
)
|
||||
strategy = SpotStrategy(settings)
|
||||
candles = _rebound_candles()
|
||||
ticker = Ticker(
|
||||
symbol="BTCUSDT",
|
||||
last_price=candles[-1].close,
|
||||
bid=candles[-1].close * 0.9999,
|
||||
ask=candles[-1].close * 1.0001,
|
||||
turnover_24h=10_000_000,
|
||||
volume_24h=1000,
|
||||
change_24h=-2.0,
|
||||
)
|
||||
|
||||
signal = strategy.entry_signal(
|
||||
"BTCUSDT",
|
||||
candles,
|
||||
ticker,
|
||||
open_positions_for_symbol=0,
|
||||
pattern={"label": "нисходящий тренд", "score": 0.28},
|
||||
forecast={
|
||||
"usable": True,
|
||||
"model": "torch_gru",
|
||||
"expected_return_percent": 0.01,
|
||||
"probability_up": 0.56,
|
||||
"skill": 0.05,
|
||||
"block_entry": False,
|
||||
},
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
|
||||
assert signal.action == "BUY"
|
||||
assert signal.diagnostics["entry_path"] == "rebound"
|
||||
assert signal.diagnostics["rebound"]["active"] is True
|
||||
assert signal.diagnostics["edge_mode"] == "rebound"
|
||||
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
|
||||
assert signal.diagnostics["position_notional_usdt"] <= settings.rebound_max_position_usdt
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_overlay_does_not_buy_negative_forecast(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
strategy_mode="torch_forecast",
|
||||
rebound_trading_enabled=True,
|
||||
rebound_entry_confidence=0.55,
|
||||
rebound_min_probability=0.55,
|
||||
time_series_min_edge_percent=0.10,
|
||||
time_series_probe_min_probability_up=0.55,
|
||||
)
|
||||
strategy = SpotStrategy(settings)
|
||||
candles = _rebound_candles()
|
||||
ticker = Ticker(
|
||||
symbol="BTCUSDT",
|
||||
last_price=candles[-1].close,
|
||||
bid=candles[-1].close * 0.9999,
|
||||
ask=candles[-1].close * 1.0001,
|
||||
turnover_24h=10_000_000,
|
||||
volume_24h=1000,
|
||||
change_24h=-2.0,
|
||||
)
|
||||
|
||||
signal = strategy.entry_signal(
|
||||
"BTCUSDT",
|
||||
candles,
|
||||
ticker,
|
||||
open_positions_for_symbol=0,
|
||||
pattern={"label": "нисходящий тренд", "score": 0.28},
|
||||
forecast={
|
||||
"usable": True,
|
||||
"model": "torch_gru",
|
||||
"expected_return_percent": -0.01,
|
||||
"probability_up": 0.56,
|
||||
"skill": 0.05,
|
||||
"block_entry": False,
|
||||
},
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
|
||||
assert signal.action == "HOLD"
|
||||
assert signal.diagnostics["rebound"]["active"] is True
|
||||
assert signal.diagnostics["rebound_entry_ok"] is False
|
||||
assert signal.diagnostics["edge_mode"] == "blocked"
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_fallback_buys_when_symbol_has_no_model(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
strategy_mode="torch_forecast",
|
||||
rebound_trading_enabled=True,
|
||||
rebound_entry_confidence=0.55,
|
||||
rebound_min_probability=0.55,
|
||||
rebound_max_position_usdt=6.0,
|
||||
time_series_rebound_fallback_enabled=True,
|
||||
stop_loss_percent=0.04,
|
||||
risk_per_trade_percent=0.01,
|
||||
)
|
||||
strategy = SpotStrategy(settings)
|
||||
candles = _rebound_candles()
|
||||
ticker = Ticker(
|
||||
symbol="HYPEUSDT",
|
||||
last_price=candles[-1].close,
|
||||
bid=candles[-1].close * 0.9999,
|
||||
ask=candles[-1].close * 1.0001,
|
||||
turnover_24h=10_000_000,
|
||||
volume_24h=1000,
|
||||
change_24h=-2.0,
|
||||
)
|
||||
|
||||
signal = strategy.entry_signal(
|
||||
"HYPEUSDT",
|
||||
candles,
|
||||
ticker,
|
||||
open_positions_for_symbol=0,
|
||||
pattern={"label": "нисходящий тренд", "score": 0.28},
|
||||
forecast={
|
||||
"usable": False,
|
||||
"model": "none",
|
||||
"reason": "no valid PyTorch LSTM/GRU model for symbol",
|
||||
"block_entry": False,
|
||||
},
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
|
||||
assert signal.action == "BUY"
|
||||
assert signal.diagnostics["entry_path"] == "rebound_fallback"
|
||||
assert signal.diagnostics["fallback_rebound_entry_ok"] is True
|
||||
assert signal.diagnostics["missing_torch_model"] is True
|
||||
assert signal.diagnostics["edge_mode"] == "rebound_fallback"
|
||||
assert signal.diagnostics["position_notional_usdt"] <= settings.rebound_max_position_usdt
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_fallback_can_be_disabled(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
strategy_mode="torch_forecast",
|
||||
rebound_trading_enabled=True,
|
||||
rebound_entry_confidence=0.55,
|
||||
rebound_min_probability=0.55,
|
||||
time_series_rebound_fallback_enabled=False,
|
||||
)
|
||||
strategy = SpotStrategy(settings)
|
||||
candles = _rebound_candles()
|
||||
ticker = Ticker(
|
||||
symbol="HYPEUSDT",
|
||||
last_price=candles[-1].close,
|
||||
bid=candles[-1].close * 0.9999,
|
||||
ask=candles[-1].close * 1.0001,
|
||||
turnover_24h=10_000_000,
|
||||
volume_24h=1000,
|
||||
change_24h=-2.0,
|
||||
)
|
||||
|
||||
signal = strategy.entry_signal(
|
||||
"HYPEUSDT",
|
||||
candles,
|
||||
ticker,
|
||||
open_positions_for_symbol=0,
|
||||
pattern={"label": "нисходящий тренд", "score": 0.28},
|
||||
forecast={
|
||||
"usable": False,
|
||||
"model": "none",
|
||||
"reason": "no valid PyTorch LSTM/GRU model for symbol",
|
||||
"block_entry": False,
|
||||
},
|
||||
account={"equity": 100.0},
|
||||
)
|
||||
|
||||
assert signal.action == "HOLD"
|
||||
assert signal.diagnostics["missing_torch_model"] is True
|
||||
assert signal.diagnostics["fallback_rebound_entry_ok"] is False
|
||||
|
||||
|
||||
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)
|
||||
strategy = SpotStrategy(settings)
|
||||
@@ -416,6 +644,74 @@ def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_pa
|
||||
assert "torch_forecast" in signal.reason
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
|
||||
strategy = SpotStrategy(settings)
|
||||
position = Position(
|
||||
1,
|
||||
"XRPUSDT",
|
||||
5,
|
||||
1.0,
|
||||
5.0,
|
||||
0.005,
|
||||
0.96,
|
||||
1.035,
|
||||
1.0,
|
||||
entry_diagnostics={"entry_path": "rebound_fallback", "edge_mode": "rebound_fallback"},
|
||||
)
|
||||
ticker = Ticker("XRPUSDT", 1.001, 1.0009, 1.0011, 10_000_000, 1000, 1.0)
|
||||
|
||||
signal = strategy.exit_signal(
|
||||
position,
|
||||
_trend_entry_candles(close=1.0, ema50=0.98),
|
||||
ticker,
|
||||
forecast={
|
||||
"usable": False,
|
||||
"model": "none",
|
||||
"reason": "no valid PyTorch LSTM/GRU model for symbol",
|
||||
"block_entry": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert signal.action == "HOLD"
|
||||
assert signal.diagnostics["rebound_fallback_position"] is True
|
||||
assert "rebound fallback" in signal.reason
|
||||
|
||||
|
||||
def test_torch_forecast_rebound_fallback_still_sells_take_profit(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(tmp_path, strategy_mode="torch_forecast")
|
||||
strategy = SpotStrategy(settings)
|
||||
position = Position(
|
||||
1,
|
||||
"XRPUSDT",
|
||||
5,
|
||||
1.0,
|
||||
5.0,
|
||||
0.005,
|
||||
0.96,
|
||||
1.035,
|
||||
1.04,
|
||||
opened_at=utc_now() - timedelta(seconds=600),
|
||||
entry_diagnostics={"entry_path": "rebound_fallback", "edge_mode": "rebound_fallback"},
|
||||
)
|
||||
ticker = Ticker("XRPUSDT", 1.036, 1.0359, 1.0361, 10_000_000, 1000, 1.0)
|
||||
|
||||
signal = strategy.exit_signal(
|
||||
position,
|
||||
_trend_entry_candles(close=1.0, ema50=0.98),
|
||||
ticker,
|
||||
forecast={
|
||||
"usable": False,
|
||||
"model": "none",
|
||||
"reason": "no valid PyTorch LSTM/GRU model for symbol",
|
||||
"block_entry": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert signal.action == "SELL"
|
||||
assert "take-profit" in signal.reason
|
||||
|
||||
|
||||
def test_strategy_emits_buy_when_score_passes_threshold(make_settings, tmp_path) -> None:
|
||||
settings = make_settings(tmp_path)
|
||||
strategy = SpotStrategy(settings)
|
||||
|
||||
Reference in New Issue
Block a user