"""Factory loop: a forward-survivor uncorrelated to the deployed book promotes (within governance) and the deployed book is HRP-allocated; a tail-correlated survivor does NOT promote; kill-switch blocks promotion.""" from __future__ import annotations import numpy as np from fxhnt.application.factory.factory_loop import FactoryLoop, LoopConfig class FakeStore: """Minimal StrategyStore stand-in keyed by strategy_id with status + forward_returns.""" def __init__(self, records): self._r = {r["id"]: r for r in records} def deployed(self): return [r for r in self._r.values() if r["status"] == "DEPLOYED"] def forward(self): return [r for r in self._r.values() if r["status"] == "FORWARD"] def set_status(self, sid, status): self._r[sid]["status"] = status def returns(self, sid): return np.array(self._r[sid]["returns"]) def _cfg(**kw): base = dict(max_tail_corr=0.5, min_marginal=0.0, tail_frac=0.1, min_forward_days=20, max_deployed=20, max_new_per_cycle=2, kill_switch=False) base.update(kw); return LoopConfig(**base) def _rec(id, status, returns): return {"id": id, "status": status, "returns": returns} def test_uncorrelated_survivor_promotes_and_allocates() -> None: rng = np.random.default_rng(0) book = list(rng.normal(0.0005, 0.01, 300)) cand = list(rng.normal(0.0005, 0.01, 300)) # independent store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", cand)]) loop = FactoryLoop(store, _cfg()) res = loop.allocate_and_promote() assert "CAND" in res.promoted assert store._r["CAND"]["status"] == "DEPLOYED" assert abs(sum(res.weights.values()) - 1.0) < 1e-9 and set(res.weights) == {"DEP", "CAND"} def test_tail_correlated_survivor_blocked() -> None: rng = np.random.default_rng(1) book = list(rng.normal(0.0005, 0.01, 300)) store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", list(book))]) # identical loop = FactoryLoop(store, _cfg()) res = loop.allocate_and_promote() assert "CAND" not in res.promoted and store._r["CAND"]["status"] == "FORWARD" def test_kill_switch_blocks_promotion() -> None: rng = np.random.default_rng(2) book = list(rng.normal(0.0005, 0.01, 300)); cand = list(rng.normal(0.0005, 0.01, 300)) store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("CAND", "FORWARD", cand)]) loop = FactoryLoop(store, _cfg(kill_switch=True)) res = loop.allocate_and_promote() assert res.promoted == [] and store._r["CAND"]["status"] == "FORWARD" # Fix 8 — new tests ----------------------------------------------------------- def test_below_min_forward_days_not_promoted() -> None: """A forward survivor with fewer than min_forward_days returns must be silently skipped.""" rng = np.random.default_rng(42) book = list(rng.normal(0.0005, 0.01, 300)) # Only 10 days of history — below the min_forward_days=20 threshold short = list(rng.normal(0.0005, 0.01, 10)) store = FakeStore([_rec("DEP", "DEPLOYED", book), _rec("SHORT", "FORWARD", short)]) loop = FactoryLoop(store, _cfg(min_forward_days=20)) res = loop.allocate_and_promote() assert "SHORT" not in res.promoted assert store._r["SHORT"]["status"] == "FORWARD" assert res.forward_count == 0 # it never reached the gate def test_empty_book_admits_first_then_gates_rest() -> None: """Empty deployed book: first uncorrelated survivor gets a founding admit, but a duplicate of it must be blocked by the running-blend re-gate on the second candidate.""" rng = np.random.default_rng(7) a_returns = list(rng.normal(0.001, 0.01, 100)) b_returns = list(a_returns) # identical to A → corr ≈ 1.0 → must be blocked after A is admitted store = FakeStore([ _rec("A", "FORWARD", a_returns), _rec("B", "FORWARD", b_returns), ]) loop = FactoryLoop(store, _cfg(max_new_per_cycle=2)) res = loop.allocate_and_promote() # Exactly one should be admitted; B is a duplicate of A so it must be blocked assert len(res.promoted) == 1 promoted_id = res.promoted[0] other_id = "B" if promoted_id == "A" else "A" assert store._r[promoted_id]["status"] == "DEPLOYED" assert store._r[other_id]["status"] == "FORWARD" def test_mutually_correlated_same_cycle_admits_blocked() -> None: """Two forward survivors uncorrelated to the book but nearly identical to each other: only ONE should be admitted per cycle even though max_new_per_cycle=2.""" rng = np.random.default_rng(99) book = list(rng.normal(0.0005, 0.01, 300)) # Candidates are independent of the book but almost perfectly correlated with each other base = rng.normal(0.001, 0.01, 300) noise = rng.normal(0, 1e-6, 300) cand_a = list(base) cand_b = list(base + noise) # corr(cand_a, cand_b) ≈ 1.0 store = FakeStore([ _rec("DEP", "DEPLOYED", book), _rec("CA", "FORWARD", cand_a), _rec("CB", "FORWARD", cand_b), ]) loop = FactoryLoop(store, _cfg(max_new_per_cycle=2)) res = loop.allocate_and_promote() # Both pass the gate vs the book, but the second must fail the running-blend re-gate assert len(res.promoted) == 1