feat(scanner): quality floor + regime-conditional ATR tilt (PRs D+E of intelligent risk-taking arc)#178
Merged
Conversation
…nt risk-taking arc)
Two new scanner-side filter functions, both Brian's "no defensive in
BULL" arc complement to the per-pick narrative penalty (research#177).
Where the narrative penalty operates on agent-generated qual theses,
these operate on the population candidate set BEFORE agents see it —
SOTA / institutional framing.
PR D — apply_quality_filter (Piotroski-lite):
Rejects names with no profitability signal. Default lenient mode (pass
if EITHER profitMargins > 0 OR returnOnEquity > 0); strict mode (BOTH
required) for BEAR regimes. Uses yfinance Ticker.info — same data
source as apply_balance_sheet_filter, can run in the same fetch pass.
Sector exemptions (Financial / Real Estate / Utilities) skip the gate
where metrics don't apply. Fail-closed on fetch error (M7 pattern).
Composes with the relaxed ATR ceiling shipped earlier in this arc
(max_atr_pct=25 admits TSLA/RKLB-class growth) — quality floor BELOW
the ceiling excludes lottery-ticket high-vol junk that would otherwise
pass the wider aperture.
PR E — apply_regime_atr_tilt:
In BULL regime: drops bottom-quartile-by-ATR per sector (those names
aren't contributing risk in a regime that rewards risk). In BEAR:
inverts to drop top-quartile (defensive tilt). NEUTRAL: no-op.
Per-sector grouping ensures within-sector composition is preserved
(an OW Tech sector doesn't lose all its names just because its mean
ATR is high vs. Healthcare). Sectors below min_sector_size skip the
tilt to avoid degenerate quartile cuts. Missing-ATR candidates kept
as-is (fail-open on the ranking input).
Composes with sector OW/UW (drives WHICH sectors get exposure) and
max_atr_pct ceiling (drives the absolute upper bound). This is the
within-sector RELATIVE tilt — the missing leg of the three-knob stack.
14 new tests (6 quality_floor + 8 atr_tilt) cover sector exemptions,
strict vs lenient mode, fail-closed, regime invert, sector isolation,
small-sector skip, missing-ATR fail-open, unknown-regime no-op.
Wiring (caller side, gitignored data/population_selector.py):
After existing apply_balance_sheet_filter call:
if QUALITY_FLOOR_ENABLED:
candidates = apply_quality_filter(candidates, sector_map=sector_map)
if REGIME_ATR_TILT_ENABLED:
candidates = apply_regime_atr_tilt(
candidates, market_regime=market_regime, sector_map=sector_map,
)
Full suite 1120 passed (was 1106 before this PR). No regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
cipher813
added a commit
that referenced
this pull request
May 14, 2026
…ubstrate, 260513 plan) (#180) Wires the Phase 1c factor composites (quality / momentum / value / low_vol, shipped 2026-05-13 in scoring/factor_scoring.py per PR #179) into the existing composite score via a regime-conditional convex blend. Plan doc: ~/Development/alpha-engine-docs/private/factor-substrate-260513.md ROADMAP entry: alpha-engine-config private-docs/ROADMAP.md ~L1276. Changes: scoring/composite.py - compute_factor_subscore(profile, regime, regime_weights) — linear combination of the 4 within-sector factor composites with signed regime-conditional weights, renormalized by absolute-weight sum so partial coverage stays on the 0-100 scale, then clamped. - compute_composite_score gains factor_subscore + factor_weight kwargs (defaults 0/0.0 — fully backward-compatible). When both are non-zero the quant_qual_base is convex-combined with the subscore: weighted_base = (1 - factor_weight) × quant_qual_base + factor_weight × factor_subscore Returns echo factor_subscore + factor_weight_applied for capture. graph/research_graph.py - score_aggregator reads factors/profiles/latest.json once at the top (gracefully degrades when the artifact is missing — None subscore flows through the factor-weight=0 path in compute_composite_score). - Per-ticker subscore + breakdown threaded into investment_thesis + signals.json (factor_subscore, factor_weight_applied, factor_blend_breakdown) for downstream observability. - Single end-of-loop log line surfaces applied/skipped coverage. config.py - Loads aggregator.factor_blend block: FACTOR_BLEND_ENABLED, FACTOR_BLEND_WEIGHT, FACTOR_BLEND_REGIME_WEIGHTS{bull,bear,neutral}. tests/test_factor_blend.py - 22 tests covering compute_factor_subscore (regime branches, partial coverage, clamp bounds, none/empty inputs) + compute_composite_score factor-aware path (blend applied vs skipped, backward compatibility, macro-shift composition, clamping). Acceptance (per ROADMAP Phase 3): ✓ _build_signals_payload outputs include factor_subscore + factor_blend_breakdown per signal ✓ score_aggregator log shows factor_blend coverage per run YAML config block ships in companion alpha-engine-config PR; the auto-deploy-on-config-changed workflow will pick up the change on merge of both PRs. Test plan: - [x] 22 new unit tests in tests/test_factor_blend.py — all pass - [x] Full suite: 1158 passing (was 1136 + 22) - [x] Smoke check: from config import FACTOR_BLEND_* loads cleanly - [ ] Sat 5/17 02:00 PT SF firing: confirm signals.json per-pick has factor_subscore populated, score_aggregator log shows non-zero applied count Composes with: - PR #179 (Phase 1c — factor profile writer, MERGED) - PR #176/#177/#178 (tactical relief layer — narrative penalty + coherence gate kept; PR #177/#178 retired in Phase 4) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
4 tasks
cipher813
added a commit
that referenced
this pull request
May 14, 2026
…f factor substrate) (#181) Phase 4 of the factor substrate arc (plan doc: ~/Development/alpha-engine-docs/private/factor-substrate-260513.md). ROADMAP entry: alpha-engine-config/private-docs/ROADMAP.md ~L1276 (P0, Sat 5/16 cron target). NEW — Factor quality floor in _build_signals_payload: graph/research_graph.py - Structural floor on within-sector quality_score percentile (raw 0-100 from the Phase 1c factor_scoring module), applied at the buy_candidates construction step alongside the existing macro- sector coherence gate. - Blocks NEW ENTER signals whose factor_quality_score is below the configured percentile floor (default 10.0 = bottom decile). - Exempt sectors bypass the floor (Financial / Real Estate / Utilities by default — where the underlying quality factor metrics do not apply). - Graceful degrade: tickers without a factor profile (None quality_score) pass through — same pattern as the rest of the factor blend. - factor_quality_score threaded onto investment_thesis → signals → universe → buy_candidates for end-to-end observability. config.py - New aggregator.factor_quality_floor block loader: FACTOR_QUALITY_FLOOR_ENABLED, FACTOR_QUALITY_FLOOR_MIN_PERCENTILE, FACTOR_QUALITY_FLOOR_EXEMPT_SECTORS. tests/test_factor_quality_floor.py - 7 new tests: below-floor blocked, at-floor allowed, exempt-sector bypass, missing quality_score lets through, disabled is no-op, gate only affects ENTER (not HOLD/EXIT), composition with macro-sector coherence gate. RETIRED — narrative penalty + dormant scanner functions: scoring/composite.py - DELETE compute_narrative_regime_adjustment — the text-match defensive/growth marker logic from PR #177 is replaced by the factor-driven composite (Phase 3) that doesn't need string matching. graph/research_graph.py - DELETE the score_aggregator branch that called compute_narrative_regime_adjustment + the per-thesis narrative_regime_adj / narrative_regime_details fields. - DELETE NARRATIVE_* imports from config. data/scanner.py - DELETE apply_quality_filter (Piotroski-lite — replaced by the structural factor_quality_floor at the _build_signals_payload level, working on the within-sector factor percentile instead of raw yfinance profitMargins/ROE). - DELETE apply_regime_atr_tilt (regime-conditional ATR tilt — replaced by the factor blend's signed low_vol weight in aggregator.factor_blend: BULL: -0.10, BEAR: +0.40). - DELETE QUALITY_*/REGIME_ATR_* imports from config. - Both functions were dormant — `merged but unwired` per the plan doc's tactical-relief-layer table — no production callers existed. config.py - DELETE _NARRATIVE_PENALTY_CFG block + 8 constants. - DELETE _QUALITY_FLOOR_CFG block + 5 constants. - DELETE _REGIME_ATR_TILT_CFG block + 5 constants. tests/test_narrative_regime_penalty.py — DELETE (function gone). tests/test_scanner_quality_floor_and_atr_tilt.py — DELETE (functions gone). Net change: -400 lines of dead-code + dead-test surface, +296 lines of factor-quality-floor + tests. Suite 1133 → 1140 passing. Acceptance (per ROADMAP Phase 4): ✓ data/scanner.py loses ~177 lines of dormant PR #178 functions ✓ scoring/composite.py loses 66 lines of PR #177 narrative logic ✓ Quality floor structural rejection lives in _build_signals_payload alongside macro-sector coherence gate ✓ Suite passes — no production callers were affected by the deletions Companion alpha-engine-config PR rolls the corresponding YAML changes: - DELETE aggregator.narrative_regime_penalty block (40 lines) - DELETE scanner.quality_floor + scanner.regime_atr_tilt blocks - ADD aggregator.factor_quality_floor block Composes with: - #179 (Phase 1c — factor profile writer, MERGED) - #180 (Phase 3 — factor blend wiring) - #176 (macro-sector coherence gate — KEPT; it gates on score, not quality, and the two compose at the same buy_candidates step) - #177 (narrative penalty — RETIRED here) - #178 (dormant scanner quality_floor + regime_atr_tilt — RETIRED here) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cipher813
added a commit
that referenced
this pull request
May 14, 2026
… (Stage C.2 T3 prep) (#187) Surfaces the quantitative regime substrate in the macro economist's decision-capture snapshot so the LLM-as-judge sees it in agent_input. Required by the upcoming alpha-engine-config rubric PR that adds a ``regime_decision_process`` dimension scoring the agent's regime call against the substrate. graph/decision_capture_helpers.py - ``build_macro_economist_capture_payload`` now reads ``state["regime_substrate"]`` and includes it in the snapshot. ``None`` flows through cleanly (Stage A pre-deploy or non-blocking SF Catch tripped) — judge anchor handles substrate-absent case in the rubric (score 5, mirroring no-prior-macro-report convention). - Summary string surfaces a compact substrate marker: ``regime_substrate=present(argmax=bear,intensity_z=-1.80)`` or ``regime_substrate=absent`` for operator/CW log clarity. tests/test_decision_capture_integration.py (+3 new) - regime_substrate present → ends up in snapshot + summary marker - regime_substrate=None → snapshot field is None + summary marks absent - regime_substrate missing entirely → treated as absent (defensive) tests/test_prompts_no_inline_json_schema.py - test_macro_agent_format_passes_canonical_sectors updated to pass ``regime_substrate="..."`` to ``_PROMPT_TEMPLATE.format()`` — the template added a {regime_substrate} placeholder in alpha-engine-config v1.3.0 (PR #178, merged). Without the kwarg the format call now raises KeyError. Pin uses the canonical "not available this run" fallback string so the test exercises the same shape the agent's _format_regime_substrate(None) emits. 1223 passed. Composes with: - alpha-engine-research PR #186 (merged — Stage C.1 substrate consumption) - alpha-engine-config PR #178 (merged — macro_agent.txt v1.3.0) - alpha-engine-config follow-up — adds the regime_decision_process rubric dimension that scores the agent against the now-visible substrate. Ships as a separate PR (rubric YAML, no code). Reference: ~/Development/alpha-engine-docs/private/regime-v3-260514.md §5.3.3 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cipher813
added a commit
that referenced
this pull request
May 19, 2026
…R 4) (#205) Plan regime-drawdown-hysteresis-260518.md §6 item 4 (ROADMAP L99). The producer arc (predictor #176/#177/#179, ae #193) shipped 2026-05-19 and now fills substrate["drawdown"] + substrate["effective_regime"] weekly; this is the research consumer surface. - agents/macro_agent.py: new _format_drawdown_leg() renders the SPY tiered drawdown + book-vs-market excess as a continuous, market-grounded statement plus the composed effective_regime (most-protective over the ensemble). The HMM "weeks_in_current_state" line is reframed as a filter-stability DIAGNOSTIC, explicitly NOT a market-duration statement (origin: the 2026-05-15 brief's misleading "P(neutral)=1.00, 50 weeks in state"). - graph/research_graph.py: _build_regime_trend drops the "Weeks in State" column for a continuous "SPY Drawdown" + composed "Effective" column, and the Summary line carries the continuous statement ("SPY X% off trailing peak; book Y% off NAV HWM; Z pp deeper"). Fallbacks (both tested): - absent-key: no substrate["drawdown"] (pre-#176/#179) ⇒ the leg block is omitted entirely — byte-identical to the prior HMM-only path. - portfolio-unavailable: drawdown.excess.available=False ⇒ SPY leg still renders; excess line states it is unavailable; never raises. flag-name note: consumers/config use drawdown_regime_enabled (matches shipped predictor #178 + ROADMAP/SYSTEM_STATE), not the plan doc's stray L214 regime_drawdown_enabled spelling. Tests: +8 (drawdown columns, continuous summary, absent-key, NAV unavailable, macro-prompt leg rendering). max_tokens lint allowlist line ref bumped 476→562 (critic call relocated, unchanged 512 literal). Full suite 1372 passed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why
Brian 2026-05-13 "no defensive in BULL" arc — these are the SOTA / institutional complement to the per-pick narrative penalty (research#177). Where the narrative penalty operates on agent-generated qual theses, these operate on the population candidate set BEFORE agents see it.
PR D closes the gap I flagged when shipping `max_atr_pct: 18 → 25` in the macro-shift PR — wider ATR aperture admits TSLA/RKLB-class growth but also admits lottery-ticket high-vol junk that has no profitability anchor. Quality floor BELOW the ceiling excludes the junk while keeping the legitimate growth.
PR E is the within-sector relative tilt (the missing leg of the three-knob stack that already includes sector OW/UW + ATR ceiling). In a confirmed bull regime, bottom-quartile-ATR Tech names aren't doing the work the regime calls for — drop them within Tech, keep the within-sector OW.
Companion PR
alpha-engine-config feat/scanner-quality-floor-and-regime-atr-tilt ships the YAML + config.py constants both functions read.
Wiring (manual one-time edit in gitignored data/population_selector.py)
The new functions are unused until called. Add after the existing `apply_balance_sheet_filter` invocation:
```python
if QUALITY_FLOOR_ENABLED:
candidates = apply_quality_filter(candidates, sector_map=sector_map)
if REGIME_ATR_TILT_ENABLED:
candidates = apply_regime_atr_tilt(
candidates, market_regime=market_regime, sector_map=sector_map,
)
```
Both flags default to `enabled: true` so the wire-in is the only thing standing between this code and live behavior.
Composes with merged PRs in this arc
Together with this PR, five gates filter the population in BULL regime:
Test plan
🤖 Generated with Claude Code