mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
4758de0eee
- Rename FTMO_* constants → generic names (RISK_PER_TRADE, MAX_DAILY_LOSS, etc.) - Rename backtest_signal_ftmo → backtest_signal_risk - Rename _apply_ftmo_mask → _apply_risk_mask - Clean all FTMO/riskMgmt mentions from commit messages via filter-branch - AGENTS.md: add non-negotiable rule — NEVER mention proprietary terms in commits/releases - Code variables and function names sanitized project-wide - Force-pushed rewritten history to remote
133 lines
4.9 KiB
Python
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_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)
|