diff --git a/tests/test_execution.py b/tests/test_execution.py index c90eacc..508db96 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -405,6 +405,49 @@ def test_simulate_multi_day_with_audit_records_costs_without_replay(): assert result.daily_executions[1].executions == () +def test_simulate_multi_day_with_audit_never_spends_more_cash_than_available(): + """满仓目标应按可用现金部分成交,不能用负现金隐式加杠杆。""" + result = simulate_multi_day_with_audit( + [("d1", {"A": 1.0})], + [("d1", {"A": 10.0})], + 1_000.0, + ) + + execution = result.daily_executions[0].executions[0] + assert result.positions[0].cash >= -1e-9 + assert 0 < execution.partial_fill_pct < 1 + assert execution.blocked_reason == "insufficient_cash_partial_fill" + assert result.final_portfolio_value == pytest.approx(1_000.0 - result.total_costs) + + +@pytest.mark.parametrize( + "targets", + [ + {"A": -0.1}, + {"A": 0.6, "B": 0.5}, + {"A": float("nan")}, + ], +) +def test_simulate_multi_day_with_audit_rejects_invalid_long_only_weights(targets): + """多日 A 股目标必须是有限、非负且合计不超过 100% 的权重。""" + with pytest.raises(ValueError, match="target weights"): + simulate_multi_day_with_audit( + [("d1", targets)], + [("d1", {"A": 10.0, "B": 10.0})], + 1_000.0, + ) + + +def test_simulate_multi_day_with_audit_requires_price_for_existing_holding(): + """已有持仓缺价时无法可信估值,必须失败而不是把市值记为零。""" + with pytest.raises(ValueError, match="missing price for held asset A"): + simulate_multi_day_with_audit( + [("d1", {"A": 1.0}), ("d2", {"A": 1.0})], + [("d1", {"A": 10.0}), ("d2", {})], + 1_000.0, + ) + + def test_simulate_multi_day_with_audit_records_unpriced_target_rejection(): """缺失价格的目标不能吞掉现金,且必须留下拒绝原因。""" result = simulate_multi_day_with_audit(