From 256481910f749928c055d55a6d64498b56d9cf21 Mon Sep 17 00:00:00 2001 From: momentum dev Date: Mon, 13 Jul 2026 22:32:03 -0400 Subject: [PATCH] population: characterization tests pinning live _population_reject (pre-extraction) 11 table-driven cases pin the CURRENT behaviour of the live population gate before plan 004 extracts it to strategy/population.py: in-band pass, below-gap, dead band, squeeze pass/fail, both price-band sides, volume floor, missing-data fail-open, the gap-is-a-FRACTION unit pin, and the gap-gate-disabled nesting. These must stay green byte-for-byte across the extraction. Co-Authored-By: Claude Fable 5 --- tests/unit/test_population_gate.py | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/unit/test_population_gate.py diff --git a/tests/unit/test_population_gate.py b/tests/unit/test_population_gate.py new file mode 100644 index 0000000..006a84a --- /dev/null +++ b/tests/unit/test_population_gate.py @@ -0,0 +1,89 @@ +"""Characterization tests pinning the CURRENT behaviour of the live population +gate (``TradingExecutionService._population_reject``) BEFORE it is extracted to +``strategy/population.py`` (plan 004). + +CONTRACT: ``gap_pct`` is a FRACTION (0.12 == 12%). These tests must stay green +byte-for-byte across the extraction — a change in any outcome means the move +altered live behaviour, which the plan forbids (it MOVES code, it does not +retune). +""" + +from trading_execution import ExecutionSettings, TradingExecutionService + +# production-shaped settings (deploy/docker/truenas-compose.yml env block) +PROD = dict( + min_entry_gap_pct=0.10, + min_day_volume=300_000, + price_band_min=2.0, + price_band_max=20.0, +) + + +def _gate(**overrides): + """Return ``_population_reject`` bound to production-shaped settings. + + Built the way the existing execution tests do it (``__new__`` + set + ``.settings``) so nothing else in the service needs constructing. + """ + svc = TradingExecutionService.__new__(TradingExecutionService) + svc.settings = ExecutionSettings(**{**PROD, **overrides}) + + def call(gap_pct=0.12, cum_volume=1_000_000, price=10.0, rvol=None): + return svc._population_reject(gap_pct, cum_volume, price=price, rvol=rvol) + + return call + + +def test_in_band_passes(): + # $2-20, 12% open gap, healthy volume => tradeable + assert _gate()(gap_pct=0.12, price=10.0, cum_volume=1_000_000) is None + + +def test_below_gap_rejects(): + assert _gate()(gap_pct=0.05) is not None + + +def test_dead_band_rejects(): + assert _gate()(gap_pct=0.40) is not None + + +def test_squeeze_exception_passes(): + # >=150% gap re-admitted ONLY with a verified squeeze (rvol >= 5) + assert _gate()(gap_pct=1.60, rvol=6.0) is None + + +def test_squeeze_rvol_fail_rejects(): + assert _gate()(gap_pct=1.60, rvol=3.0) is not None + + +def test_price_band_low_rejects(): + assert _gate()(price=1.50) is not None + + +def test_price_band_high_rejects(): + assert _gate()(price=21.5) is not None + + +def test_volume_floor_rejects(): + assert _gate()(cum_volume=100_000) is not None + + +def test_missing_data_fail_open(): + # None gap / None volume / None price => never blocks + assert _gate()(gap_pct=None, cum_volume=None, price=None) is None + + +def test_gap_is_a_fraction_not_percent(): + # 0.40 (== 40%) lands in the 35-150% dead band. A caller that wrongly passed + # 40.0 (percent) would trip the >=150% squeeze branch instead — a DIFFERENT + # reason. The distinct reason strings pin the fraction contract. + assert "dead band" in _gate()(gap_pct=0.40) + assert "squeeze" in _gate()(gap_pct=40.0) + + +def test_gap_gate_disabled_when_min_zero(): + # The whole gap logic is nested under ``min_entry_gap_pct > 0``. + g = _gate(min_entry_gap_pct=0.0) + assert g(gap_pct=0.05) is None # below-floor not checked + assert g(gap_pct=0.40) is None # dead band not checked + assert g(gap_pct=1.60, rvol=1.0) is None # squeeze not checked