Files
tick-stock-panel/backend/tests/backtest/test_optimizer_run.py
T
lytem28 b6cf0495e1 feat: complete matrix-native backtest engine
Unify strategy execution across backtest, screener, and monitoring; isolate backtest workloads in spawn workers; and add shared matrix caching plus valid-bar indicator acceleration.
2026-07-16 12:17:27 +08:00

362 lines
12 KiB
Python

"""优化器编排测试 — 用假 service 注入受控 stats, 验证排序/取消/进度/目标方向。"""
from __future__ import annotations
import threading
from dataclasses import dataclass
from datetime import date
import pytest
from app.backtest.optimizer import OptimizeConfig, StrategyOptimizer
# ---- 假 StrategyDef / 引擎 / service ----
@dataclass
class _FakeDef:
meta: dict
execution_backend: str = "polars_expr"
class _FakeEngine:
def __init__(self, params_meta, execution_backend="polars_expr"):
self._def = _FakeDef(
meta={"params": params_meta},
execution_backend=execution_backend,
)
def get(self, strategy_id):
return self._def
@dataclass
class _FakeResult:
stats: dict
error: str | None = None
class _FakeCache:
def __init__(self):
self.closed = False
def snapshot(self):
return {"current_bytes": 64, "hits": 2}
def close(self):
self.closed = True
class _FakeService:
"""run() 依据 params 返回受控 stats: sortino = ma_proximity 的映射, 便于校验排序。"""
def __init__(self, score_fn):
self.score_fn = score_fn
self.calls = []
self.prepared_calls = []
self.prepared_values = []
self.result_policies = []
self.matrix_cache_max_bytes = None
self.cache = _FakeCache()
self._lock = threading.Lock()
def prepare_matrix_optimization(self, configs, *, matrix_cache_max_bytes):
self.prepared_calls.append(configs)
self.matrix_cache_max_bytes = matrix_cache_max_bytes
return type("Prepared", (), {
"market_data": type("Market", (), {"nbytes": 1234})(),
"compute_cache": self.cache,
})()
def run(
self,
config,
progress_cb=None,
cancel_event=None,
prepared=None,
result_policy=None,
):
with self._lock:
self.calls.append(dict(config.params or {}))
self.prepared_values.append(prepared)
self.result_policies.append(result_policy)
return self.score_fn(config.params or {})
PARAMS_META = [
{"id": "ma_proximity", "type": "float", "default": 0.02, "min": 0.01, "max": 0.05, "step": 0.005},
]
def _optimizer(score_fn, execution_backend="polars_expr"):
return StrategyOptimizer(
_FakeService(score_fn),
_FakeEngine(PARAMS_META, execution_backend=execution_backend),
)
def _cfg(**kw):
base = dict(
strategy_id="s", symbols=None, start=date(2024, 1, 1), end=date(2024, 6, 1),
param_grid={"ma_proximity": [0.01, 0.02, 0.03]}, objective="sortino", max_workers=4,
)
base.update(kw)
return OptimizeConfig(**base)
def test_ranks_best_by_objective_max():
# sortino 随 ma_proximity 递增 -> 最大值应为 0.03
def score(p):
return _FakeResult(stats={"sortino": p["ma_proximity"] * 100})
out = _optimizer(score).optimize(_cfg())
assert out["best_params"] == {"ma_proximity": 0.03}
assert out["best_score"] == 3.0
assert out["n_combinations"] == 3
assert out["n_completed"] == 3
assert [r["rank"] for r in out["results"]] == [1, 2, 3]
assert out["results"][0]["params"] == {"ma_proximity": 0.03}
def test_all_combos_executed_once():
def score(p):
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score)
out = opt.optimize(_cfg(param_grid={"ma_proximity": [0.01, 0.02, 0.03, 0.04, 0.05]}))
assert out["n_combinations"] == 5
# 每组恰跑一次
ran = sorted(
call["ma_proximity"]
for call, policy in zip(opt.service.calls, opt.service.result_policies, strict=True)
if policy is not None
)
assert ran == [0.01, 0.02, 0.03, 0.04, 0.05]
assert opt.service.result_policies.count(None) == 1
def test_min_direction_objective_restores_display_sign():
# avg_holding_days 是 min 方向: 最小者最优, 且 best_score 必须是原始正值 (非内部取负值)
def score(p):
return _FakeResult(stats={"avg_holding_days": p["ma_proximity"] * 100})
out = _optimizer(score).optimize(_cfg(objective="avg_holding_days"))
assert out["best_params"] == {"ma_proximity": 0.01}
# min 方向: 最优 avg_holding_days = 0.01*100 = 1.0, 用户应看到 +1.0 而非 -1.0
assert out["best_score"] == 1.0
# results 不应外露内部排序键 _sort
assert all("_sort" not in r for r in out["results"])
assert out["results"][0]["objective_raw"] == 1.0
def test_max_drawdown_objective_prefers_smaller_drawdown():
# max_drawdown 为负值, max 方向: -0.1 (回撤更小) 应优于 -0.3
def score(p):
dd = {0.01: -0.1, 0.02: -0.3, 0.03: -0.2}[p["ma_proximity"]]
return _FakeResult(stats={"max_drawdown": dd})
out = _optimizer(score).optimize(_cfg(objective="max_drawdown"))
assert out["best_params"] == {"ma_proximity": 0.01}
assert out["best_score"] == -0.1 # 展示原始负值
def test_service_exception_isolated_not_crashing_batch():
# 某组 service.run 抛异常 -> 应记为该组失败, 其余组正常完成, 不拖垮整批
def score(p):
if p["ma_proximity"] == 0.02:
raise KeyError("boom")
return _FakeResult(stats={"sortino": p["ma_proximity"] * 100})
out = _optimizer(score).optimize(_cfg())
assert out["n_completed"] == 3 # 三组都有结果记录 (含失败组)
assert out["best_params"] == {"ma_proximity": 0.03} # 最优组不受影响
failed = [r for r in out["results"] if r.get("error")]
assert len(failed) == 1
assert "boom" in failed[0]["error"]
def test_backtest_kwargs_illegal_key_rejected():
def score(p):
return _FakeResult(stats={"sortino": 1.0})
with pytest.raises(ValueError, match=r"非法字段|不能包含"):
_optimizer(score).optimize(_cfg(backtest_kwargs={"bad_field": 1}))
def test_backtest_kwargs_reserved_key_rejected():
def score(p):
return _FakeResult(stats={"sortino": 1.0})
with pytest.raises(ValueError, match="不能包含"):
_optimizer(score).optimize(_cfg(backtest_kwargs={"symbols": ["x"]}))
def test_base_params_merged_and_overridden_by_sweep():
# base_params 提供固定参数, combo 覆盖同名; 记录 service 实际收到的 params
def score(p):
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score)
opt.optimize(_cfg(base_params={"ma_proximity": 0.99, "other": 7}))
# 每次 run 收到的 params: ma_proximity 被 combo 覆盖, other 保留
for call, policy in zip(opt.service.calls, opt.service.result_policies, strict=True):
if policy is None:
continue
assert call["other"] == 7
assert call["ma_proximity"] in (0.01, 0.02, 0.03)
def test_none_and_error_results_sink_to_bottom():
# ma_proximity=0.02 的组返回 error, 0.03 的 sortino=None -> 都应排在有效结果之后
def score(p):
if p["ma_proximity"] == 0.02:
return _FakeResult(stats={}, error="boom")
if p["ma_proximity"] == 0.03:
return _FakeResult(stats={"sortino": None})
return _FakeResult(stats={"sortino": 5.0})
out = _optimizer(score).optimize(_cfg())
assert out["best_params"] == {"ma_proximity": 0.01}
assert out["best_score"] == 5.0
# 失败/None 组仍在结果里但 rank 靠后
assert out["n_completed"] == 3
assert out["results"][0]["params"] == {"ma_proximity": 0.01}
def test_cancel_event_stops_remaining():
ev = threading.Event()
ev.set() # 一开始就取消
def score(p):
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score)
out = opt.optimize(_cfg(), cancel_event=ev)
# 取消后所有组跳过 -> 无有效结果
assert opt.service.calls == []
assert out["best_params"] is None
def test_progress_callback_reports_done_total():
seen = []
def score(p):
return _FakeResult(stats={"sortino": 1.0})
def cb(msg):
seen.append(msg)
_optimizer(score).optimize(_cfg(), progress_cb=cb)
assert len(seen) == 4
assert seen[-1]["type"] == "optimizer_finalize"
assert seen[-1]["done"] == 3
assert all(m["total"] == 3 for m in seen)
def test_matrix_optimizer_prepares_once_and_reuses_same_market_data():
def score(p):
return _FakeResult(stats={"sortino": p["ma_proximity"]})
opt = _optimizer(score, execution_backend="matrix_native")
out = opt.optimize(_cfg(max_workers=8))
assert len(opt.service.prepared_calls) == 1
assert len(opt.service.prepared_calls[0]) == 3
assert len({id(value) for value in opt.service.prepared_values}) == 1
assert opt.service.prepared_values[0] is not None
assert out["requested_max_workers"] == 8
assert out["effective_workers"] == 1
assert out["shared_market_data"] is True
assert out["shared_market_data_bytes"] == 1234
assert out["best_backtest"] is not None
assert out["matrix_compute_cache"]["released"] is True
assert opt.service.cache.closed is True
assert opt.service.matrix_cache_max_bytes == 512 * 1024 * 1024
def test_optimizer_trial_policy_skips_mc_but_best_backtest_is_full():
def score(p):
return _FakeResult(stats={"sortino": p["ma_proximity"]})
opt = _optimizer(score)
opt.optimize(_cfg())
trial_policies = [policy for policy in opt.service.result_policies if policy is not None]
assert trial_policies
assert all(policy.include_monte_carlo is False for policy in trial_policies)
assert opt.service.result_policies[-1] is None
def test_mc_objective_trial_policy_keeps_monte_carlo():
def score(p):
return _FakeResult(stats={"mc_maxdd_p95": -p["ma_proximity"]})
opt = _optimizer(score)
opt.optimize(_cfg(objective="mc_maxdd_p95"))
trial_policies = [policy for policy in opt.service.result_policies if policy is not None]
assert all(policy.include_monte_carlo is True for policy in trial_policies)
def test_missing_objective_is_an_explicit_trial_error():
def score(p):
return _FakeResult(stats={"sharpe": 1.0})
out = _optimizer(score).optimize(_cfg())
assert out["best_params"] is None
assert all("缺少优化目标字段" in row["error"] for row in out["results"])
def test_matrix_cache_closes_when_progress_callback_raises_after_prepare():
def score(p):
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score, execution_backend="matrix_native")
def fail_on_prepare(message):
if message["type"] == "optimizer_prepare":
raise RuntimeError("progress failed")
with pytest.raises(RuntimeError, match="progress failed"):
opt.optimize(_cfg(), progress_cb=fail_on_prepare)
assert opt.service.cache.closed is True
def test_matrix_cache_closes_when_cancelled_after_first_trial():
event = threading.Event()
def score(p):
event.set()
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score, execution_backend="matrix_native")
out = opt.optimize(_cfg(), cancel_event=event)
assert out["n_completed"] == 1
assert out["best_backtest"] is None
assert opt.service.cache.closed is True
def test_matrix_cache_closes_when_best_backtest_raises():
call_count = 0
def score(p):
nonlocal call_count
call_count += 1
if call_count == 4:
raise RuntimeError("final failed")
return _FakeResult(stats={"sortino": p["ma_proximity"]})
opt = _optimizer(score, execution_backend="matrix_native")
with pytest.raises(RuntimeError, match="final failed"):
opt.optimize(_cfg())
assert opt.service.cache.closed is True
def test_cancelled_matrix_optimizer_skips_expensive_preparation():
event = threading.Event()
event.set()
def score(p):
return _FakeResult(stats={"sortino": 1.0})
opt = _optimizer(score, execution_backend="matrix_native")
out = opt.optimize(_cfg(), cancel_event=event)
assert opt.service.prepared_calls == []
assert opt.service.calls == []
assert out["n_completed"] == 0
def test_invalid_objective_rejected():
def score(p):
return _FakeResult(stats={"sortino": 1.0})
with pytest.raises(ValueError, match="不支持的优化目标"):
_optimizer(score).optimize(_cfg(objective="not_a_metric"))