from __future__ import annotations from crypto_spot_bot.analytics import risk_guard_snapshot from crypto_spot_bot.data_quality import analyze_symbol_quality from crypto_spot_bot.models import Candle, Ticker, Trade, utc_now from crypto_spot_bot.storage import Storage def test_risk_guard_reduces_size_after_consecutive_losses(make_settings, tmp_path) -> None: settings = make_settings(tmp_path, risk_max_consecutive_losses=2) storage = Storage(settings.database_path) now = utc_now() for _ in range(2): storage.insert_trade( Trade( id=None, symbol="BTCUSDT", side="SELL", qty=1.0, entry_price=100.0, exit_price=99.0, net_pnl=-1.0, opened_at=now, closed_at=now, entry_diagnostics={"forecast": {"probability_up": 0.64, "model": "torch_gru"}}, ) ) guard = risk_guard_snapshot(settings, storage.closed_trades(), storage.latest_equity()) assert guard["block_new_entries"] is False assert "consecutive_losses" in guard["reasons"] assert guard["position_size_multiplier"] == settings.risk_reduce_multiplier def test_risk_guard_blocks_only_bad_symbol(make_settings, tmp_path) -> None: settings = make_settings( tmp_path, risk_symbol_guard_enabled=True, risk_max_consecutive_losses=3, symbols=["BTCUSDT", "ETHUSDT"], ) storage = Storage(settings.database_path) now = utc_now() for _ in range(3): storage.insert_trade( Trade( id=None, symbol="BTCUSDT", side="SELL", qty=1.0, entry_price=100.0, exit_price=99.0, net_pnl=-1.0, opened_at=now, closed_at=now, entry_diagnostics={"forecast": {"probability_up": 0.64, "model": "torch_gru"}}, ) ) storage.insert_trade( Trade( id=None, symbol="ETHUSDT", side="SELL", qty=1.0, entry_price=100.0, exit_price=102.0, net_pnl=2.0, opened_at=now, closed_at=now, entry_diagnostics={"forecast": {"probability_up": 0.64, "model": "torch_gru"}}, ) ) guard = risk_guard_snapshot(settings, storage.closed_trades(), storage.latest_equity()) assert guard["block_new_entries"] is False assert guard["blocked_symbols"] == ["BTCUSDT"] symbol_rows = {row["symbol"]: row for row in guard["symbols"]} assert symbol_rows["BTCUSDT"]["block_new_entries"] is True assert symbol_rows["ETHUSDT"]["block_new_entries"] is False def test_risk_guard_can_disable_symbol_blocks(make_settings, tmp_path) -> None: settings = make_settings( tmp_path, risk_symbol_guard_enabled=False, risk_max_consecutive_losses=3, symbols=["BTCUSDT", "ETHUSDT"], ) storage = Storage(settings.database_path) now = utc_now() for _ in range(3): storage.insert_trade( Trade( id=None, symbol="BTCUSDT", side="SELL", qty=1.0, entry_price=100.0, exit_price=99.0, net_pnl=-1.0, opened_at=now, closed_at=now, entry_diagnostics={"forecast": {"probability_up": 0.64, "model": "torch_gru"}}, ) ) guard = risk_guard_snapshot(settings, storage.closed_trades(), storage.latest_equity()) assert guard["symbol_guard_enabled"] is False assert guard["blocked_symbols"] == [] symbol_rows = {row["symbol"]: row for row in guard["symbols"]} assert symbol_rows["BTCUSDT"]["consecutive_losses"] == 3 assert symbol_rows["BTCUSDT"]["block_new_entries"] is False assert symbol_rows["BTCUSDT"]["reasons"] == [] def test_risk_guard_ignores_trades_outside_active_universe(make_settings, tmp_path) -> None: settings = make_settings(tmp_path, risk_max_consecutive_losses=2, symbols=["BTCUSDT", "ETHUSDT"]) storage = Storage(settings.database_path) now = utc_now() for _ in range(4): storage.insert_trade( Trade( id=None, symbol="HYPEUSDT", side="SELL", qty=1.0, entry_price=100.0, exit_price=99.0, net_pnl=-1.0, opened_at=now, closed_at=now, ) ) guard = risk_guard_snapshot(settings, storage.closed_trades(), storage.latest_equity()) assert guard["block_new_entries"] is False assert guard["reasons"] == [] assert guard["blocked_symbols"] == [] def test_data_quality_flags_missing_candle_gap() -> None: candles = [ Candle(1_000_000, 100, 101, 99, 100.5, 10), Candle(1_000_000 + 60 * 60 * 1000 * 3, 100.5, 102, 100, 101, 12), ] ticker = Ticker("BTCUSDT", 101, 100.99, 101.01, 1_000_000, 100, 0) row = analyze_symbol_quality(symbol="BTCUSDT", candles=candles, ticker=ticker, interval="60") assert row["status"] == "warn" assert any(issue["code"] == "missing_candles" for issue in row["issues"])