diff --git a/tests/test_execution.py b/tests/test_execution.py index d244ad2..ad26c50 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -11,6 +11,7 @@ import pytest from quant_engine.execution import ( ExecutionConfig, ExecutionResult, + ExecutionSimulationResult, apply_bid_ask_spread, apply_volume_constraint, check_price_limit, @@ -20,6 +21,7 @@ from quant_engine.execution import ( run_end_to_end_poc, simulate_execution, simulate_multi_day, + simulate_multi_day_with_audit, simulate_with_daily_data, total_costs, total_turnover, @@ -357,6 +359,73 @@ def test_simulate_multi_day_holdings_evolution(): assert "A" in positions[2].holdings +def test_simulate_multi_day_with_audit_rebalances_target_weights_by_delta(): + """相同目标权重不应在每个交易日重复买入。""" + config = ExecutionConfig( + commission_bps=0, + stamp_tax_bps=0, + slippage_bps=0, + min_trade_amount=0, + ) + targets = [(date, {"A": 1.0}) for date in ("d1", "d2", "d3")] + prices = [(date, {"A": 10.0}) for date in ("d1", "d2", "d3")] + + result = simulate_multi_day_with_audit(targets, prices, 1_000.0, config) + + assert isinstance(result, ExecutionSimulationResult) + assert [len(day.executions) for day in result.daily_executions] == [1, 0, 0] + assert result.total_turnover == pytest.approx(1_000.0) + assert [position.cash for position in result.positions] == pytest.approx([0.0, 0.0, 0.0]) + assert [position.holdings["A"] for position in result.positions] == pytest.approx( + [100.0, 100.0, 100.0] + ) + assert [position.portfolio_value for position in result.positions] == pytest.approx( + [1_000.0, 1_000.0, 1_000.0] + ) + + +def test_simulate_multi_day_with_audit_records_costs_without_replay(): + """成交成本与日末 NAV 应来自同一次状态推进。""" + targets = [("d1", {"A": 1.0}), ("d2", {"A": 1.0})] + prices = [("d1", {"A": 10.0}), ("d2", {"A": 10.0})] + + result = simulate_multi_day_with_audit(targets, prices, 1_000.0) + + first_day = result.daily_executions[0] + assert first_day.nav_before == pytest.approx(1_000.0) + assert first_day.nav_after == pytest.approx(result.positions[0].portfolio_value) + assert result.total_costs == pytest.approx(sum(r.total_cost for r in first_day.executions)) + assert result.final_portfolio_value == pytest.approx(1_000.0 - result.total_costs) + assert result.daily_executions[1].executions == () + + +def test_simulate_multi_day_with_audit_records_unpriced_target_rejection(): + """缺失价格的目标不能吞掉现金,且必须留下拒绝原因。""" + result = simulate_multi_day_with_audit( + [("d1", {"A": 1.0})], + [("d1", {"B": 10.0})], + 1_000.0, + ) + + rejection = result.daily_executions[0].executions[0] + assert rejection.stock_code == "A" + assert rejection.executed_value == 0.0 + assert rejection.partial_fill_pct == 0.0 + assert rejection.blocked_reason == "missing_price" + assert result.positions[0].cash == 1_000.0 + assert result.positions[0].holdings == {} + + +def test_simulate_multi_day_with_audit_requires_matching_dates(): + """权重与价格日期错位必须显式失败,不能按位置静默配对。""" + with pytest.raises(ValueError, match="dates must match"): + simulate_multi_day_with_audit( + [("d1", {"A": 1.0})], + [("d2", {"A": 10.0})], + 1_000.0, + ) + + # ── v1.2.0 Phase 1:端到端 POC(run_end_to_end_poc) ─────