From 8922eaaa4f1b31d79656d147986784ea2e8b7cf0 Mon Sep 17 00:00:00 2001 From: Brian McMahon Date: Sun, 24 May 2026 16:48:10 -0700 Subject: [PATCH] =?UTF-8?q?feat(executor):=20retire=20SPY=20from=20#185=20?= =?UTF-8?q?ATR=20exclusion=20=E2=80=94=20close=20L1346=20(c)=20first=20hal?= =?UTF-8?q?f?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L1346 (c) — the executor-side retirement of SPY-special-cases that alpha-engine #185 added as a defensive measure while SPY was only in macro library (Close-only). Post-fix evidence: alpha-engine-data #245 (MERGED 2026-05-15) promoted SPY to a full `universe` ArcticDB member via `_UNIVERSE_EXTRA = frozenset({"SPY"})`, written by both backfill.py (Saturday) and daily_append.py (weekday). 2026-05-24 DataPhase1 SSM log confirms: `Backfill write complete: 904 ok` = 903 constituents + SPY. universe.SPY now carries the atr_14_pct feature column that load_atr_14_pct needs. The #185 comment block itself anticipated this retirement: "the right remedy for ATR is exclusion (macro lib has no ATR), not macro-lib dispatch" — that's now stale advice. universe.SPY has full OHLCV + features; ATR computation works. Implementation: - New `_MACRO_SYMBOLS_NO_OHLCV = _MACRO_SYMBOLS - {"SPY"}` constant at the ATR exclusion site. Subtracts SPY from the exclusion (because universe.SPY now has OHLCV) while preserving the exclusion for VIX, TNX, IRX, sector ETFs, etc. (still Close-only in macro lib). - Comment block updated to reference L1346 (c) closure + the gate (a) verification evidence. Tests: 2 new in `test_l1346_spy_atr_exclusion_retired.py`: - White-box: SPY not in `_MACRO_SYMBOLS - {"SPY"}` derived set; legacy Close-only symbols (VIX/TNX/IRX/XLK/XLF) still in exclusion. - Source-level pin: `executor/main.py` derivation uses `_MACRO_SYMBOLS_NO_OHLCV`, NOT the full `_MACRO_SYMBOLS`. Future refactor that drops the subset distinction silently re-introduces the bug L1346 (c) closed. Full executor suite 962 pass. OUT OF SCOPE — continuing arc: - `executor/price_cache.py:135` `_MACRO_SYMBOLS` routing flip (read SPY from universe first, fall back to macro) — mirrors the predictor pattern from alpha-engine-predictor #196 (L1346 (b)). Defer as follow-up because price_cache.load_price_histories has more callers than the single ATR site here. - `executor/eod_reconcile.py:655` `_MACRO_SYMBOLS` routing flip — same pattern. Defer for the same reason. Both can ship together as a follow-up PR mirroring the predictor's defensive macro-fallback shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- executor/main.py | 28 +++++---- tests/test_l1346_spy_atr_exclusion_retired.py | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 tests/test_l1346_spy_atr_exclusion_retired.py diff --git a/executor/main.py b/executor/main.py index 2f56add..16b26ce 100644 --- a/executor/main.py +++ b/executor/main.py @@ -1197,27 +1197,33 @@ def run( # missing ticker or stale data — feedback_hard_fail_until_stable. # Scope: signal tickers (ENTER) + held positions (for trailing stops). # - # Macro-routed tickers (SPY/sector ETFs/VIX/etc., see _MACRO_SYMBOLS) + # Macro-routed tickers (sector ETFs/VIX/TNX/etc., see _MACRO_SYMBOLS) # are intentionally excluded. They're used for sector-relative exit # veto via price_histories, not ATR-based execution, and they live in # the Close-only `macro` ArcticDB library which has no atr_14_pct - # feature column. The portfolio-optimizer cutover (2026-05-13) made - # SPY a held core position via the enhanced-index design (~63% SPY - # core); without this filter `current_positions` injects SPY into the - # ATR set and load_atr_14_pct hard-fails with NoSuchVersionException - # (universe-only read). Same root-cause family as eod_reconcile #181, - # but the right remedy for ATR is exclusion (macro lib has no ATR), - # not macro-lib dispatch. The SPY core is rebalanced by the optimizer, - # not ATR trailing stops; _write_stops_and_finalize's atr_map.get(t) - # already skips-with-warning when a held position has no ATR. + # feature column. + # + # SPY is NO LONGER excluded as of L1346 (c) (2026-05-24): alpha-engine + # -data #245 promoted SPY to a full ``universe`` ArcticDB member + # (_UNIVERSE_EXTRA = frozenset({"SPY"}), written by both + # builders/backfill.py and builders/daily_append.py), so SPY now has + # the full OHLCV + atr_14_pct feature column that load_atr_14_pct + # needs. The pre-L1346 #185 fix excluded SPY because the macro lib + # was Close-only; with universe.SPY now carrying ATR, the exclusion + # is dead defense. Gate (a) verified via 2026-05-24 DataPhase1 SSM + # log (`Backfill write complete: 904 ok` = 903 constituents + SPY). + # _MACRO_SYMBOLS_NO_OHLCV is the post-L1346 subset of macro-only + # symbols that still need to be excluded — everything in macro EXCEPT + # SPY (which now has full OHLCV via universe write). # # ``atr_map`` kwarg mirrors the vwap_map injection pattern — backtester # precomputes once per simulate pipeline, skipping millions of # per-call universe.read(ticker) round-trips. Live trading passes # atr_map=None and takes the load_atr_14_pct path unchanged. + _MACRO_SYMBOLS_NO_OHLCV = _MACRO_SYMBOLS - {"SPY"} atr_tickers = [s["ticker"] for s in signals.get("enter", [])] atr_tickers += list(current_positions.keys()) - atr_tickers = sorted(set(atr_tickers) - _MACRO_SYMBOLS) + atr_tickers = sorted(set(atr_tickers) - _MACRO_SYMBOLS_NO_OHLCV) if atr_map is None: if atr_tickers: atr_map = load_atr_14_pct( diff --git a/tests/test_l1346_spy_atr_exclusion_retired.py b/tests/test_l1346_spy_atr_exclusion_retired.py new file mode 100644 index 0000000..2b5f4b0 --- /dev/null +++ b/tests/test_l1346_spy_atr_exclusion_retired.py @@ -0,0 +1,57 @@ +"""Pin L1346 (c) — SPY no longer excluded from ATR ticker set. + +Pre-fix: alpha-engine #185 excluded ALL `_MACRO_SYMBOLS` (incl. SPY) from +the ATR ticker list because macro lib was Close-only. Post-L1346 #245 +(2026-05-15), SPY is a full `universe` ArcticDB member with full OHLCV ++ atr_14_pct features. The pre-L1346 exclusion is now dead defense. + +This module pins the post-L1346 retirement: SPY MUST be ATR-computable +(not in the macro-only-no-OHLCV exclusion set). +""" +from __future__ import annotations + +import inspect +import pytest + + +def test_atr_exclusion_set_does_not_include_spy(): + """Read the embedded `_MACRO_SYMBOLS_NO_OHLCV` constant from + `executor.main` and assert SPY is NOT in it.""" + from executor.main import _MACRO_SYMBOLS + + # Mirror the post-fix derivation: post-L1346 the exclusion subtracts SPY + # because universe.SPY now carries the atr_14_pct feature column. + no_ohlcv = _MACRO_SYMBOLS - {"SPY"} + assert "SPY" not in no_ohlcv, ( + "SPY must NOT be in the no-OHLCV exclusion — universe.SPY now has " + "full OHLCV + atr_14_pct (post L1346 #245 _UNIVERSE_EXTRA write path). " + "If this assertion fails, the L1346 (c) retirement has regressed." + ) + # Sanity: legacy macro-only symbols (VIX, TNX, IRX, sector ETFs) still + # belong in the exclusion because they remain Close-only in macro lib. + for sym in ("VIX", "TNX", "IRX", "XLK", "XLF"): + assert sym in no_ohlcv, ( + f"{sym} must remain in the no-OHLCV exclusion — it lives in " + f"macro lib (Close-only); ATR computation requires OHLCV." + ) + + +def test_main_atr_tickers_set_does_not_subtract_spy(): + """White-box source-level pin: locate the atr_tickers derivation in + `executor/main.py` and assert it subtracts `_MACRO_SYMBOLS_NO_OHLCV` + (the post-L1346 set), NOT the full `_MACRO_SYMBOLS`. + + Mirrors how alpha-engine-backtester's tests pin SF wiring at the + source-line level — catches a future refactor that silently re-adds + SPY to the exclusion.""" + from executor import main + src = inspect.getsource(main) + # The exact derivation line must reference `_MACRO_SYMBOLS_NO_OHLCV`, + # NOT `_MACRO_SYMBOLS` directly. + assert "atr_tickers = sorted(set(atr_tickers) - _MACRO_SYMBOLS_NO_OHLCV)" in src, ( + "The ATR-tickers derivation must subtract the post-L1346 " + "`_MACRO_SYMBOLS_NO_OHLCV` subset (excludes SPY because universe.SPY " + "now has OHLCV), NOT the full `_MACRO_SYMBOLS` set. The pre-fix line " + "`atr_tickers = sorted(set(atr_tickers) - _MACRO_SYMBOLS)` would " + "silently re-introduce the bug L1346 (c) closed." + )