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.
213 lines
9.2 KiB
Python
213 lines
9.2 KiB
Python
"""Open-source test suite V2 — fixed assertions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
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))
|
|
|
|
|
|
class TestNexQuantCLI:
|
|
def test_cli_commands_available(self):
|
|
import subprocess
|
|
r = subprocess.run([sys.executable, "nexquant.py", "--help"], capture_output=True, text=True, timeout=10)
|
|
assert r.returncode == 0
|
|
for cmd in ["evaluate", "top", "best", "portfolio", "build-strategies", "generate-strategies", "health"]:
|
|
assert cmd in r.stdout.lower(), f"Missing command: {cmd}"
|
|
|
|
|
|
class TestBacktestEdgeCases:
|
|
def test_all_zero_signal(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
n = 500
|
|
close = pd.Series(1.10 + np.arange(n) * 0.0001, index=pd.date_range("2024-01-01", periods=n, freq="1min"))
|
|
result = backtest_signal(close, pd.Series(0.0, index=close.index))
|
|
assert result["n_trades"] == 0
|
|
|
|
def test_sortino_present(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
n = 2000
|
|
dates = pd.date_range("2024-01-01", periods=n, freq="1min")
|
|
close = pd.Series(1.10 * np.exp(np.cumsum(np.random.default_rng(42).normal(0, 0.0002, n))), index=dates)
|
|
signal = pd.Series(np.where(np.random.default_rng(43).normal(0, 1, n) > 0, 1.0, -1.0), index=dates)
|
|
result = backtest_signal(close, signal)
|
|
assert "sortino" in result
|
|
assert result["sortino"] is not None
|
|
|
|
def test_calmar_present(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
n = 2000
|
|
dates = pd.date_range("2024-01-01", periods=n, freq="1min")
|
|
close = pd.Series(1.10 * np.exp(np.cumsum(np.random.default_rng(42).normal(0, 0.0002, n))), index=dates)
|
|
signal = pd.Series(np.where(np.random.default_rng(44).normal(0, 1, n) > 0, 1.0, -1.0), index=dates)
|
|
result = backtest_signal(close, signal)
|
|
assert "calmar" in result
|
|
|
|
def test_all_required_keys_present(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
n = 2000
|
|
dates = pd.date_range("2024-01-01", periods=n, freq="1min")
|
|
close = pd.Series(1.10 * np.exp(np.cumsum(np.random.default_rng(42).normal(0, 0.0002, n))), index=dates)
|
|
signal = pd.Series(np.where(np.random.default_rng(43).normal(0, 1, n) > 0, 1.0, -1.0), index=dates)
|
|
result = backtest_signal(close, signal)
|
|
required = ["sharpe", "max_drawdown", "win_rate", "total_return", "n_trades",
|
|
"annual_return_pct", "monthly_return_pct", "sortino", "calmar"]
|
|
for key in required:
|
|
assert key in result, f"Missing: {key}"
|
|
|
|
|
|
class TestCoreUtils:
|
|
def test_multiprocessing_wrapper(self):
|
|
from rdagent.core.utils import multiprocessing_wrapper
|
|
def fn(x):
|
|
return x * 2
|
|
results = multiprocessing_wrapper([(fn, (5,))], n=1)
|
|
assert results[0] == 10
|
|
|
|
def test_import_class_valid(self):
|
|
from rdagent.core.utils import import_class
|
|
cls = import_class("rdagent.core.exception.WorkflowError")
|
|
assert cls is not None
|
|
|
|
def test_singleton(self):
|
|
from rdagent.core.utils import SingletonBaseClass
|
|
class A(SingletonBaseClass):
|
|
pass
|
|
assert A() is A()
|
|
|
|
|
|
class TestBacktestFromFwdReturns:
|
|
def test_all_nan(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_from_forward_returns
|
|
idx = pd.MultiIndex.from_arrays([pd.date_range("2024-01-01", periods=500, freq="1min"),
|
|
["EURUSD"] * 500], names=["datetime", "instrument"])
|
|
close = pd.Series(1.10 + np.arange(500) * 0.0001, index=idx)
|
|
fwd = close.groupby(level="instrument").shift(-96) / close - 1
|
|
result = backtest_from_forward_returns(pd.Series([np.nan] * 500, index=idx), fwd, close)
|
|
assert result["status"] == "failed"
|
|
|
|
def test_ic_bounds(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_from_forward_returns
|
|
idx = pd.MultiIndex.from_arrays([pd.date_range("2024-01-01", periods=500, freq="1min"),
|
|
["EURUSD"] * 500], names=["datetime", "instrument"])
|
|
close = pd.Series(1.10 + np.arange(500) * 0.0001, index=idx)
|
|
fwd = close.groupby(level="instrument").shift(-96) / close - 1
|
|
factor = pd.Series(np.random.default_rng(42).normal(0, 1, 500), index=idx)
|
|
result = backtest_from_forward_returns(factor, fwd, close)
|
|
if result["status"] == "success" and "ic" in result:
|
|
assert -1.0 <= result["ic"] <= 1.0
|
|
|
|
|
|
class TestProtectionEdgeCases:
|
|
def test_empty_manager(self):
|
|
from rdagent.components.backtesting.protections import ProtectionManager
|
|
pm = ProtectionManager()
|
|
r = pm.check_all(returns=[0.01], timestamps=[], current_equity=100000, peak_equity=100000)
|
|
assert not r.should_block
|
|
|
|
def test_with_defaults(self):
|
|
from rdagent.components.backtesting.protections import ProtectionManager
|
|
pm = ProtectionManager()
|
|
pm.create_default_protections()
|
|
r = pm.check_all(returns=[0.01], timestamps=[pd.Timestamp.now()], current_equity=100000, peak_equity=101000)
|
|
assert not r.should_block
|
|
|
|
def test_get_stats(self):
|
|
from rdagent.components.backtesting.protections import ProtectionManager
|
|
pm = ProtectionManager()
|
|
pm.create_default_protections()
|
|
stats = pm.get_stats()
|
|
assert isinstance(stats, dict)
|
|
|
|
def test_protection_result_active(self):
|
|
from rdagent.components.backtesting.protections import ProtectionResult
|
|
from datetime import datetime, timedelta
|
|
pr = ProtectionResult(should_block=True, reason="test", until=datetime.now() + timedelta(hours=1))
|
|
assert pr.is_active
|
|
|
|
|
|
class TestEnvImports:
|
|
def test_all_importable(self):
|
|
from rdagent.utils.env import Env, QTDockerEnv, QlibCondaConf, QlibCondaEnv, KGDockerEnv
|
|
assert all([Env, QTDockerEnv, QlibCondaConf, QlibCondaEnv, KGDockerEnv])
|
|
|
|
|
|
class TestLogInfra:
|
|
def test_all_importable(self):
|
|
from rdagent.log.conf import LOG_SETTINGS
|
|
from rdagent.log.logger import RDAgentLog
|
|
from rdagent.log.daily_log import session
|
|
from rdagent.log.timer import RD_Agent_TIMER_wrapper
|
|
assert LOG_SETTINGS is not None
|
|
assert RDAgentLog is not None
|
|
assert callable(session)
|
|
assert RD_Agent_TIMER_wrapper is not None
|
|
|
|
|
|
class TestCoreExperiment:
|
|
def test_task_and_experiment(self):
|
|
from rdagent.core.experiment import Task, Experiment, FBWorkspace
|
|
t = Task(name="t", description="d")
|
|
exp = Experiment(sub_tasks=[t])
|
|
assert len(exp.sub_tasks) == 1
|
|
ws = FBWorkspace()
|
|
assert ws.workspace_path is not None
|
|
|
|
|
|
class TestPromptLoader:
|
|
def test_loads_strategy_generation(self):
|
|
from rdagent.components.prompt_loader import load_prompt
|
|
result = load_prompt("strategy_generation")
|
|
assert isinstance(result, dict)
|
|
|
|
def test_missing_raises(self):
|
|
from rdagent.components.prompt_loader import load_prompt
|
|
with pytest.raises(FileNotFoundError):
|
|
load_prompt("xyz_nonexistent")
|
|
|
|
|
|
class TestApplyFTMOMask:
|
|
def test_output_same_length(self):
|
|
from rdagent.components.backtesting.vbt_backtest import _apply_ftmo_mask
|
|
dates = pd.date_range("2024-01-01", periods=100, freq="1min")
|
|
close = pd.Series(1.10, index=dates)
|
|
signal = pd.Series(np.where(np.arange(100) % 2 == 0, 1.0, -1.0), index=dates)
|
|
masked, metrics = _apply_ftmo_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
|
assert len(masked) == len(signal)
|
|
assert isinstance(metrics, dict)
|
|
|
|
def test_flat_signal(self):
|
|
from rdagent.components.backtesting.vbt_backtest import _apply_ftmo_mask
|
|
dates = pd.date_range("2024-01-01", periods=200, freq="1min")
|
|
close = pd.Series(1.10, index=dates)
|
|
signal = pd.Series(0.0, index=dates)
|
|
masked, metrics = _apply_ftmo_mask(signal, close, leverage=1.0, txn_cost_bps=2.14)
|
|
assert isinstance(metrics, dict)
|
|
|
|
|
|
class TestBacktestSignalMetrics:
|
|
def test_flat_signal_zero_trades(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
dates = pd.date_range("2024-01-01", periods=500, freq="1min")
|
|
close = pd.Series(1.10, index=dates)
|
|
result = backtest_signal(close, pd.Series(0.0, index=dates))
|
|
assert result["n_trades"] == 0
|
|
|
|
def test_nan_signal_handled(self):
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
|
n = 200
|
|
dates = pd.date_range("2024-01-01", periods=n, freq="1min")
|
|
close = pd.Series(1.10 + np.random.default_rng(42).normal(0, 0.001, n).cumsum(), index=dates)
|
|
signal = pd.Series(np.where(np.random.default_rng(42).normal(0, 1, n) > 0, 1.0, np.nan), index=dates)
|
|
result = backtest_signal(close, signal)
|
|
assert result["status"] in ("success", "failed")
|