diff --git a/README.md b/README.md index e31922d..93d3103 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理) - `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark) - `portfolio_construction` — 多期因子分数 → Top-K → 等权目标权重表 +- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 执行审计(防前视编排) - `metrics` — 绩效(年化收益 / 波动率 / Sharpe / 最大回撤 / Calmar) - `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test) - `portfolio_decomp` — 组合分解(risk_parity / mean_variance / 因子归因) @@ -61,8 +62,8 @@ from quant_engine.alpha_factors import alpha_001, alpha_005, ALPHA158_REGISTRY from quant_engine.execution import ( ExecutionConfig, simulate_multi_day_with_audit, simulate_with_daily_data, ) +from quant_engine.research_pipeline import run_factor_execution_research from quant_engine.backtest import run_weight_backtest -from quant_engine.portfolio_construction import scores_to_weight_table from quant_engine.indicators import macd, bollinger, kdj from quant_engine.data_adapter import ( long_to_wide, wide_to_long, rename_tushare_columns, @@ -73,8 +74,9 @@ from quant_engine.data_adapter import ( # 端到端:qtdb_pro 长表 → 适配 → alpha158 → execution df = load_qtdb_daily(["000001.SZ"], "2024-01-01", with_adj=True) -prices, volumes = prepare_execution_inputs(df) -result = simulate_with_daily_data(prices, initial_cash=1_000_000.0) +close_prices, volumes = prepare_execution_inputs(df) +open_prices, _ = prepare_execution_inputs(df, price_col="open") +result = simulate_with_daily_data(close_prices, initial_cash=1_000_000.0) # 已正确滞后的目标权重 → 现金约束执行 → 唯一来源的成交/拒绝/日末持仓/NAV execution = simulate_multi_day_with_audit( @@ -92,18 +94,25 @@ execution = simulate_multi_day_with_audit( print(execution.nav_series) print(execution.daily_executions) -# 多期因子分数 → Top-K 等权组合 → 稳定回测结果 -rebalance_weights = scores_to_weight_table( +# 多期因子分数(必须是 point-in-time 数据)→ Top-K → 下一交易日 open 执行 +factor_execution = run_factor_execution_research( factor_scores, top_k=20, - gross_exposure=1.0, + execution_prices=open_prices, + execution_price_field="open", + initial_cash=1_000_000.0, ) + +# run_weight_backtest 是低层算子:只接受收益区间开始前已经生效的持仓权重。 +# 不要把 signal-date 的 factor_scores/decision_weights 直接传给它。 backtest = run_weight_backtest( - weights=rebalance_weights, + weights=effective_holding_weights, stock_returns=daily_returns, initial_capital=1_000_000.0, benchmark_nav=benchmark_nav, ) +print(factor_execution.schedule.signal_to_execution) +print(factor_execution.execution.daily_executions) print(backtest.stats()) print(backtest.benchmark_report()) ``` diff --git a/src/quant_engine/backtest.py b/src/quant_engine/backtest.py index a705c4a..46058c2 100644 --- a/src/quant_engine/backtest.py +++ b/src/quant_engine/backtest.py @@ -76,8 +76,9 @@ def compute_nav_from_weights( ) -> pd.Series: """从调仓表(日期 × 股票权重)+ 个股日收益 → 净值曲线。 - 假设:在调仓日之间权重不变(**前向填充**)。 - 调仓日的权重 = `weights.loc[rebalance_date]`。 + 假设:输入是该收益测量区间开始前已经生效的持仓权重,并在调仓日之间 + 保持不变(**前向填充**)。本函数不会把信号日自动解释为执行日;因子分数 + 应先经交易日历调度和实际执行时点处理,避免把同一时点未知的收益计入。 Args: weights: 调仓日 × 股票代码 的权重 DataFrame(**0~1**,行和 ≤ 1) diff --git a/tests/test_portfolio_construction.py b/tests/test_portfolio_construction.py index cd43af1..b12a345 100644 --- a/tests/test_portfolio_construction.py +++ b/tests/test_portfolio_construction.py @@ -91,10 +91,10 @@ def test_scores_to_weight_table_constructs_each_rebalance_independently() -> Non pd.testing.assert_series_equal(result.iloc[0], changed_result.iloc[0]) -def test_factor_scores_flow_directly_into_weight_backtest() -> None: +def test_effective_holding_weights_flow_into_weight_backtest() -> None: dates = pd.date_range("2026-01-05", periods=3, freq="B") - scores = pd.DataFrame( - {"A": [2.0, 0.0], "B": [1.0, 3.0]}, + effective_weights = pd.DataFrame( + {"A": [1.0, 0.0], "B": [0.0, 1.0]}, index=dates[[0, 2]], ) stock_returns = pd.DataFrame( @@ -102,11 +102,10 @@ def test_factor_scores_flow_directly_into_weight_backtest() -> None: index=dates, ) - weights = scores_to_weight_table(scores, top_k=1) - result = run_weight_backtest(weights, stock_returns) + result = run_weight_backtest(effective_weights, stock_returns) pd.testing.assert_series_equal(result.nav, pd.Series([1.1, 1.1, 1.32], index=dates)) - pd.testing.assert_frame_equal(result.weights, weights) + pd.testing.assert_frame_equal(result.weights, effective_weights) @pytest.mark.parametrize("top_k", [0, -1])