59 lines
1.9 KiB
Python
59 lines
1.9 KiB
Python
"""Policies observe actual post-fill holdings and only schedule the next open."""
|
|
|
|
import pytest
|
|
|
|
from quant_engine.execution import ExecutionConfig, simulate_daily_ledger_with_audit
|
|
|
|
|
|
def test_policy_next_open_actual_holdings_and_immutable_past_snapshots():
|
|
seen = []
|
|
|
|
def decide(position):
|
|
seen.append((position.date, dict(position.holdings), position.cash))
|
|
position.holdings.clear()
|
|
return {"A": 0.5} if position.date == "d1" else None
|
|
|
|
result = simulate_daily_ledger_with_audit(
|
|
[],
|
|
[("d1", {"A": 10}), ("d2", {"A": 20}), ("d3", {"A": 30})],
|
|
[("d1", {"A": 10}), ("d2", {"A": 25}), ("d3", {"A": 40})],
|
|
1000,
|
|
ExecutionConfig(commission_bps=0, stamp_tax_bps=0, slippage_bps=0, min_trade_amount=0),
|
|
decision_policy=decide,
|
|
)
|
|
assert seen[0][1] == {}
|
|
assert seen[1][1] == {"A": 25}
|
|
assert result.nav_series.tolist() == [1000, 1125, 1500]
|
|
assert result.positions[1].holdings == {"A": 25}
|
|
assert len(result.trades_frame) == 1
|
|
|
|
|
|
def test_rejected_entry_does_not_create_a_position_for_policy():
|
|
holdings = []
|
|
|
|
def decide(position):
|
|
holdings.append(dict(position.holdings))
|
|
return {"A": 1} if position.date == "d1" else None
|
|
|
|
result = simulate_daily_ledger_with_audit(
|
|
[],
|
|
[("d1", {"A": 10}), ("d2", {"A": 10})],
|
|
[("d1", {"A": 10}), ("d2", {"A": 10})],
|
|
1000,
|
|
ExecutionConfig(min_trade_amount=2000),
|
|
decision_policy=decide,
|
|
)
|
|
assert holdings == [{}, {}]
|
|
assert result.trades_frame.empty
|
|
|
|
|
|
def test_policy_and_fixed_schedule_cannot_be_mixed():
|
|
with pytest.raises(ValueError, match="fixed"):
|
|
simulate_daily_ledger_with_audit(
|
|
[("d1", {"A": 1})],
|
|
[("d1", {"A": 10})],
|
|
[("d1", {"A": 10})],
|
|
1000,
|
|
decision_policy=lambda p: None,
|
|
)
|