Skip to content

fix(executor): treat optimal zero-trade optimizer solve as a valid HOLD#196

Merged
cipher813 merged 1 commit into
mainfrom
fix/optimizer-loggable-zero-trade-hold
May 19, 2026
Merged

fix(executor): treat optimal zero-trade optimizer solve as a valid HOLD#196
cipher813 merged 1 commit into
mainfrom
fix/optimizer-loggable-zero-trade-hold

Conversation

@cipher813
Copy link
Copy Markdown
Owner

Surfaced by

The 2026-05-19 weekday-SF rerun (after the #194/#264 stack fixed the MorningEnrich timeout). With the pipeline running to completion, the planner hit:

ERROR main.run: use_portfolio_optimizer=True but optimizer log is not usable
(shadow_status='ok', diag='optimal') — leaving order book empty for safety.
Operator must investigate.

Root cause

is_log_usable() returned False on not would_be_trades. The shadow log was healthy: shadow_status=ok, diagnostics.status=optimal, target_weightscurrent_weights, turnover_one_way=0.0017 (0.17%, below the trade threshold) → would_be_trades=[]. That is the optimizer's legitimate "current portfolio already matches target — hold" verdict, conflated with genuine failures (None / non-ok / non-optimal diag).

Severity: low / cosmetic-but-misleading. It never suppressed real trades — on any day the optimizer wants trades, would_be_trades is non-empty so is_log_usable was already True. It only mislabeled valid hold-days, emitting a false "operator must investigate" ERROR and framing a correct hold as a safety fallback. The optimizer remains the authoritative trade driver (cutover live since 2026-05-13, config #172).

Fix

  • is_log_usable: usability depends solely on a clean ok + optimal/optimal_inaccurate solve. Emptiness of would_be_trades is the caller's concern, not a usability signal.
  • main.py: usable + zero entries/exits → INFO explicitly stating it is a valid HOLD (with turnover_one_way); the ERROR + "operator must investigate" + "leaving order book empty for safety" is now reserved strictly for genuine non-usable logs.
  • Tests: flip the bug-encoding test_is_log_usable_empty_trades to expect usable (renamed + regression-documented), add missing-key and apply-is-safe-noop coverage. Full suite: 914 passed.

Behaviorally inert for today (zero trades either way) — this stops the recurring false incident on every legitimate no-rebalance day. Kept as its own PR (sensitive optimizer-cutover path), separate from the 2026-05-19 SF-failure stack (#264/#233/#194/#265).

🤖 Generated with Claude Code

is_log_usable() returned False on empty would_be_trades, conflating the
optimizer's legitimate "current portfolio already matches target, hold"
verdict with genuine failures (None / non-ok / non-optimal diag). The
2026-05-19 weekday rerun solved optimal with turnover_one_way=0.0017
(0.17%, below the trade threshold) → would_be_trades=[]; the planner
then logged a false `optimizer log is not usable … Operator must
investigate` ERROR and framed a correct hold as a safety fallback.

The optimizer is the authoritative trade driver (cutover live since
2026-05-13). This never suppressed real trades — on any day the
optimizer wants trades, would_be_trades is non-empty so is_log_usable
was already True — it only mislabeled valid hold-days and cried wolf.

- is_log_usable: usability now depends solely on a clean ok/optimal
  (or optimal_inaccurate) solve. Emptiness is the caller's concern.
- main.py: usable + zero entries/exits → INFO stating it is a valid
  HOLD (with turnover_one_way); the ERROR + "operator must investigate"
  is now reserved strictly for genuine non-usable logs.
- tests: flip the (bug-encoding) empty-trades case to expect usable,
  add missing-key + apply-is-safe-noop coverage. Suite 914 passed.

Separate concern from the 2026-05-19 SF-failure PR stack; kept its own
PR in the sensitive optimizer-cutover path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cipher813 cipher813 merged commit 9ebff40 into main May 19, 2026
1 check passed
@cipher813 cipher813 deleted the fix/optimizer-loggable-zero-trade-hold branch May 19, 2026 15:49
cipher813 added a commit that referenced this pull request May 24, 2026
…first half (#207)

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) <noreply@anthropic.com>
cipher813 added a commit that referenced this pull request May 25, 2026
…refer universe.SPY with macro fallback (#208)

Completes the L1346 (c) retirement of SPY-special-cases across the
executor. Pre-fix sites used `if ticker in _MACRO_SYMBOLS` to route
SPY reads to macro library (Close-only); post-L1346 #245 universe.SPY
now has full OHLCV via `_UNIVERSE_EXTRA`.

Implementation (mirrors alpha-engine-predictor #196 defensive shape):

- `executor/price_cache.py::load_price_histories` — gates on
  `_MACRO_SYMBOLS_NO_OHLCV = _MACRO_SYMBOLS - {"SPY"}`. SPY routes to
  universe first; on universe-read failure, defensive fallback to
  macro.SPY (clearly logged via dual-exception-class read_errors line).
- `executor/eod_reconcile.py` — same routing flip + defensive fallback.

Non-SPY macro symbols (VIX/TNX/IRX/sector ETFs) remain macro-routed
(still Close-only in macro library; no universe entry).

Defensive fallback can be removed once L1346 (b)+(c) soak clean for
≥1 Saturday cycle (operator-promoted soak; ~1-2 Saturdays out).

Test plan:
- Full executor suite 962 pass.
- Pre-existing alpha-engine #185 ATR exclusion site already updated
  to `_MACRO_SYMBOLS_NO_OHLCV` (PR #207).

Closes ROADMAP L1346 (c) second half. Combined with #207 (first half)
+ #196 (predictor (b) gate), the entire L1346 retirement arc is now
shipped except the operator soak-watch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant