Skip to content

feat(fundamentals): Growth + Stewardship pillar substrate (Phase 3a)#279

Merged
cipher813 merged 1 commit into
mainfrom
feat/growth-stewardship-fundamental-fields-phase3a
May 20, 2026
Merged

feat(fundamentals): Growth + Stewardship pillar substrate (Phase 3a)#279
cipher813 merged 1 commit into
mainfrom
feat/growth-stewardship-fundamental-fields-phase3a

Conversation

@cipher813
Copy link
Copy Markdown
Owner

Summary

Fields added

Pillar Field Source Clip
Growth revenue_growth_3y Finnhub revenueGrowth3YrevenueGrowth5Y [-0.5, 1.5]
Growth eps_growth_3y epsGrowth3YepsBasicExclExtraItemsAnnual5YepsGrowth5Y [-1.0, 2.0]
Stewardship payout_ratio payoutRatioTTMpayoutRatioAnnual [0, 2]
Stewardship dividend_yield dividendYieldIndicatedAnnualcurrentDividendYieldTTM [0, 0.20]
Stewardship capex_growth_5y capitalSpendingGrowth5Y [-1, 2]

3y CAGR > TTM YoY for cross-sectional ranking (base-effect noise + single-quarter anomalies average out). Annual / 5y fallbacks handle newer listings without a full 3y history.

Insider ownership % is NOT in this PR — Finnhub metric=all does not surface it; would require a separate /stock/insider-transactions integration. Deferred to a follow-up if/when stewardship's composite weight in the backtester optimization argues for it.

What changed

  • collectors/fundamentals.py: NEUTRAL grows 8 → 13 fields; _fetch_single_ticker extracts the 5 new Finnhub fields with TTM-preferred / annual-fallback chains; values clipped to documented ranges.
  • features/registry.py: 5 new FeatureEntry records (group=fundamental), bringing fundamental group 8 → 13.
  • features/feature_engineer.py: 5 new columns in EXPECTED_FEATURE_COLUMNS; DataFrame-write site populates from fundamental_data dict or falls back to 0.0 when None (matches existing 8-field pattern).

Tests

12 new tests in test_fundamentals_finnhub.py:

  • NEUTRAL field completeness
  • Field mapping for typical AAPL payload (both growth + stewardship blocks)
  • Fallback chains for each of the 5 fields (revenue 3y→5y, EPS 3y→annual→5y, payout TTM→annual, dividend yield indicated→TTM)
  • Clipping bounds (extreme growth → 1.5/2.0, extreme yield → 0.20, payout > 2 → 2.0)
  • Empty-payload preservation (new fields zeroed alongside legacy ones)

Suite: 1400 → 1412 passing, zero regressions, 7.47s.

Activation path

  1. This PR merges → Saturday SF DataPhase1 next firing writes fundamental.parquet with 13 columns
  2. Phase 3b PR (alpha-engine-research) extends scoring/factor_scoring.py _COMPOSITE_DEFS with growth_score + stewardship_score, referencing these 5 new columns + derived sustainable_growth_rate = roe × (1 - payout_ratio). The factors/profiles/latest.json schema grows from 4 composites to 6.

Composes with

  • factor-substrate-260513 (this PR extends its fundamental.parquet schema)
  • alpha-engine-research Phase 3b (composite extension lands in follow-up PR)
  • [[feedback_sota_institutional_default_no_shortcuts]] (2026-05-20)

Test plan

  • 12 new pillar-substrate tests pass
  • Full data suite 1412 green (zero regressions vs 1400 baseline)
  • Pre-commit hook (secret scan) passes
  • CI green
  • First post-merge Saturday SF DataPhase1 firing produces fundamental.parquet with all 5 new columns populated for ≥95% of universe tickers

🤖 Generated with Claude Code

