mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
cbe1c52e00
Rename all source files, scripts, tests, documentation, and configuration from Predix/predix to NexQuant/nexquant across the entire codebase.
166 lines
6.3 KiB
Python
166 lines
6.3 KiB
Python
"""Deep tests for nexquant_autopilot.py — property-based, mocks, edge cases.
|
|
|
|
Tests the core logic of the 24/7 strategy generator by mocking
|
|
the StrategyOrchestrator at the correct import path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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
|
|
from hypothesis import strategies as st
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_orch():
|
|
"""Mock StrategyOrchestrator that returns configurable results."""
|
|
with patch(
|
|
"rdagent.scenarios.qlib.local.strategy_orchestrator.StrategyOrchestrator",
|
|
autospec=True,
|
|
) as mock:
|
|
instance = MagicMock()
|
|
mock.return_value = instance
|
|
yield mock, instance
|
|
|
|
|
|
class TestMainRound:
|
|
def test_returns_zero_on_init_failure(self):
|
|
with patch(
|
|
"rdagent.scenarios.qlib.local.strategy_orchestrator.StrategyOrchestrator",
|
|
side_effect=RuntimeError("no data"),
|
|
):
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 0
|
|
|
|
def test_returns_zero_on_generate_failure(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.side_effect = RuntimeError("crash")
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 0
|
|
|
|
def test_counts_accepted(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted", "strategy_name": "s1", "sharpe_ratio": 0.5, "oos_sharpe": 0.3},
|
|
{"status": "rejected", "strategy_name": "s2", "reason": "low"},
|
|
]
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 1
|
|
|
|
def test_ensemble_called_when_2_accepted(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted", "strategy_name": "a", "sharpe_ratio": 0.5, "oos_sharpe": 0.3},
|
|
{"status": "accepted", "strategy_name": "b", "sharpe_ratio": 0.6, "oos_sharpe": 0.4},
|
|
]
|
|
instance.build_ensemble.return_value = {
|
|
"status": "success", "sharpe_ratio": 0.95, "oos_sharpe": 0.65,
|
|
"members": ["a", "b"],
|
|
}
|
|
from scripts.nexquant_autopilot import main_round
|
|
main_round("daytrading", 1)
|
|
instance.build_ensemble.assert_called_once()
|
|
|
|
def test_ensemble_not_called_when_lt_2_accepted(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted", "strategy_name": "a", "sharpe_ratio": 0.5, "oos_sharpe": 0.3},
|
|
{"status": "rejected", "strategy_name": "b", "reason": "no"},
|
|
]
|
|
from scripts.nexquant_autopilot import main_round
|
|
main_round("daytrading", 1)
|
|
instance.build_ensemble.assert_not_called()
|
|
|
|
def test_ensemble_failure_doesnt_crash(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted", "strategy_name": "a", "sharpe_ratio": 0.5, "oos_sharpe": 0.3},
|
|
{"status": "accepted", "strategy_name": "b", "sharpe_ratio": 0.6, "oos_sharpe": 0.4},
|
|
]
|
|
instance.build_ensemble.side_effect = RuntimeError("boom")
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 2 # Still counts accepted
|
|
|
|
def test_empty_results_returns_zero(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = []
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 0
|
|
|
|
def test_ensemble_none_returns_zero_extra(self, mock_orch):
|
|
mock_cls, instance = mock_orch
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted", "strategy_name": "a", "sharpe_ratio": 0.5, "oos_sharpe": 0.3},
|
|
{"status": "accepted", "strategy_name": "b", "sharpe_ratio": 0.6, "oos_sharpe": 0.4},
|
|
]
|
|
instance.build_ensemble.return_value = None
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert result == 2
|
|
|
|
@given(
|
|
sharpe=st.floats(min_value=-5, max_value=5, allow_nan=False, allow_infinity=False),
|
|
name=st.text(min_size=1, max_size=20),
|
|
)
|
|
@settings(max_examples=100, deadline=5000)
|
|
def test_main_round_never_crashes_property(self, sharpe, name):
|
|
with patch(
|
|
"rdagent.scenarios.qlib.local.strategy_orchestrator.StrategyOrchestrator",
|
|
autospec=True,
|
|
) as mock_cls:
|
|
instance = MagicMock()
|
|
instance.generate_strategies.return_value = [
|
|
{"status": "accepted" if sharpe > 0.1 else "rejected",
|
|
"strategy_name": name, "sharpe_ratio": sharpe, "oos_sharpe": 0.0,
|
|
"reason": "test"},
|
|
]
|
|
mock_cls.return_value = instance
|
|
from scripts.nexquant_autopilot import main_round
|
|
result = main_round("daytrading", 1)
|
|
assert isinstance(result, int) and result >= 0
|
|
|
|
|
|
class TestConfig:
|
|
def test_batch_size_positive(self):
|
|
from scripts import nexquant_autopilot
|
|
assert nexquant_autopilot.BATCH_SIZE > 0
|
|
|
|
def test_optuna_trials_positive(self):
|
|
from scripts import nexquant_autopilot
|
|
assert nexquant_autopilot.OPTUNA_TRIALS > 0
|
|
|
|
def test_cooldown_positive(self):
|
|
from scripts import nexquant_autopilot
|
|
assert nexquant_autopilot.COOLDOWN > 0
|
|
|
|
def test_max_consecutive_fails_positive(self):
|
|
from scripts import nexquant_autopilot
|
|
assert nexquant_autopilot.MAX_CONSECUTIVE_FAILS > 0
|
|
|
|
|
|
class TestStyleCycling:
|
|
def test_odd_rounds_are_daytrading(self):
|
|
styles = ["swing", "daytrading"]
|
|
for r in range(1, 20, 2):
|
|
assert styles[r % 2] == "daytrading"
|
|
|
|
def test_even_rounds_are_swing(self):
|
|
styles = ["swing", "daytrading"]
|
|
for r in range(2, 21, 2):
|
|
assert styles[r % 2] == "swing"
|