Files
NexQuant/test/local/test_continuous_strategies.py
T

133 lines
4.9 KiB
Python

"""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_ftmo")
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_ftmo")
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)