Phase 3a of the pillar-decomposed attractiveness scoring arc (plan doc
`alpha-engine-docs/private/attractiveness-pillars-260520.md`; ROADMAP P1
= alpha-engine-config #254 merged; lib Phase 1 = alpha-engine-lib #53
v0.22.0; research+config Phase 2 = alpha-engine-research #207 +
alpha-engine-config #255 merged).

Adds 5 new fundamental fields surfaced from the existing Finnhub
`/stock/metric?metric=all` response — no new API integrations, no
extra rate-limit pressure on the existing collector budget. The fields
back the Growth + Stewardship pillar quant composites that get added
to `scoring/factor_scoring.py` in the follow-up Phase 3b PR
(alpha-engine-research):

Growth pillar quant substrate (2):
- `revenue_growth_3y` — 3y revenue CAGR (Finnhub `revenueGrowth3Y`,
  fallback `revenueGrowth5Y`); clipped to [-0.5, 1.5]
- `eps_growth_3y` — 3y EPS CAGR (Finnhub `epsGrowth3Y`, fallbacks
  `epsBasicExclExtraItemsAnnual5Y` / `epsGrowth5Y`); clipped to [-1.0, 2.0]

Stewardship pillar quant substrate (3):
- `payout_ratio` — TTM dividends / NI (Finnhub `payoutRatioTTM` →
  `payoutRatioAnnual`); clipped to [0, 2]
- `dividend_yield` — Indicated annual yield (Finnhub
  `dividendYieldIndicatedAnnual` → `currentDividendYieldTTM`); clipped
  to [0, 0.20]
- `capex_growth_5y` — 5y CAPEX growth (Finnhub
  `capitalSpendingGrowth5Y`); clipped to [-1, 2]

3y CAGR is preferred over TTM YoY for ranking because it's smoother
(base-effect / single-quarter anomalies average out); 5y/annual
fallbacks for newer listings without a full 3y history.

Insider ownership % is NOT in this PR — Finnhub `metric=all` does not
surface it; would require a separate `/stock/insider-transactions`
integration (extra API calls + rate-limit pressure). Deferred to a
follow-up if/when stewardship's composite weight in the backtester
optimization argues for it. The three signals shipped here cover the
"capital allocation discipline" axis: payout (return-of-capital
intensity), dividend yield (combined with payout, identifies
low-yield + low-payout = buyback-heavy retainers), and CAPEX growth
(reinvestment intensity).

Plumbed end-to-end:

- `collectors/fundamentals.py`: NEUTRAL grows to 13 fields (was 8);
  `_fetch_single_ticker` extracts the 5 new Finnhub fields with
  TTM-preferred / annual-fallback chains; values clipped to the ranges
  above.
- `features/registry.py`: 5 new `FeatureEntry` records (group=
  "fundamental"), bringing the fundamental group from 8 → 13.
- `features/feature_engineer.py`: 5 new columns in
  `EXPECTED_FEATURE_COLUMNS` + DataFrame-write site populates from
  `fundamental_data` dict / falls back to 0.0 when fundamental_data
  is None (matches existing pattern).

Behavior change: when the Saturday SF DataPhase1 runs after this PR
merges + deploys, `features/{date}/fundamental.parquet` carries 13
columns instead of 8. Downstream consumers (alpha-engine-research
`scoring/factor_scoring.py` + the Phase 3b composite extension)
treat the new columns as additive — the 4 existing composites
(quality / momentum / value / low_vol) and their _n counts continue
to compute identically. No behavioral coupling with Phase 3b — the
new columns can sit in the parquet for a soak before research's
Phase 3b PR consumes them.

Tests: 12 new tests in `tests/test_fundamentals_finnhub.py` covering
NEUTRAL field completeness, field mapping for typical AAPL payload,
fallback chains for each of the 5 fields, clipping bounds (extreme
growth, extreme yield, payout > 2), empty-payload preservation.

Suite: 1400 → 1412 passing (zero regressions, 7.47s).

Composes with: factor-substrate-260513 (this PR extends its
fundamental.parquet schema); alpha-engine-research Phase 3b
(consumer composites land in a follow-up PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cipher813 cipher813 merged commit a0fe226 into main May 20, 2026
1 check passed
@cipher813 cipher813 deleted the feat/growth-stewardship-fundamental-fields-phase3a branch May 20, 2026 23:07
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