diff --git a/README.md b/README.md index 9fec119..e31922d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ## 模块 - `alpha_factors` — 158 alpha 公式 + 24 基础算子(移植自 qlib alpha158) -- `execution` — 执行仿真(成本/滑点/T+1/涨跌停/部分成交/价差)+ 多日 NAV + PnL 拆解(借鉴 hikyuu 部件化思想) +- `execution` — A 股长仓执行仿真(成本/滑点/现金约束)+ 逐日成交/拒绝/持仓/NAV 审计;T+1、涨跌停、成交量与价差提供独立约束函数 - `indicators` — 50+ 技术指标(MACD / KDJ / 布林 / ATR / ADX / 等) - `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理) - `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark) @@ -59,7 +59,7 @@ ruff check src/ tests/ # lint ```python from quant_engine.alpha_factors import alpha_001, alpha_005, ALPHA158_REGISTRY from quant_engine.execution import ( - ExecutionConfig, simulate_with_daily_data, compute_realized_pnl, + ExecutionConfig, simulate_multi_day_with_audit, simulate_with_daily_data, ) from quant_engine.backtest import run_weight_backtest from quant_engine.portfolio_construction import scores_to_weight_table @@ -76,6 +76,22 @@ 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) +# 已正确滞后的目标权重 → 现金约束执行 → 唯一来源的成交/拒绝/日末持仓/NAV +execution = simulate_multi_day_with_audit( + target_weights_history=[ + ("2024-01-02", {"000001.SZ": 1.0}), + ("2024-01-03", {"000001.SZ": 1.0}), + ], + price_history=[ + ("2024-01-02", {"000001.SZ": 10.0}), + ("2024-01-03", {"000001.SZ": 10.5}), + ], + initial_cash=1_000_000.0, + config=ExecutionConfig(), +) +print(execution.nav_series) +print(execution.daily_executions) + # 多期因子分数 → Top-K 等权组合 → 稳定回测结果 rebalance_weights = scores_to_weight_table( factor_scores, diff --git a/tests/test_execution.py b/tests/test_execution.py index 508db96..e1fdd6b 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -344,15 +344,11 @@ def test_simulate_multi_day_first_day_value_equals_initial(): def test_simulate_multi_day_holdings_evolution(): - """调仓后 holdings 演化。 - - 注意:positions[i] 是第 i 天 rebalance 之前的快照。 - 所以要看 d2 rebalance 后的 holdings,需要看 positions[2](d3 的快照)。 - """ + """日末快照应反映当天调仓后的 holdings。""" signals = [ ("d1", {"A": 0.5, "B": 0.5}), ("d2", {"A": 1.0, "B": 0.0}), # 全仓 A - ("d3", {"A": 1.0, "B": 0.0}), # 第三天的快照才能看到 d2 rebalance 后的 holdings + ("d3", {"A": 1.0, "B": 0.0}), ] prices = [ ("d1", {"A": 10.0, "B": 20.0}), @@ -360,9 +356,8 @@ def test_simulate_multi_day_holdings_evolution(): ("d3", {"A": 12.0, "B": 22.0}), ] positions = simulate_multi_day(signals, prices, 1_000_000.0) - # d3 的 PRE-trade snapshot 应该只有 A(B 在 d2 被平仓) - assert "B" not in positions[2].holdings - assert "A" in positions[2].holdings + assert "B" not in positions[1].holdings + assert "A" in positions[1].holdings def test_simulate_multi_day_with_audit_rebalances_target_weights_by_delta(): @@ -567,6 +562,15 @@ def test_run_end_to_end_poc_costs_recorded(): result = run_end_to_end_poc(signals, prices, 1_000_000.0) assert result["total_costs"] > 0 assert result["total_turnover"] > 0 + executions = [ + execution + for daily in result["daily_executions"] + for execution in daily.executions + ] + assert result["total_costs"] == pytest.approx(sum(item.total_cost for item in executions)) + assert result["total_turnover"] == pytest.approx( + sum(item.executed_value for item in executions) + ) # ── v1.2.0 Phase 2: T+1 / 涨跌停 / 部分成交 / 买卖价差 ─────