feat(fundamentals): Growth + Stewardship pillar substrate (Phase 3a)#279
Merged
cipher813 merged 1 commit intoMay 20, 2026
Merged
Conversation
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>
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
alpha-engine-docs/private/attractiveness-pillars-260520.md; ROADMAP P1 = alpha-engine-config feat(data): per-collector value-range validation (ROADMAP L1243 / extends #215) #254 merged; Phase 1 = alpha-engine-lib feat: short_interest collector + postflight contract (soft-launch ready) #53 / v0.22.0; Phase 2 = alpha-engine-research fix(constituents): find Wikipedia table by columns, not by position #207 + alpha-engine-config fix(data): institutional EDGAR cache → /tmp (Lambda read-only $HOME) + scrub API keys in alt-data logs #255 merged).features/{date}/fundamental.parquet, all surfaced from the existing Finnhub/stock/metric?metric=allresponse — no new API integrations, no extra rate-limit pressure. The fields back the Growth + Stewardship pillar quant composites that land in the Phase 3b PR (alpha-engine-researchscoring/factor_scoring.pyextension).Fields added
revenue_growth_3yrevenueGrowth3Y→revenueGrowth5Yeps_growth_3yepsGrowth3Y→epsBasicExclExtraItemsAnnual5Y→epsGrowth5Ypayout_ratiopayoutRatioTTM→payoutRatioAnnualdividend_yielddividendYieldIndicatedAnnual→currentDividendYieldTTMcapex_growth_5ycapitalSpendingGrowth5Y3y 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=alldoes not surface it; would require a separate/stock/insider-transactionsintegration. 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_tickerextracts the 5 new Finnhub fields with TTM-preferred / annual-fallback chains; values clipped to documented ranges.features/registry.py: 5 newFeatureEntryrecords (group=fundamental), bringing fundamental group 8 → 13.features/feature_engineer.py: 5 new columns inEXPECTED_FEATURE_COLUMNS; DataFrame-write site populates fromfundamental_datadict or falls back to 0.0 when None (matches existing 8-field pattern).Tests
12 new tests in
test_fundamentals_finnhub.py:Suite: 1400 → 1412 passing, zero regressions, 7.47s.
Activation path
fundamental.parquetwith 13 columnsscoring/factor_scoring.py_COMPOSITE_DEFSwithgrowth_score+stewardship_score, referencing these 5 new columns + derivedsustainable_growth_rate = roe × (1 - payout_ratio). Thefactors/profiles/latest.jsonschema grows from 4 composites to 6.Composes with
factor-substrate-260513(this PR extends its fundamental.parquet schema)Test plan
fundamental.parquetwith all 5 new columns populated for ≥95% of universe tickers🤖 Generated with Claude Code