510f77e6ea
- BacktestResult 加 id;save_result 设 result.id=lastrowid(修 get_result bug) - runner._on_done 用 result.id(getattr 兜底 FactorReport) - cta_engine 构建 equity_curve/trades DataFrame;save 传 file_dir - result_store parquet→JSON(去 pyarrow 依赖,本机/容器都稳) - 16 tests passed
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""Tests for sanguo_backtest.result_store module."""
|
|
import pytest
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
from sanguo_backtest.result_store import (
|
|
BacktestResult,
|
|
save_result,
|
|
load_result,
|
|
list_results
|
|
)
|
|
|
|
|
|
def test_save_and_load_result(temp_db_path):
|
|
"""Test saving and loading a backtest result without parquet files."""
|
|
# Create a result without equity_curve and trades (file_dir=None by default)
|
|
result = BacktestResult(
|
|
task_id="test_task_001",
|
|
type="cta",
|
|
status="completed",
|
|
strategy="DualThrustStrategy",
|
|
symbol="IF2401",
|
|
params={"window": 20, "entry_threshold": 0.5},
|
|
start="2024-01-01",
|
|
end="2024-03-31",
|
|
statistics={
|
|
"total_return": 0.15,
|
|
"sharpe_ratio": 1.2,
|
|
"max_drawdown": -0.08,
|
|
"win_rate": 0.55
|
|
}
|
|
)
|
|
|
|
# Save the result
|
|
result_id = save_result(result, temp_db_path)
|
|
assert result_id > 0
|
|
assert isinstance(result_id, int)
|
|
|
|
# Load the result
|
|
loaded_result = load_result(result_id, temp_db_path)
|
|
|
|
# Verify all fields match
|
|
assert loaded_result.task_id == result.task_id
|
|
assert loaded_result.type == result.type
|
|
assert loaded_result.status == result.status
|
|
assert loaded_result.strategy == result.strategy
|
|
assert loaded_result.symbol == result.symbol
|
|
assert loaded_result.params == result.params
|
|
assert loaded_result.start == result.start
|
|
assert loaded_result.end == result.end
|
|
assert loaded_result.statistics == result.statistics
|
|
assert loaded_result.equity_curve is None
|
|
assert loaded_result.trades is None
|
|
assert loaded_result.error_msg is None
|
|
|
|
|
|
def test_list_results_filter(temp_db_path):
|
|
"""Test listing results with type filter."""
|
|
# Create multiple results of different types
|
|
cta_result = BacktestResult(
|
|
task_id="cta_task_001",
|
|
type="cta",
|
|
status="completed",
|
|
strategy="DualThrustStrategy",
|
|
symbol="IF2401",
|
|
params={"window": 20},
|
|
start="2024-01-01",
|
|
end="2024-03-31",
|
|
statistics={"total_return": 0.15}
|
|
)
|
|
|
|
factor_result = BacktestResult(
|
|
task_id="factor_task_001",
|
|
type="factor",
|
|
status="completed",
|
|
strategy="Alpha158Strategy",
|
|
symbol="000001.SZ",
|
|
params={"factors": ["alpha001", "alpha002"]},
|
|
start="2024-01-01",
|
|
end="2024-03-31",
|
|
statistics={"total_return": 0.20}
|
|
)
|
|
|
|
# Save both results
|
|
cta_id = save_result(cta_result, temp_db_path)
|
|
factor_id = save_result(factor_result, temp_db_path)
|
|
|
|
# List all results
|
|
all_results = list_results(db_path=temp_db_path)
|
|
assert len(all_results) == 2
|
|
|
|
# List only CTA results
|
|
cta_results = list_results(type_filter="cta", db_path=temp_db_path)
|
|
assert len(cta_results) == 1
|
|
assert cta_results[0].type == "cta"
|
|
assert cta_results[0].task_id == "cta_task_001"
|
|
|
|
# List only factor results
|
|
factor_results = list_results(type_filter="factor", db_path=temp_db_path)
|
|
assert len(factor_results) == 1
|
|
assert factor_results[0].type == "factor"
|
|
assert factor_results[0].task_id == "factor_task_001"
|
|
|
|
|
|
def test_failed_result_stores_error_msg(temp_db_path):
|
|
"""Test that failed results store error messages."""
|
|
# Create a failed result with error message
|
|
failed_result = BacktestResult(
|
|
task_id="failed_task_001",
|
|
type="cta",
|
|
status="failed",
|
|
strategy="DualThrustStrategy",
|
|
symbol="IF2401",
|
|
params={"window": 20},
|
|
start="2024-01-01",
|
|
end="2024-03-31",
|
|
statistics={}, # Empty statistics for failed result
|
|
error_msg="Data loading failed: insufficient historical data"
|
|
)
|
|
|
|
# Save the failed result
|
|
result_id = save_result(failed_result, temp_db_path)
|
|
|
|
# Load and verify error message is preserved
|
|
loaded_result = load_result(result_id, temp_db_path)
|
|
assert loaded_result.status == "failed"
|
|
assert loaded_result.error_msg == "Data loading failed: insufficient historical data"
|
|
assert loaded_result.statistics == {}
|
|
|
|
|
|
def test_save_sets_result_id_attribute(temp_db_path, tmp_path):
|
|
"""S1.1: save_result must set result.id to the DB row id (orchestrator uses it)."""
|
|
result = BacktestResult(
|
|
task_id="cta_id_test", type="cta", status="done", strategy="S", symbol="600000",
|
|
params={"a": 1}, start="2024-01-01", end="2024-06-30", statistics={"sharpe": 1.2},
|
|
)
|
|
save_result(result, db_path=temp_db_path)
|
|
assert result.id is not None
|
|
assert isinstance(result.id, int)
|
|
|
|
|
|
def test_save_load_roundtrip_with_equity_curve(temp_db_path, tmp_path):
|
|
"""S1.1: equity_curve persists to parquet and reloads via result.id."""
|
|
fdir = str(tmp_path / "files")
|
|
result = BacktestResult(
|
|
task_id="cta_eq_test", type="cta", status="done", strategy="S", symbol="600000",
|
|
params={"a": 1}, start="2024-01-01", end="2024-06-30", statistics={"sharpe": 1.2},
|
|
equity_curve=pd.DataFrame([
|
|
{"date": "2024-01-01", "balance": 1_000_000},
|
|
{"date": "2024-01-02", "balance": 1_010_000},
|
|
]),
|
|
)
|
|
save_result(result, db_path=temp_db_path, file_dir=fdir)
|
|
assert result.id is not None
|
|
|
|
loaded = load_result(result.id, temp_db_path)
|
|
assert loaded.statistics == {"sharpe": 1.2}
|
|
assert loaded.equity_curve is not None
|
|
assert len(loaded.equity_curve) == 2
|
|
assert loaded.equity_curve.iloc[1]["balance"] == 1_010_000
|