Skip to content

feat(ui): Loss Chance % heuristic chip (PR 4e)#72

Merged
dackclup merged 1 commit into
mainfrom
feat/phase-4e-loss-chance
May 15, 2026
Merged

feat(ui): Loss Chance % heuristic chip (PR 4e)#72
dackclup merged 1 commit into
mainfrom
feat/phase-4e-loss-chance

Conversation

@dackclup
Copy link
Copy Markdown
Owner

@dackclup dackclup commented May 14, 2026

Summary

PR 4e — the second user-facing feature of the Phase 4 UX trio (after PR 4d recommendation badge). Adds a deterministic 5-95% "Loss Chance" chip to every stock — answers "if I buy at the current price, what's my heuristic chance of loss?"

Tag target on merge: v1.1.0-rc2 per v1-to-v1-1-migration/PLAN.md sequencing.

Pre-implementation audit + calibration

Per user request "ตรวจสอบ 4e ว่าทุกอย่างถูกต้องแล้ว", validated PLAN against:

✅ Option D terminology locked
✅ Schema additive only (loss_chance_pct: float | None)
✅ Display position locked (after MoS bar)
✅ 5-band outlined-light + no dark: variants (lesson PR #70)
✅ Threshold-relative tests (lesson PR 4d)

Calibration delta — the PLAN draft weights yielded universe-median LC of 64 with bands skewed Sell-side. Re-calibrated against live S&P 500 production data:

Constant PLAN draft Locked
BASELINE_PCT 50 40
MOS_SCALE 0.4 (symmetric ±25) 0.35 (asymmetric: cap_neg=35, cap_pos=20)
COMPOSITE_SCALE /5 /2.0
DATA_QUALITY_PENALTY 20 15
ALTMAN_PENALTY 15 12
GOING_CONCERN_PENALTY 10 8

Predicted distribution

Applied to commit eb861e12 production data — universe median 49 (target ~50 ✅):

Band Count Target
<25% (Low) 7.6% ~10%
25-40% 20.9% ~20% ✅
40-60% (Neutral) 43.8% ~30%
60-80% 26.3% ~25% ✅
>80% (High) 1.4% ~15% (S&P 500 mostly fully-priced)

Per-recommendation tier median:

  • Bullish: 24 (target 10-25 ✅)
  • Lean Bullish: 42 (target 25-40)
  • Neutral: 56 (target 40-60 ✅)
  • Cautious: 62 (target 60-90 ✅)

Sample tier-aligned outputs:

  • HST LC=24 (Strong Buy, +14% MoS)
  • CF LC=28 (Buy, +9% MoS, beneish_high disqualifies SB)
  • NVDA LC=57 (Sell, -271% MoS, asymmetric cap limits LC)
  • WBD/SBUX/SJM LC=80+ (Sell, deep overvaluation + flags)

Files (+674 LOC)

Backend (4 files):

  • compute/scoring/loss_chance.py (new) — pure heuristic combiner, 13 exported constants
  • compute/output/schemas.pyloss_chance_pct field on StockSummary + StockDetail
  • compute/main.py — wires derive_loss_chance() after derive_recommendation()
  • tests/test_scoring/test_loss_chance.py (new, 21 tests)

Frontend (4 files):

  • frontend/components/LossChanceBadge.tsx (new) — 5-band outlined-light × 3 sizes; em-dash placeholder when MoS missing; italic "heuristic" qualifier on detail page
  • frontend/lib/types.tsloss_chance_pct field
  • frontend/lib/schema-snapshot.json — regenerated
  • frontend/components/RankingTable.tsx — new "Loss Chance" column after MoS; mobile-card row
  • frontend/app/stock/[ticker]/page.tsx — new metric column in header card next to MoS

Test plan

  • ruff check . → All passed
  • python -m pytest tests/ -m "not network"772 passed (was 751 + 21 new)
  • python -m compute.output.schema_check → in sync
  • npx tsc --noEmit → clean
  • npx next build → clean (502 SSG pages)
  • CI green on this PR
  • Post-merge weekly run: Loss Chance % field populates on all 502; distribution matches simulation; chip renders in correct band per stock

Notes

  • No new third-party deps
  • Schema additive — null for legacy data (badge renders em-dash)
  • Outlined-light + no dark: variants — matches lesson learned from PR ui(badge): unify recommendation badge with sector/MoS outlined-light pattern #70
  • Threshold-relative tests — re-tuning constants won't break tests
  • Disclaimer: chip text says "heuristic" not "probability" because the rubric is deterministic, not backtested. Backtest-calibrated probability requires Phase 5+ Triple-Barrier + Conformal Prediction work.

Next in Phase 4 UX trio: PR 4f (price-chart-enhancements) — depends on PR 4d's recommendation field for target-price-line conditional.

https://claude.ai/code/session_015649aRyi2bvciQYZVNACd2


Generated by Claude Code

Phase 4 UX feature #2 of the trio (after recommendation-badge / PR 4d).
Adds a deterministic 5-95% "Loss Chance" chip to every stock — answers
"if I buy at the current price, what's my heuristic chance of loss?"

Locked decisions (per phase-4-kickoff-checklist/PLAN.md §1):
- Terminology: Option D — "Loss Chance %" + small italic "heuristic"
  qualifier on detail page; tooltip qualifier on ranking row
- Display: after MoS bar on overview table + detail header card
- 5-band color: outlined-light pattern per frontend-design-system Rule 2
  (emerald-50/-50/slate-100/red-50/-50, NO `dark:` variants — lesson
  PR #70)
- Schema additive: `loss_chance_pct: float | None` on StockSummary +
  StockDetail

Calibration (locked 2026-05-14 post-simulation on live S&P 500):
The PLAN's draft weights (baseline 50, mos_scale 0.4, dq=20, altman=15)
yielded a universe-median LC of 64 with bands skewed Sell-side. Locked
constants:
  BASELINE_PCT             40   (not 50)
  MOS_SCALE                0.35 (asymmetric caps)
  MOS_NEG_CAP              35   (positive MoS push-down)
  MOS_POS_CAP              20   (negative MoS push-up)
  COMPOSITE_SCALE          2.0  (each 10 composite-pts → 5 pp)
  DATA_QUALITY_PENALTY     15   (was 20)
  ALTMAN_PENALTY           12   (was 15)
  GOING_CONCERN_PENALTY    8    (was 10)
  SLOAN_NSI_PENALTY        3    (was 5, per flag)
  BENEISH_DECHOW_PENALTY   3    (was 5)

Predicted distribution on commit eb861e1 production data:
  Universe median: 49 (target ~50 ✅)
  <25%:  7.6%   (target ~10%)
  25-40%: 20.9% (target ~20%)
  40-60%: 43.8% (target ~30%)
  60-80%: 26.3% (target ~25%)
  >80%:   1.4%  (target ~15% — S&P 500 mostly fully-priced)

Per-recommendation tier median:
  Bullish:      24  (target 10-25 ✅)
  Lean Bullish: 42  (target 25-40, slightly above)
  Neutral:      56  (target 40-60 ✅)
  Cautious:     62  (target 60-90 ✅)

Files (+450 LOC):
- compute/scoring/loss_chance.py (new) — pure heuristic combiner, ~13
  exported constants for downstream tuning, [5, 95] honest clip
- compute/output/schemas.py — `loss_chance_pct: float | None` on both
  StockSummary + StockDetail (additive, None for legacy data)
- compute/main.py — wire derive_loss_chance() in per-ticker loop after
  derive_recommendation()
- tests/test_scoring/test_loss_chance.py (new, 21 tests) — covers
  5-band gradient, MoS asymmetric caps, clipping, flag additivity,
  input shape tolerance, tier-aligned expectations. Threshold-relative
  so future tuning doesn't break tests
- frontend/lib/types.ts — mirror loss_chance_pct field
- frontend/lib/schema-snapshot.json — regenerated
- frontend/components/LossChanceBadge.tsx (new) — 5-band outlined-light
  badge, 3 sizes (xs/sm/md), optional italic "heuristic" qualifier for
  detail-page contexts, em-dash placeholder when MoS missing
- frontend/components/RankingTable.tsx — new "Loss Chance" column
  after MoS on desktop table; new row after MoS-and-fair-price on
  mobile card
- frontend/app/stock/[ticker]/page.tsx — new metric column in header
  card next to MoS, with showQualifier (small italic "heuristic" text)
- .claude/skills/phase-4/loss-chance/PLAN.md — marked ✅ Implemented;
  documented calibration delta vs draft rubric

No new third-party deps. No external API surface change beyond the
single additive Pydantic / TypeScript field.

Verification ladder:
- ruff check .                              → All passed
- python -m pytest tests/ -m "not network"  → 772 passed (was 751 + 21 new)
- python -m compute.output.schema_check     → in sync
- npx tsc --noEmit                          → clean
- npx next build                            → clean (502 SSG pages)

Tag target on merge: v1.1.0-rc2 per v1-to-v1-1-migration/PLAN.md
sequencing. Next: PR 4f (price-chart-enhancements).

https://claude.ai/code/session_015649aRyi2bvciQYZVNACd2
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quantrank Ready Ready Preview, Comment May 14, 2026 6:11pm

@dackclup dackclup marked this pull request as ready for review May 15, 2026 01:03
@dackclup dackclup merged commit e47fa37 into main May 15, 2026
4 checks passed
@dackclup dackclup deleted the feat/phase-4e-loss-chance branch May 15, 2026 01:04
dackclup added a commit that referenced this pull request May 28, 2026
Closes the silent-failure gap surfaced by Issue #287 PR A's Rule 18
instrumentation on cron Run #71 (368dccd, 2026-05-28 08:44 UTC). The
PR #292 GOOG/GOOGL per-class XBRL share-override fix did not fire in
production despite the code being correct on the runner.

Root cause (edgar-debugger 2026-05-28 verdict):

  - PR #292 (e9aaab3, 04:22 UTC) landed the per-class XBRL override
    at compute/ingest/fundamentals.py:1043-1067 (Branch 3 of
    _build_snapshot).
  - Branch 3 only executes on live EDGAR fetch — fetch_fundamentals
    short-circuits at _is_fresh() (line 1292-1294) when cached parquet
    age by latest_filed_date < FUNDAMENTALS_REFETCH_DAYS = 45.
  - Earlier same-day cron 0ad1d57 (03:22 UTC, pre-PR-#292) wrote a
    stale aggregate parquet (GOOG shares_outstanding = 12.116B).
  - Cron Run #71 restored that parquet from the GitHub Actions cache;
    _is_fresh() returned True on latest_filed_date=2026-04-30 (28d
    < 45d), and Branch 3 never ran.
  - metadata.multi_class_per_class_attempt_count = 0 (PR #292 Rule 18
    disambiguator working as designed — the smoking gun).
  - fundamentals_latency_p50_seconds = 0.0 (warm-cache replay confirmed
    universe-wide).

Fix scope (6 files, YAML + paired test bump):

  - .github/workflows/compute-rankings.yml — 3 instances cache-v4- →
    cache-v5- (key + 2 restore-keys) + comment block expanded to cite
    Issue #288 follow-up + PR #292 + PR #269 + introduce a 2-trigger
    bump taxonomy (schema change OR value-correctness fix in live-
    fetch-only path).
  - .github/workflows/pre-merge-prod-sim.yml — mirror 3-string flip
    per the file's own "bump together if either changes" comment.
    Without this the simulate workflow would lose all 11 warm caches
    on every PR.
  - tests/test_workflow_cache_coverage.py — paired-test bump per the
    PR 4c.1 v3→v4 precedent. Function renamed
    test_workflow_cache_key_is_v4 → _v5; docstring rewritten to cite
    Issue #288 + PR #292 + the 3-trigger bump taxonomy.
  - CLAUDE.md §Phase status — drain stale "in flight" wording for
    PR #297 (now merged) + empirical-validation note for cron Run #71
    + "in flight this PR" entry for the cache-v5 bump.
  - AGENTS.md open-issues list — update #287 (PR A merged via #297),
    #288 (fix in flight this PR), #289 (closed by PR #293).
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry appended per
    PR #237 side-file convention.

Why Option A (cache-key bump) over alternatives (per edgar-debugger):

  - Option B (targeted per-ticker invalidation): introduces cache-layer-
    knows-multi-class semantics + chicken-and-egg "detect stale
    aggregate from cached parquet" condition.
  - Option C (refactor override out of fetch path): cache hit triggers
    live SEC call (violates cache semantics) + FundamentalsSnapshot is
    frozen.
  - Option A: matches PR 4c.1 v3→v4 precedent exactly + zero compute/
    change + guaranteed correctness on next cron.

One-time cost: ~25-50 min cold cron on the immediately-following
weekly run (full S&P 500 universe live re-fetch). Subsequent crons
return to warm-cache ~5-10 min budget. No timeout-minutes impact —
PR #297 just bumped to 195m which absorbs cold-cache reality.

Verification on next cron Run #72:

  - metadata.multi_class_per_class_attempt_count = 2 (was 0)
  - metadata.multi_class_per_class_override_count = 2
  - stocks/GOOG.json shares_outstanding ≈ 5.429B (Class C, was 12.116B)
  - stocks/GOOGL.json shares_outstanding ≈ 5.822B (Class A, was 12.116B)
  - stocks/GOOG.json market_cap ≈ $2.09T (was $4.66T)
  - stocks/GOOGL.json market_cap ≈ $2.59T (was $4.71T)
  - metadata.fundamentals_latency_p50_seconds > 0.0 (live fetch active)

Adjacent findings deferred (NOT in this PR):

  - FOX / FOXA / NWS / NWSA: same multi_class_aggregate_shares_suspected
    annotate firing but they are on MULTI_CLASS_SHARE_ALLOWLIST
    (UNDERCOUNT path, PR #257). Decision on whether to add to overcount
    allowlist deferred to Q3 2026-08-19 quarterly cohort audit per
    methodology-scientist precedent (needs live XBRL probe).
  - OSAP wall-clock 347.1s on Run #71: cold OSAP download (cache > 31d
    mtime or evicted). Single observation; not a regression. Watch on
    next 2-3 crons.

Hard constraints honored:

  - No compute / scoring / schema / valuation / Rule 16 / Top-5
    invariant touched
  - No new defense flag · No new dep · No new env-var
  - YAML + paired-test diff (per quantrank-reviewer feedback on
    PR-title framing — original "YAML-only" was misleading)
  - Schema version UNCHANGED at 0.10.9-phase4.6 (no Pydantic / TS /
    snapshot change)

Pre-push 3-reviewer gate:

  - phase-coordinator Mode B (sonnet): LOCKSTEP-SATISFIED — both
    CLAUDE.md + AGENTS.md substance touched, INFLIGHT entry well-
    shaped, branch in-sync with origin/main (no rebase needed)
  - quantrank-reviewer (opus): FIX-AND-RE-REVIEW → 2 FAIL + 4 WARN.
    Both FAILs fixed in this commit (tests/test_workflow_cache_coverage.py
    test pin + pre-merge-prod-sim.yml cache-v4 stragglers). WARN 1
    (Issue #288 lifecycle) addressed via Reopens/Closes directives
    below. WARN 2 (AGENTS.md cron #69 cross-ref), WARN 3 (comment
    density) deferred — minor.

Reopens #288
Closes #288

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 28, 2026
…rkers + bump pointers (#299)

Closes today's 10-PR cycle (#286 / #290 / #291 / #292 / #293 / #294 /
#295 / #296 / #297 / #298). Mirror of PR #286 (post-v1.4.0 cycle
drain) for the post-cron-#71 cycle.

Three stale `(in flight, 2026-05-28)` markers in
PHASE_STATUS_INFLIGHT.md drained to `(merged 2026-05-28, <SHA>)`:

  - PR #295 (`2d2ec83e`) — Post-session housekeeping drain 6 INFLIGHT
    + bump pointers
  - PR #297 (`ecb60e64`) — Issue #287 PR A: durable timeout + cache
    canary + per-loop wall-clock Metadata (schema 0.10.8 → 0.10.9-phase4.6)
  - PR #298 (`030675e9`) — Issue #288 follow-up: cache-key bump v4 → v5

Bodies preserved (historical record).

CLAUDE.md §Phase status — drained the "(In flight this PR — cache-v5)"
qualifier (PR #298 merged) + added post-PR-#298 confirmation note +
cron Run #71 production-verified pointer.

AGENTS.md open-issues list — #288 status flipped "(fix in flight this
PR)" → "(closed by PR #298 cache-v5 bump)" + clarified the silent-
failure root-cause + Run #72 verification gate.

Why this PR exists: without end-of-day drain, session N+1 reading
CLAUDE.md / PHASE_STATUS_INFLIGHT.md would see 3 PRs still marked
"in flight" despite them merging hours earlier — the same friction
pattern PR #286 closed for the post-v1.4.0 cycle. Three same-day
drains in one PR keeps the side-file disciplined.

Scope (3 files, doc-only):

  - PHASE_STATUS_INFLIGHT.md — 3 header substitutions + this PR's
    own in-flight entry appended per PR #237 side-file convention
  - CLAUDE.md §Phase status pointer refresh
  - AGENTS.md open-issues list #288 status update

Hard constraints honored:

  - No code / scoring / schema / valuation / Rule 16 / Top-5
    invariant touched
  - No new defense flag · No new dep · No new env-var
  - Doc-only diff (Markdown only)
  - Schema version UNCHANGED at 0.10.9-phase4.6 (no Pydantic / TS /
    snapshot change)

PHASE_STATUS_INFLIGHT.md side-file satisfies §Conventions "ship with
every PR" lockstep per PR #237 convention. Same drain template as
PR #286 (post-v1.4.0 cycle).

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 28, 2026
… instrumentation

Methodology-scientist Mode B Q2 follow-up deferred from PR #294 (sector-
CoE flip, 2026-05-28 05:39 UTC). Adds
`Metadata.value_trap_risk_delta_by_sector: dict[str, int] | None` so
Q3 2026-08-19 quarterly cohort audit has visible per-sector shape
evidence — not just the aggregate `value_trap_risk_count_*_sector_coe`
scalars that landed in PR #204.

Schema PATCH bump 0.10.9 → 0.10.10-phase4.6 (additive Metadata-only).

Methodology context (Damodaran 2019 Ch. 8.4 §"Industry Beta"):

  After `USE_SECTOR_COE = True` per-sector Ke replaces the flat 10%
  baseline at SECTOR_COST_OF_EQUITY (11 GICS sectors, Ke 6%-12%).
  Directional predictions:

  - Lower-Ke sectors (Utilities ~6-7% / Real Estate ~7-8% / Consumer
    Staples ~7-8%): ROE ≥ Ke threshold relaxed → fewer RIM-skipped →
    POSITIVE delta (sector DROPPED flags)
  - Higher-Ke sectors (Information Technology ~11-12% / Energy
    ~10-12%): ROE ≥ Ke tightened → more RIM-skipped → NEGATIVE delta
  - Neutral sectors (6 GICS sectors at ~9-11%): small delta near zero

Cron #69 + Run #71 universe-wide already confirmed the aggregate:
132 → 109 (−23 tickers, −17.4%). This PR breaks the −23 down by sector.

Scope (10 files, additive only):

  - compute/output/schemas.py — new value_trap_risk_delta_by_sector
    field with full docstring (methodology-scientist verdict +
    Damodaran 2019 anchor + direction semantics)
  - frontend/lib/types.ts — mirror TS field as Record<string, number> | null
  - frontend/lib/schema-snapshot.json — regenerated via --update-snapshot
  - compute/config.py — SCHEMA_VERSION = "0.10.10-phase4.6"
  - compute/main.py — 3 surgical edits mirroring existing scalar
    dual-counter pattern (init two dict[str, int] counters / per-sector
    increment co-located with the existing scalar bump in both branches
    / delta computation in Metadata constructor)
  - tests/test_config.py — schema version pin bump + docstring rewrite
  - tests/test_output/test_value_trap_delta_by_sector_schema.py (NEW) —
    2 active GREEN schema-contract tests (mirror test_wall_clock_schema.py
    pattern from PR #297)
  - CLAUDE.md — §Phase status pointer block refresh
  - AGENTS.md — open-issues #67 status: flip landed + per-sector
    follow-up in flight this PR
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry per PR #237
    side-file convention

Implementation note:

  Per-sector dict construction uses
  `sorted(set(without) | set(with))` for stable key ordering;
  `.get(sec, 0)` fallback handles sectors appearing in only one path;
  `{} or None` falls back to None when both dicts are empty (test-mode
  universe). Co-located with the existing scalar bump in both
  `_rim_flat` (flat-Ke) and `_rim_sector` (sector-Ke) branches at the
  same `value_trap_risk_roe_below_cost_of_equity` reason guard — scalar
  and dict always stay in lockstep.

Verification ladder:

  - ruff check .                              PASS
  - python -m compute.output.schema_check     PASS (triple in sync 0.10.10)
  - pytest tests/test_config.py -v            11/11 PASS (pin held)
  - python -m pytest tests/test_output/       2/2 NEW PASS
  - Full offline suite via test-engineer      1367 → 1369 (+2 NEW)

Pre-push 3-reviewer gate:

  - schema-sentinel (sonnet)        PASS (52 fields, triple aligned,
                                    PATCH bump correct, snapshot
                                    alphabetical ordering held)
  - test-engineer (sonnet)          GREEN (2/2 new tests pass,
                                    1367 → 1369, 0 regressions,
                                    0 skipped stubs)
  - quantrank-reviewer (opus)       READY-TO-PUSH (0 FAIL, 4 WARN
                                    all pre-existing PR-#297-era
                                    drift, defer to next housekeeping
                                    PR — incl. SKILL.md/PHASE_STATUS.md
                                    schema-table tops still on 0.10.8)

Empirical validation gate (post-merge, next cron Run #72):

  - metadata.value_trap_risk_delta_by_sector populates as non-null dict
  - Damodaran shape directionally correct: Util/Real Estate/Staples
    POSITIVE, Information Technology/Energy NEGATIVE
  - sum(delta.values()) == without_sector_coe_count - with_sector_coe_count
    (= 23 per Run #71 universe-wide; matches within rounding)

Note: per-sector accumulation runs in the Step 8 per-ticker loop,
INDEPENDENT of cache-v5 cache busting (PR #298). Field populates on
next cron regardless of warm/cold fetch path.

Hard constraints honored:

  - No new defense flag · No scoring formula change · No Rule 16 /
    Top-5 violation
  - Additive-only schema change (PATCH bump)
  - Field nullable per Rule 18 graceful-degradation
  - Phase 4.5e PR 5 (cluster weight promotion) gate-data UNCHANGED —
    independent track

Methodology decision: methodology-scientist verdict NOT re-requested —
this is the EXACT field shape Mode B Q2 verdict from PR #294 explicitly
authorized. Future re-trigger only if post-merge cron shows sector
breakdown contradicting Damodaran prediction OR Q3 2026-08-19 audit
reads ≥ 6 crons of data and per-sector decay pattern needs interpretation.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa
dackclup added a commit that referenced this pull request May 28, 2026
…ta instrumentation (#300)

Methodology-scientist Mode B Q2 follow-up deferred from PR #294 (sector-
CoE flip, 2026-05-28 05:39 UTC). Adds
`Metadata.value_trap_risk_delta_by_sector: dict[str, int] | None` so
Q3 2026-08-19 quarterly cohort audit has visible per-sector shape
evidence — not just the aggregate `value_trap_risk_count_*_sector_coe`
scalars that landed in PR #204.

Schema PATCH bump 0.10.9 → 0.10.10-phase4.6 (additive Metadata-only).

Methodology context (Damodaran 2019 Ch. 8.4 §"Industry Beta"):

  After `USE_SECTOR_COE = True` per-sector Ke replaces the flat 10%
  baseline at SECTOR_COST_OF_EQUITY (11 GICS sectors, Ke 6%-12%).
  Directional predictions:

  - Lower-Ke sectors (Utilities ~6-7% / Real Estate ~7-8% / Consumer
    Staples ~7-8%): ROE ≥ Ke threshold relaxed → fewer RIM-skipped →
    POSITIVE delta (sector DROPPED flags)
  - Higher-Ke sectors (Information Technology ~11-12% / Energy
    ~10-12%): ROE ≥ Ke tightened → more RIM-skipped → NEGATIVE delta
  - Neutral sectors (6 GICS sectors at ~9-11%): small delta near zero

Cron #69 + Run #71 universe-wide already confirmed the aggregate:
132 → 109 (−23 tickers, −17.4%). This PR breaks the −23 down by sector.

Scope (10 files, additive only):

  - compute/output/schemas.py — new value_trap_risk_delta_by_sector
    field with full docstring (methodology-scientist verdict +
    Damodaran 2019 anchor + direction semantics)
  - frontend/lib/types.ts — mirror TS field as Record<string, number> | null
  - frontend/lib/schema-snapshot.json — regenerated via --update-snapshot
  - compute/config.py — SCHEMA_VERSION = "0.10.10-phase4.6"
  - compute/main.py — 3 surgical edits mirroring existing scalar
    dual-counter pattern (init two dict[str, int] counters / per-sector
    increment co-located with the existing scalar bump in both branches
    / delta computation in Metadata constructor)
  - tests/test_config.py — schema version pin bump + docstring rewrite
  - tests/test_output/test_value_trap_delta_by_sector_schema.py (NEW) —
    2 active GREEN schema-contract tests (mirror test_wall_clock_schema.py
    pattern from PR #297)
  - CLAUDE.md — §Phase status pointer block refresh
  - AGENTS.md — open-issues #67 status: flip landed + per-sector
    follow-up in flight this PR
  - PHASE_STATUS_INFLIGHT.md — full in-flight entry per PR #237
    side-file convention

Implementation note:

  Per-sector dict construction uses
  `sorted(set(without) | set(with))` for stable key ordering;
  `.get(sec, 0)` fallback handles sectors appearing in only one path;
  `{} or None` falls back to None when both dicts are empty (test-mode
  universe). Co-located with the existing scalar bump in both
  `_rim_flat` (flat-Ke) and `_rim_sector` (sector-Ke) branches at the
  same `value_trap_risk_roe_below_cost_of_equity` reason guard — scalar
  and dict always stay in lockstep.

Verification ladder:

  - ruff check .                              PASS
  - python -m compute.output.schema_check     PASS (triple in sync 0.10.10)
  - pytest tests/test_config.py -v            11/11 PASS (pin held)
  - python -m pytest tests/test_output/       2/2 NEW PASS
  - Full offline suite via test-engineer      1367 → 1369 (+2 NEW)

Pre-push 3-reviewer gate:

  - schema-sentinel (sonnet)        PASS (52 fields, triple aligned,
                                    PATCH bump correct, snapshot
                                    alphabetical ordering held)
  - test-engineer (sonnet)          GREEN (2/2 new tests pass,
                                    1367 → 1369, 0 regressions,
                                    0 skipped stubs)
  - quantrank-reviewer (opus)       READY-TO-PUSH (0 FAIL, 4 WARN
                                    all pre-existing PR-#297-era
                                    drift, defer to next housekeeping
                                    PR — incl. SKILL.md/PHASE_STATUS.md
                                    schema-table tops still on 0.10.8)

Empirical validation gate (post-merge, next cron Run #72):

  - metadata.value_trap_risk_delta_by_sector populates as non-null dict
  - Damodaran shape directionally correct: Util/Real Estate/Staples
    POSITIVE, Information Technology/Energy NEGATIVE
  - sum(delta.values()) == without_sector_coe_count - with_sector_coe_count
    (= 23 per Run #71 universe-wide; matches within rounding)

Note: per-sector accumulation runs in the Step 8 per-ticker loop,
INDEPENDENT of cache-v5 cache busting (PR #298). Field populates on
next cron regardless of warm/cold fetch path.

Hard constraints honored:

  - No new defense flag · No scoring formula change · No Rule 16 /
    Top-5 violation
  - Additive-only schema change (PATCH bump)
  - Field nullable per Rule 18 graceful-degradation
  - Phase 4.5e PR 5 (cluster weight promotion) gate-data UNCHANGED —
    independent track

Methodology decision: methodology-scientist verdict NOT re-requested —
this is the EXACT field shape Mode B Q2 verdict from PR #294 explicitly
authorized. Future re-trigger only if post-merge cron shows sector
breakdown contradicting Damodaran prediction OR Q3 2026-08-19 audit
reads ≥ 6 crons of data and per-sector decay pattern needs interpretation.

https://claude.ai/code/session_01AGU8d6pm4u2fQQ5cebg9qa

Co-authored-by: Claude <noreply@anthropic.com>
dackclup pushed a commit that referenced this pull request May 28, 2026
quantrank-reviewer (opus) flagged 3 non-blocking WARNs on the
initial PR 6 commit. 2 addressed inline; 1 deferred to follow-up.

- WARN-2 FIXED: `_NEGATION_REGEX` BEFORE branch gains an inline
  NOTE block explaining `no` is intentionally BEFORE-only (post-
  mention "10b5-1 plan, no shares sold" reads as affirmative
  disclosure + non-negation use of `no` → FP risk too high). AFTER
  branch carries a cross-reference comment so the asymmetry is
  self-documenting at the regex.
- WARN-3 FIXED: new
  `test_M3_negation_patterns_each_appear_in_compiled_regex`
  drift-detector — every token in `_NEGATION_PATTERNS` must appear
  somewhere in `_NEGATION_REGEX.pattern` source string. Catches
  drift in both directions (add to frozenset without updating
  regex; remove from regex without updating frozenset). Carve-outs
  handle the `cancelled` ↔ `canceled` collapse to `cancell?ed` and
  the multi-word `not in effect` `\s+` split. Tests 33 → 34;
  full suite 1399 → 1400.
- WARN-1 DEFERRED in `PHASE_STATUS_INFLIGHT.md` PR #303 entry:
  `_NEGATION_REGEX` anchor matches only 2 of upstream
  `detect_10b5_1_plan`'s 6 substrings. Bias direction remains safe
  (over-includes legit trades in cohort, never under-excludes).
  Fix path noted; gated on cron Run #72+ empirical data showing
  whether the gap is material.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 29, 2026
…al footgun #1) (#303)

* feat(scoring): Phase 4.5e PR 6 — Form-4 10b5-1 negation guard (residual footgun #1)

Closes the residual of footgun #1 from `compute/scoring/form4_signals.py`
module docstring + the PR 4-eq Mode B verdict (2026-05-23, pre-approved
"harden detect_10b5_1_plan with a negation guard against FP matches on
phrases like '10b5-1 plan terminated'"). PR 6 implements the engineering
of the approved mitigation.

**Architecture** — post-detector wrapper:
- `compute/scoring/form4_insider.py` gains `_NEGATION_PATTERNS` (11 tokens)
  + `_NEGATION_REGEX` (compiled bidirectional regex; case-insensitive;
  accepts both `10b5-1` and `10b-5-1` spellings; ±5 word-token window)
  + `_has_negation()` helper + thread-safe module-level counter
- `_detect_10b5_1_on_transaction` modified: detector returns True →
  check `_has_negation(resolved_text)` → on match return False and bump
  the counter; detector returns False or None → pass through unchanged.
  Guard never fabricates a positive signal (only downgrades True → False).

**Schema** `0.10.10 → 0.10.11-phase4.6` (PATCH — additive Metadata field).
Triple touched: Pydantic + TypeScript + snapshot regenerated; verified
clean via `python -m compute.output.schema_check`.

**Rule 18 observability**: new
`Metadata.form4_negation_guard_downgrade_count: int | None`. Counter
is module-level + thread-safe (`threading.Lock` around an int) for the
`EDGAR_MAX_WORKERS=8` parallel Form-4 fetch loop. `compute/main.py`
resets before the `ThreadPoolExecutor` block and reads on the success
path. `None` semantics mirrors `form4_wall_clock_seconds`: None when
`FORM4_FETCH_SKIP=1` OR outer try/except fired.

**Methodology** — pre-approved (no fresh consultation needed). Expected
delta firing-rate per Cohen 2008 §III + Jagolinzer 2009 §3.2:
`insider_sell_cluster` +5% to +10% relative on a universe-baseline cron
(absolute << 1%; most 10b5-1 disclosures are affirmative). Bias direction
reversed: was conservative (over-excluded legit opportunistic trades);
now closer to ground truth (terminated + former plans no longer fire).

**Tests** — 33 new tests in `tests/test_scoring/test_form4_negation_guard.py`:
13 pattern coverage (parametrized) + 5 negative paths + 2 ±5-token window
boundary + 2 spelling variants + 5 `_detect_10b5_1_on_transaction`
integration with mocked footnotes_dict + 2 thread-safety (incl. 100
concurrent bumps / 8 workers) + 2 Hypothesis properties (idempotence +
monotonicity) + 2 manifest pins. Suite 1366 → 1399; zero regressions;
ruff clean. `tests/test_config.py` schema pin bumped with PR 6 rationale.

**Defense layer unchanged** at 33 declared boolean flags (PR hardens an
existing input filter; no new flag). Cluster + C-suite weights UNCHANGED
(5.0 / 3.0; promotion to 7.0 still gated on Q3 2026-08-19 audit per
PR 4-eq verdict).

**Closes**: residual of footgun #1 (Form-4 10b5-1 contamination) from
`compute/scoring/form4_signals.py` module docstring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(scoring): PR 6 review WARN-2 + WARN-3 cleanup

quantrank-reviewer (opus) flagged 3 non-blocking WARNs on the
initial PR 6 commit. 2 addressed inline; 1 deferred to follow-up.

- WARN-2 FIXED: `_NEGATION_REGEX` BEFORE branch gains an inline
  NOTE block explaining `no` is intentionally BEFORE-only (post-
  mention "10b5-1 plan, no shares sold" reads as affirmative
  disclosure + non-negation use of `no` → FP risk too high). AFTER
  branch carries a cross-reference comment so the asymmetry is
  self-documenting at the regex.
- WARN-3 FIXED: new
  `test_M3_negation_patterns_each_appear_in_compiled_regex`
  drift-detector — every token in `_NEGATION_PATTERNS` must appear
  somewhere in `_NEGATION_REGEX.pattern` source string. Catches
  drift in both directions (add to frozenset without updating
  regex; remove from regex without updating frozenset). Carve-outs
  handle the `cancelled` ↔ `canceled` collapse to `cancell?ed` and
  the multi-word `not in effect` `\s+` split. Tests 33 → 34;
  full suite 1399 → 1400.
- WARN-1 DEFERRED in `PHASE_STATUS_INFLIGHT.md` PR #303 entry:
  `_NEGATION_REGEX` anchor matches only 2 of upstream
  `detect_10b5_1_plan`'s 6 substrings. Bias direction remains safe
  (over-includes legit trades in cohort, never under-excludes).
  Fix path noted; gated on cron Run #72+ empirical data showing
  whether the gap is material.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(phase-status): bump for PR #303 — schema 0.10.10 → 0.10.11 + Recently merged refresh (PR #300/#301/#302)

* docs(workflow): bump session-start pointer to schema 0.10.10 on main + PR #303 in-flight (Phase 4.5e PR 6 negation guard 0.10.11)

* test(form4): PR 6 cross-reference smoke tests in test_form4_insider.py — quality-gate Section D

* ci: re-trigger simulate on 0a1fc86 (prior run cancelled by concurrency supersede)

---------

Co-authored-by: Claude <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.

2 participants