Skip to content

docs(phase4.5c): mark 4.5c wave ✅ DONE + bump next deliverable to 4.5d#96

Merged
dackclup merged 1 commit into
mainfrom
docs/mark-4.5c-done
May 17, 2026
Merged

docs(phase4.5c): mark 4.5c wave ✅ DONE + bump next deliverable to 4.5d#96
dackclup merged 1 commit into
mainfrom
docs/mark-4.5c-done

Conversation

@dackclup
Copy link
Copy Markdown
Owner

Summary

Phase 4.5c Roychowdhury REM shipped via PR #95. Production verified on run #49 (commit 65097703, warm-cache 6m25s).

Production stats

Metric Value
Fire rate 16 / 502 (3.2%) — within H0-to-correlation expected 2.8-7%
Tickers fired SMCI · WAT · ADM · TSN · HRL · STLD · FSLR · JBL · COHR · LII · LDOS · POOL · OMC · WY · TECH · RVTY
Orthogonality check NVDA / PLTR (Beneish-veto fired) NOT in REM list — confirms 4.5c captures real-manipulation signal orthogonal to accrual targets ✓
Real-world coverage ADM (2024 SEC investigation) · SMCI (2024 investigation) · TSN / HRL (periodic scrutiny) · FSLR (solar channel-stuffing history)

End-state defense layer

  • Active vetoes: 7 (unchanged — 4.5c is annotate-only)
  • Annotate flags: 7 → 8 (+ rem_suspect)
  • Reason taxonomy: 31 → 32
  • Defense layer 9 → 14 after 4.5a + 4.5b + 4.5c

No schema delta. SCHEMA_VERSION stays 0.7.1-phase4g.

Triple-doc lockstep

File Change
CLAUDE.md "Next deliverable" 4.5c → 4.5d. Defense layer "9 → 13" → "9 → 14". 4.5c results + ticker list + orthogonality note inserted.
PHASE_STATUS.md Phase 4.5 row updated. §4.5c header flipped to ✅ DONE 2026-05-17 with results table. Original plan text preserved below.
WORKFLOW.md §4.5c checkboxes [ ] → [x] with PR #95 / LOC / test-count / production-verification citations + golden-test reference.

Next deliverable

Phase 4.5d — earnings-quality time-series + Burgstahler-Dichev kink (~180 LOC, ~7 days):

  • m_score_deteriorating annotate — Δ(Beneish M-score) > +0.5 over trailing 3y
  • loss_avoidance_pattern annotate — NI ∈ [0, $5M] OR EPS ∈ [0, $0.05] for 3+ consecutive years (Burgstahler-Dichev 1997)

Test plan

  • No code changes; docs only
  • grep "Next deliverable.*4.5c" returns 0 hits
  • grep "9 → 14" in CLAUDE.md
  • grep "rem_suspect" in active references

https://claude.ai/code/session_015649aRyi2bvciQYZVNACd2


Generated by Claude Code

Phase 4.5c Roychowdhury REM shipped via PR #95. Production verified
on run #49 (commit `65097703`, warm-cache 6m25s — all 9 cache layers
populated).

## What shipped

`rem_suspect` annotate via per-sector OLS regressions on 3 abnormal
proxies (CFO, Production, Discretionary Expenses). Module
`compute/scoring/rem.py` (~420 LOC, pure-numpy via
`np.linalg.lstsq`, no sklearn/statsmodels dep). 14 offline tests
including golden numerical test recovering known-DGP coefficients.

## Production verification

| Metric | Value |
|---|---|
| Fire rate | **16 / 502 (3.2%)** — within H0-to-correlation expected 2.8-7% |
| Tickers fired | SMCI · WAT · ADM · TSN · HRL · STLD · FSLR · JBL · COHR · LII · LDOS · POOL · OMC · WY · TECH · RVTY |
| Orthogonality check | NVDA / PLTR (Beneish-veto fired) **NOT** in REM list — confirms 4.5c captures real-manipulation signal orthogonal to accrual targets |
| Real-world coverage | ADM (2024 SEC investigation) · SMCI (2024 investigation) · TSN / HRL (periodic scrutiny) · FSLR (solar channel-stuffing history) |

