"""QlibEngine 数据管线测试:落盘格式、D.features 读回、端到端回测(合成数据、临时 qlib 目录)。""" from __future__ import annotations from datetime import date from pathlib import Path import numpy as np import pandas as pd import pytest from app.domain.entities.research import ResearchSpec, UniverseSpec from app.quant.qlib_adapter.dataset import load_close_panel from app.quant.qlib_adapter.engine import QlibEngine from app.quant.qlib_adapter.provider import build_qlib_dataset from conftest_quant import synthetic_daily def _daily5(n: int = 220) -> pd.DataFrame: drifts = { "600519.SH": 0.004, "600036.SH": 0.002, "601318.SH": 0.001, "000001.SZ": -0.001, "600030.SH": -0.003, } return synthetic_daily(drifts, n=n) def _spec(**kw) -> ResearchSpec: base = dict( type="backtest", universe=UniverseSpec(exclude_st=False, min_listing_days=0), factors=[{"name": "momentum_60", "weight": 1.0}], selection={"top_n": 2}, rebalance="monthly", period=(date(2024, 5, 1), date(2024, 8, 31)), ) base.update(kw) return ResearchSpec.model_validate(base) class TestProviderFormat: def test_bin_has_start_index_header(self, tmp_path: Path) -> None: daily = synthetic_daily({"600519.SH": 0.003, "600036.SH": -0.001}, n=200) uri = build_qlib_dataset(daily, tmp_path) cal = (uri / "calendars" / "day.txt").read_text().strip().splitlines() assert len(cal) == 200 inst_line = (uri / "instruments" / "all.txt").read_text().strip().splitlines()[0] assert inst_line.count("\t") == 2 # 3 列,无 TYPE 列 # 落盘目录用小写 instrument;bin 首 4 字节 = 起始日历下标 raw = (uri / "features" / "600519.sh" / "close.day.bin").read_bytes() assert np.frombuffer(raw[:4], dtype=" None: daily = synthetic_daily({"600000.SH": 0.002, "600001.SH": 0.001}, n=120) days = daily["trade_date"].unique() merged = pd.concat( [ daily[daily["symbol"] == "600000.SH"], daily[(daily["symbol"] == "600001.SH") & (daily["trade_date"] >= days[50])], ] ) uri = build_qlib_dataset(merged, tmp_path) raw = (uri / "features" / "600001.sh" / "close.day.bin").read_bytes() assert np.frombuffer(raw[:4], dtype=" None: daily = synthetic_daily({"600519.SH": 0.003, "600036.SH": -0.001}, n=120) uri = build_qlib_dataset(daily, tmp_path) panel = load_close_panel( uri, ["600519.SH", "600036.SH"], date(2024, 2, 1), date(2024, 5, 31) ) assert len(panel) > 0 assert set(panel.columns) == {"600519.SH", "600036.SH"} src = daily[daily["symbol"] == "600519.SH"].set_index("trade_date")["close"] src.index = pd.to_datetime(src.index) target = panel["600519.SH"] overlap = src.index.intersection(target.index) assert len(overlap) > 10 rel = (target.loc[overlap] / src.loc[overlap].astype(float) - 1).abs().max() assert rel < 1e-3 # qlib 存储为 float32,允许微小舍入 class TestQlibEngine: def test_backtest_via_qlib_pipeline(self, tmp_path: Path) -> None: daily = _daily5() result = QlibEngine(qlib_dir=tmp_path).run_backtest(daily, _spec()) assert result.summary.total_return_pct > 0 # 强趋势下动量择股盈利 assert len(result.equity_curve) > 50 assert any("QlibEngine v1" in u for u in result.unimplemented) assert result.config_snapshot["factors"][0]["name"] == "momentum_60" assert (tmp_path / "features").is_dir() # 数据确实落盘到 qlib 目录 def test_factor_test_shared_path(self, tmp_path: Path) -> None: daily = _daily5() spec = _spec() spec.type = "factor_test" report = QlibEngine(qlib_dir=tmp_path).run_factor_test(daily, spec) assert report.factor_name == "momentum_60" assert report.sample_days > 5 assert report.ic_mean > 0 # 5 只强趋势股票的截面动量 IC 为正