Whole-branch Important: dashboard showed the stale hardcoded ceiling while the engine sizes against the resolved (paper $1M / live $35k) ceiling. Task-5 Minor: ruff I001 import-sort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
4.4 KiB
Python
82 lines
4.4 KiB
Python
import datetime as dt
|
|
|
|
from fxhnt import registry
|
|
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
|
|
from fxhnt.application.allocation_ingest import record_allocation
|
|
from fxhnt.application.allocation_policy import (
|
|
ALLOCATION_POLICY_VERSION,
|
|
allocation_policy_hash,
|
|
resolve_allocation_policy,
|
|
)
|
|
from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO
|
|
from fxhnt.config import get_settings
|
|
|
|
|
|
def test_records_allocation_for_deploy_set(tmp_path, monkeypatch):
|
|
dsn = f"sqlite:///{tmp_path}/ai.db"
|
|
repo = ForwardNavRepo(dsn)
|
|
repo.migrate()
|
|
at = dt.datetime(2026, 7, 9, 23, 30)
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d1",
|
|
{"display_name": "D1", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d2",
|
|
{"display_name": "D2", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
dates = [(dt.date(2026, 6, 1) + dt.timedelta(days=i)).isoformat() for i in range(60)] # 60 valid calendar days
|
|
for sid, flip in (("d1", 1), ("d2", 0)):
|
|
for i, dd in enumerate(dates):
|
|
repo.upsert_rows([NavRowDTO(strategy_id=sid, date=dd,
|
|
ret=(0.01 if (i % 2) == flip else -0.008), nav=1.0)], at=at)
|
|
out = record_allocation(dsn, equity=20_000.0)
|
|
assert set(out) <= {"d1", "d2"} # only deploy-set considered
|
|
assert sum(abs(v) for v in out.values()) <= 35_000.0 + 1e-6 # capped by the default ceiling
|
|
assert repo.current_allocation() == out # recorded
|
|
|
|
|
|
def test_dropped_strategy_leaves_no_stale_allocation_row(tmp_path, monkeypatch):
|
|
dsn = f"sqlite:///{tmp_path}/prune.db"
|
|
repo = ForwardNavRepo(dsn)
|
|
repo.migrate()
|
|
at = dt.datetime(2026, 7, 9, 23, 30)
|
|
base = dt.date(2026, 4, 1)
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d1",
|
|
{"display_name": "D1", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d2",
|
|
{"display_name": "D2", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
for sid, flip in (("d1", 1), ("d2", 0)):
|
|
for i in range(70):
|
|
d = (base + dt.timedelta(days=i)).isoformat()
|
|
repo.upsert_rows([NavRowDTO(strategy_id=sid, date=d,
|
|
ret=(0.01 if (i % 2) == flip else -0.008), nav=1.0)], at=at)
|
|
record_allocation(dsn, equity=20_000.0) # night 1: both deploy-tier
|
|
# night 2: d2 is no longer deploy-tier (dropped from the deploy set)
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d2",
|
|
{"display_name": "D2", "sleeve": "x", "gate_spec": {}, "tier": "research"})
|
|
record_allocation(dsn, equity=20_000.0)
|
|
assert "d2" not in repo.current_allocation() # no stale row for the dropped strategy
|
|
|
|
|
|
def test_record_allocation_uses_resolved_policy_v2(tmp_path, monkeypatch):
|
|
dsn = f"sqlite:///{tmp_path}/resolved.db"
|
|
repo = ForwardNavRepo(dsn)
|
|
repo.migrate()
|
|
at = dt.datetime(2026, 7, 9, 23, 30)
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d1",
|
|
{"display_name": "D1", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "d2",
|
|
{"display_name": "D2", "sleeve": "x", "gate_spec": {}, "tier": "deploy"})
|
|
dates = [(dt.date(2026, 6, 1) + dt.timedelta(days=i)).isoformat() for i in range(60)]
|
|
for sid, flip in (("d1", 1), ("d2", 0)):
|
|
for i, dd in enumerate(dates):
|
|
repo.upsert_rows([NavRowDTO(strategy_id=sid, date=dd,
|
|
ret=(0.01 if (i % 2) == flip else -0.008), nav=1.0)], at=at)
|
|
record_allocation(dsn, equity=200_000.0)
|
|
rows = repo.all_allocations()
|
|
assert rows # allocation was recorded
|
|
settings = get_settings()
|
|
expected_policy = resolve_allocation_policy(settings)
|
|
expected_hash = allocation_policy_hash(expected_policy, ALLOCATION_POLICY_VERSION)
|
|
assert all(r.policy_version == ALLOCATION_POLICY_VERSION for r in rows) # == 2
|
|
assert all(r.policy_hash == expected_hash for r in rows)
|
|
# default settings -> allow_live False -> paper ceiling ($1M); gross stays well under it here.
|
|
assert sum(abs(r.target_dollars) for r in rows) <= expected_policy.capital_ceiling + 1e-6
|