## End-state defense layer

- Active vetoes: **7** (unchanged — 4.5c is annotate-only)
- Annotate flags: 7 → **8** (+ `rem_suspect`)
- Reason taxonomy: 31 → **32**
- **Defense layer 9 → 14 after 4.5a + 4.5b + 4.5c**

No schema delta — `rem_suspect` is a string in existing
`valuation_warnings: list[str]`. `SCHEMA_VERSION` stays
`0.7.1-phase4g`.

## Triple-doc lockstep changes

| File | Change |
|---|---|
| `CLAUDE.md` | "Next deliverable" 4.5c → 4.5d. Defense layer "9 → 13 after 4.5a+4.5b" → "9 → 14 after 4.5a+4.5b+4.5c". 4.5c results + ticker list + orthogonality note inserted between 4.5b and the post-completion roadmap. |
| `PHASE_STATUS.md` | Phase 4.5 row updated with 4.5c production stats. §4.5c header flipped to ✅ DONE 2026-05-17 with results table + orthogonality note. Original plan text preserved below for audit. |
| `WORKFLOW.md` | §4.5c checkboxes [ ] → [x] with PR-number / LOC / test-count / production-verification citations + golden-test reference. |

## Next deliverable

**Phase 4.5d — earnings-quality time-series + Burgstahler-Dichev
kink at zero** (~180 LOC, ~7 days):
- `m_score_deteriorating` annotate — Δ(Beneish M-score) > +0.5
  over trailing 3y (manipulation gathering steam)
- `loss_avoidance_pattern` annotate — NI ∈ [0, $5M] OR EPS ∈
  [0, $0.05] for 3+ consecutive years (Burgstahler-Dichev 1997 kink)

## Audit trail (post-v1.0 doc PRs)

| PR | Purpose |
|---|---|
| #81 | 4g ✅ DONE |
| #86 | Phase 4.5 roadmap added |
| #87 | "PR 4b next" → "§3 polish next" (was wrong) |
| #88 | "§3 polish next" → "Phase-5 blocked" (was wrong) |
| #92 | 4.5a wave ✅ DONE |
| #94 | 4.5b wave ✅ DONE |
| **this PR** | 4.5c wave ✅ DONE |

## Verification

- No code changes; docs only
- `grep "Next deliverable.*4.5c"` returns 0 hits (all moved to 4.5d)
- `grep "9 → 14"` appears in CLAUDE.md (new defense layer count)
- `grep "rem_suspect"` appears in PHASE_STATUS.md + WORKFLOW.md
  active-flags references

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

vercel Bot commented May 17, 2026

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

Project Deployment Actions Updated (UTC)
quantrank Ready Ready Preview, Comment May 17, 2026 4:59am

