| """Qlib-integrated backtest engine.""" |
|
|
| from __future__ import annotations |
|
|
| import pandas as pd |
|
|
| from backtest.performance_metrics import summarize_returns |
| from backtest.portfolio_construction import top_k_equal_weight |
| from backtest.transaction_cost import apply_transaction_cost |
| from data_pipeline.init_qlib import init_qlib |
| from qlib.contrib.evaluate import risk_analysis |
| from qlib.contrib.strategy import TopkDropoutStrategy |
| from qlib.backtest import backtest as qlib_backtest |
|
|
|
|
| def run_qlib_topk_backtest( |
| pred_score: pd.Series, |
| topk: int = 30, |
| n_drop: int = 5, |
| start_time: str = "2024-01-01", |
| end_time: str = "2025-12-31", |
| config_path: str | None = None, |
| ) -> dict: |
| """ |
| Run qlib TopkDropout backtest on model prediction scores. |
| |
| pred_score: MultiIndex (instrument, datetime) Series. |
| """ |
| init_qlib(config_path) |
|
|
| strategy = TopkDropoutStrategy( |
| signal=pred_score, |
| topk=topk, |
| n_drop=n_drop, |
| ) |
|
|
| executor_config = { |
| "class": "SimulatorExecutor", |
| "module_path": "qlib.backtest.executor", |
| "kwargs": {"time_per_step": "day", "generate_portfolio_metrics": True}, |
| } |
|
|
| portfolio_metric_dict, positions = qlib_backtest( |
| start_time=start_time, |
| end_time=end_time, |
| strategy=strategy, |
| executor=executor_config, |
| account=100_000_000, |
| benchmark="SH000300", |
| ) |
|
|
| freq_key = next(iter(portfolio_metric_dict.keys())) |
| report, pos = portfolio_metric_dict[freq_key] |
|
|
| analysis = risk_analysis(report["return"]) |
| return {"report": report, "positions": pos, "risk": analysis} |
|
|
|
|
| def simple_long_only_backtest( |
| panel: pd.DataFrame, |
| score_col: str = "score", |
| ret_col: str = "label", |
| top_k: int = 30, |
| cost_rate: float = 0.0015, |
| ) -> dict: |
| """Lightweight backtest without full qlib exchange simulator.""" |
| daily_rets = [] |
| prev_holdings = set() |
|
|
| for dt, group in panel.groupby("date"): |
| top = group.nlargest(top_k, score_col) |
| holdings = set(top["symbol"]) |
| turnover = len(holdings.symmetric_difference(prev_holdings)) / max(2 * top_k, 1) |
| gross = top[ret_col].mean() |
| net = gross - apply_transaction_cost(turnover, cost_rate) |
| daily_rets.append({"date": dt, "return": net}) |
| prev_holdings = holdings |
|
|
| ret_series = pd.Series({r["date"]: r["return"] for r in daily_rets}).sort_index() |
| return summarize_returns(ret_series) |
|
|