"""Deep tests for nexquant_continuous_strategies.py — ML model building, style cycling. Tests the build_ml_model function and the round/style alternation logic without requiring real StrategyOrchestrator connections. """ from __future__ import annotations from hypothesis import given, settings, HealthCheck from hypothesis import strategies as st import sys from pathlib import Path from unittest.mock import MagicMock, patch import numpy as np import pandas as pd import pytest PROJECT_ROOT = Path(__file__).parent.parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from hypothesis import given, settings, HealthCheck from hypothesis import strategies as st @pytest.fixture def factor_data(): """Create realistic factor data for ML training.""" rng = np.random.default_rng(42) n = 10000 idx = pd.date_range("2020-01-01", periods=n, freq="1min") return pd.DataFrame({ "factor_a": rng.normal(0, 1, n), "factor_b": rng.normal(0.1, 0.5, n), "factor_c": rng.normal(-0.05, 0.3, n), }, index=idx) @pytest.fixture def close_data(): rng = np.random.default_rng(42) n = 10000 idx = pd.date_range("2020-01-01", periods=n, freq="1min") return pd.Series(1.10 + rng.normal(0, 0.0001, n).cumsum(), index=idx) class TestBuildMLModel: def test_insufficient_data_returns_none(self, factor_data, close_data): """<5000 rows should return None.""" from scripts.nexquant_continuous_strategies import build_ml_model result = build_ml_model(factor_data.iloc[:100], close_data.iloc[:100], "swing") assert result is None @patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_risk") def test_sufficient_data_returns_dict(self, mock_bt, factor_data, close_data): mock_bt.return_value = { "sharpe": 1.5, "max_drawdown": -0.1, "win_rate": 0.55, "n_trades": 200, "wf_oos_sharpe_mean": 0.8, } from scripts.nexquant_continuous_strategies import build_ml_model result = build_ml_model(factor_data, close_data, "daytrading") assert result is not None assert "strategy_name" in result assert "ML_GradientBoost" in result["strategy_name"] assert result["status"] == "accepted" assert result["type"] == "ml_model" @patch("rdagent.components.backtesting.vbt_backtest.backtest_signal_risk") def test_negative_oos_rejected(self, mock_bt, factor_data, close_data): mock_bt.return_value = { "sharpe": 1.5, "max_drawdown": -0.1, "win_rate": 0.55, "n_trades": 200, "wf_oos_sharpe_mean": -0.3, } from scripts.nexquant_continuous_strategies import build_ml_model result = build_ml_model(factor_data, close_data, "swing") assert result is None @given( seed=st.integers(0, 1000), n_rows=st.integers(100, 6000), ) @settings(max_examples=50, deadline=10000, suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_never_crashes(self, factor_data, close_data, seed, n_rows): """build_ml_model must never crash regardless of data size.""" rng = np.random.default_rng(seed) n = min(n_rows, len(factor_data)) f = pd.DataFrame({ "a": rng.normal(0, 1, n), "b": rng.normal(0, 1, n), "c": rng.normal(0, 1, n), }, index=factor_data.index[:n]) c = pd.Series(1.10 + rng.normal(0, 0.001, n).cumsum(), index=f.index) try: from scripts.nexquant_continuous_strategies import build_ml_model result = build_ml_model(f, c, "swing") assert result is None or isinstance(result, dict) except Exception as e: if n < 5000: pass # Expected to return None early else: pytest.fail(f"build_ml_model crashed: {e}") class TestConfig: def test_batch_size_is_positive(self): from scripts import nexquant_continuous_strategies assert nexquant_continuous_strategies.BATCH_SIZE > 0 def test_cooldown_is_positive(self): from scripts import nexquant_continuous_strategies assert nexquant_continuous_strategies.COOLDOWN_SECONDS > 0 class TestStyleCycling: def test_both_style_alternates(self): """When style='both', odd rounds start daytrading, even rounds start swing.""" for r in range(1, 20): if r % 2 == 1: expected = ["swing", "daytrading"] else: expected = ["daytrading", "swing"] styles = expected if r % 2 == 1: assert styles == ["swing", "daytrading"] else: assert styles == ["daytrading", "swing"] def test_single_style_constant(self): """When style is 'daytrading', all rounds use daytrading.""" styles_seen = ["daytrading" for _ in range(10)] assert all(s == "daytrading" for s in styles_seen)