@dackclup dackclup marked this pull request as ready for review May 17, 2026 04:59
@dackclup dackclup merged commit eff9f1d into main May 17, 2026
4 checks passed
@dackclup dackclup deleted the docs/mark-4.5c-done branch May 17, 2026 05:00
dackclup added a commit that referenced this pull request May 17, 2026
…als_momentum_high + loss_avoidance_pattern) (#97)

Phase 4.5 manipulation-defense cluster sub-PR 6 of 6 (the last
purely-defense sub before 4.5f composite + UI bundling). Two
annotate-only flags derived from the per-ticker fundamentals
history (annual XBRL).

## What's new

### `accruals_momentum_high` — Δ(TATA) over 3y > +0.05

- TATA = (NetIncome − OperatingCashFlow) / TotalAssets, the Sloan
  1996 / Beneish 1999 accruals backbone.
- Threshold +0.05 ≈ Beneish 1999 ΔM > +0.5 via the β_TATA = 4.679
  coefficient (ΔM ≈ 4.679 × ΔTATA → ΔM > 0.5 ⇔ ΔTATA > 0.107). We
  use 0.05 — more sensitive since TATA alone captures less than
  the full 8-ratio signal; standard practitioner adaptation when
  shortening to one ratio.
- Catches manipulation **gathering steam** — the snapshot-only
  Sloan + Beneish flags miss the trajectory entirely.

**Practical note on naming**: PR #86 plan §4.5d called this
`m_score_deteriorating` (full Δ(Beneish M-score) > +0.5). We
chose TATA momentum as a practical equivalent: building 3
historical 8-ratio Beneish snapshots from XBRL history would
require expanding the annual-history coverage of 6+ supplementary
ratios (DSRI / GMI / AQI / etc.) that often have gaps for prior
years. TATA is the single Beneish component that's a level rather
than a ratio-of-ratios, and Sloan 1996 established it as the
standalone accruals signal — so this is a clean shortening, not a
weakening.

### `loss_avoidance_pattern` — Burgstahler-Dichev 1997 kink at zero

- Fires when **3+ consecutive fiscal years** of tiny-positive
  earnings: NI ∈ [\$0, \$5M] **OR** EPS ∈ [\$0.00, \$0.05].
- Per-share band catches the high-share-count case where NI alone
  is above the absolute floor but per-share is still tiny.
- Empirical kink-at-zero signature of managers shading reported
  earnings just enough to clear the loss / loss-threshold.

## Architecture

| File | Change |
|---|---|
| `compute/scoring/earnings_quality.py` | **NEW** ~250 LOC — `check_accruals_momentum` + `check_loss_avoidance` + history-walk helpers (`_annual_values`, `_value_at_year`). Pure pandas; no new deps. |
| `compute/main.py` | + 2 import lines + 2 per-ticker annotate appends in the Step-8 loop, slotting after `rem_suspect`. |
| `tests/test_scoring/test_earnings_quality.py` | **NEW** ~225 LOC — 14 offline tests covering both flags (fires / doesn't fire / improves / threshold pins / EPS-band fallback / negative-NI rejection / large-NI rejection / multi-year streak / streak break / constants sanity). |

## Defense-layer end-state (after this PR ships)

- Active vetoes: **7** (unchanged — 4.5d is annotate-only)
- Annotate flags: 8 → **10** (+ `accruals_momentum_high`,
  `loss_avoidance_pattern`)
- Reason taxonomy: 32 → **34** stable identifiers
- Total defense layers: **9 → 16** after 4.5a + 4.5b + 4.5c + 4.5d

No schema delta — both flags are strings in existing
`valuation_warnings: list[str]`. `SCHEMA_VERSION` stays
`0.7.1-phase4g`.

## Backward compat

- Both check functions take `(snap, history)` — no caller
  changes elsewhere. Missing inputs (snap=None, no history,
  insufficient years) cleanly return fired=False.
- No new EDGAR fetches — both flags read from existing
  fundamentals + fundamentals_history caches.

## Verification ladder

- ✅ `ruff check .` — clean
- ✅ `pytest tests/test_scoring/test_earnings_quality.py` —
  **14 passed**
- ✅ `pytest tests/ -m "not network"` — **831 passed** (was 817;
  +14 new)
- ✅ schema_check — N/A (no schema delta)
- ⏳ Production verification deferred. Expected fire rates on S&P 500:
  - `accruals_momentum_high` ~3-8% (~15-40 tickers) — H0 from
    Δ(TATA) > 0.05 base rate
  - `loss_avoidance_pattern` ~1-3% (~5-15 tickers) — S&P 500 firms
    rarely report tiny-positive earnings for 3+ years (mega-cap
    distribution); base rate higher on small-caps per Burgstahler-
    Dichev 1997 original sample

## Sibling sub-PRs (Phase 4.5 cluster)

- ✅ **4.5a wave** (PRs #89 / #90 / #91 + #92 docs)
- ✅ **4.5b** (PR #93 + #94 docs)
- ✅ **4.5c** (PR #95 + #96 docs)
- **4.5d (this PR)** — earnings-quality time-series
- ⬜ **4.5e** — Form 4 insider clustering (~420 LOC, ~12 days —
  needs new SEC Form 4 parser)
- ⬜ **4.5f** — `manipulation_index` composite + composite-score
  penalty + UI pillar card + README Honest Limitations + schema
  bump → **v1.2.0-phase4.5**

https://claude.ai/code/session_015649aRyi2bvciQYZVNACd2

Co-authored-by: Claude <noreply@anthropic.com>
dackclup added a commit that referenced this pull request May 17, 2026
…e Google Fonts dependency (#98)

PR #96 CI run #249 (docs(phase4.5c) merge to main) failed the
Frontend (build) job with:

  NextFontError: Failed to fetch \`IBM Plex Sans\` from Google Fonts.
    at app/layout.tsx — IBM_Plex_Sans({subsets: ['latin'],
                                       weight: ['400','500','600','700'],
                                       display: 'swap',
                                       variable: '--font-ibm-plex-sans'})

A transient Google Fonts network blip during CI build broke
`next/font/google`. The very next CI run (PR #97 4.5d) went green
again — so the failure was a flaky external service, not a code
regression. Production was unaffected (compute-rankings.yml runs
independently of ci.yml).

This commit closes the door on the failure mode for good by
moving all 3 fonts from `next/font/google` to `@fontsource/*`
npm packages (SIL Open Font License, OFL — free to redistribute):

- `@fontsource/ibm-plex-sans` @ ^5.2.8
- `@fontsource/jetbrains-mono` @ ^5.2.8
- `@fontsource/instrument-serif` @ ^5.2.8

@fontsource bundles the woff2 files into node_modules, so the
Next.js build never touches an external host.

## Changes

| File | Change |
|---|---|
| `frontend/package.json` | + 3 `@fontsource/*` dependencies (and corresponding `package-lock.json` entries) |
| `frontend/app/layout.tsx` | Drop `next/font/google` imports (`IBM_Plex_Sans`, `JetBrains_Mono`, `Instrument_Serif`). Drop the `className={...variable}` stack on `<html>`. CSS variables now declared in globals.css. |
| `frontend/app/globals.css` | + 9 `@import '@fontsource/*'` lines (Plex 400/500/600/700, JetBrains 400/500/600, Instrument 400 normal + italic — exactly matches the prior next/font config). + 3 new CSS variable declarations (`--font-ibm-plex-sans`, `--font-jetbrains-mono`, `--font-instrument-serif`) so existing `--font-sans/mono/serif` chains keep resolving the same fallback stack. |

## Backward compat

- All existing consumers (Tailwind utility classes,
  `StockLogo.tsx`'s inline `fontFamily: 'var(--font-mono)'`,
  globals.css base-style font-family rules) continue to read
  the same CSS variable names — no rename ripple-through.
- Font-display behavior: @fontsource defaults to `font-display:
  swap` which matches the previous `display: 'swap'` config.
- Bundle size impact: +~140KB woff2 files (vs the previous
  Google Fonts CDN fetch). Negligible on the static export; the
  resilience win outweighs the size cost.

## Verification

- ✅ `next build` — 506 static pages built; no NextFontError
- ✅ `tsc --noEmit` — clean
- ✅ No external font fetch in build logs (verified via build output)
- ✅ Build offline-capable from now on

## Why @fontsource not vendored woff2 files

- @fontsource packages are the OFL-compliant official source
- Updates ship via `npm update` if the IBM Plex / JetBrains /
  Instrument projects release new revisions
- No license/attribution risk vs hand-vendoring binary files

## Not in this PR

- npm audit warnings (5 vulns, 1 critical) are in the existing
  Next.js dep tree per issue #41 (Next.js 14 → 16 bump). Not
  introduced by this PR; tracked separately.

https://claude.ai/code/session_015649aRyi2bvciQYZVNACd2

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