Files
fxhnt/docs/superpowers/specs/2026-06-16-equity-factor-sleeve-design.md
jgrusewski 5f292deb45 docs(b2): spec — equity-factor sleeve (value+quality+momentum, 3 construction variants)
Cross-sectional multi-factor book on broad liquid US universe (Tiingo fundamentals + 30yr EOD); long-only /
long-short / long-tilt as 3 ForwardStrategy paper-tracks through the gate. Deterministic domain+fake-port
tests. Backtest/DSR layer = separate increment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:32:00 +02:00

7.7 KiB
Raw Blame History

Equity-Factor Sleeve — Design (B2)

Status: design (approved in substance 2026-06-16) Predecessor: B1 (6 paper-track strategies + Tiingo equity data). Reuses the ForwardStrategy/ForwardTracker port, the cockpit STRATEGY_REGISTRY + cockpit_forward ingest, and TiingoDailyClient.

Goal

A systematic cross-sectional multi-factor equity book on a broad liquid US universe, run as three construction variants — long-only, long-short market-neutral, and long-tilt — each a separate ForwardStrategy paper-forward track through the existing gate. The point is platform-native: build all three, let the forward-gate + DSR decide which construction holds up out-of-sample. Diversifies the (crypto-heavy) book into a new asset class using data we already pay for (Tiingo fundamentals + 30yr EOD).

What this is NOT (scope boundary)

  • NOT a backtest — this is the forward-track sleeve. A 30yr backtest/DSR pre-screen layer over the same factor logic is a separate increment (strongly recommended to pair, so candidates are screened before months of blind forward). Out of scope here.
  • NOT live execution — paper-forward only (no broker orders).
  • NOT analyst/estimate data — Benzinga (earnings estimates/ratings) is not entitled; we use Tiingo actuals + price.

Data (verified live on our Tiingo Power key)

  • EOD (/tiingo/daily/{t}/prices) — adjusted OHLCV, 30yr; via existing TiingoDailyClient.
  • Fundamentals daily (/tiingo/fundamentals/{t}/daily) — peRatio, pbRatio, marketCap, enterpriseVal, trailingPEG1Y.
  • Fundamentals statements (/tiingo/fundamentals/{t}/statements) — piotroskiFScore, roe, roa, debtEquity, grossMargin, profitMargin, revenueQoQ, … (85 fields via /definitions).
  • Supported tickers (Tiingo supported_tickers.zip) — the candidate list for the universe screener (assetType=Stock, US exchanges, active).

Architecture (hexagonal, fits the platform)

domain/strategies/equity_factor.py   pure: z-scores, composite, quantile/rank weights, book return (no I/O)
ports/market_data.py (extend)        FundamentalsClient + UniverseSource Protocols
adapters/data/tiingo_fundamentals.py TiingoFundamentalsClient (daily metrics + statements)
adapters/data/tiingo_universe.py     TiingoUniverseSource (supported-tickers → filter → dollar-volume rank → top-N)
application/equity_factor_strategy.py 3 ForwardStrategy services (long / ls / tilt) sharing the universe+factor compute
registry.py (extend)                 3 entries: eqfactor_long, eqfactor_ls, eqfactor_tilt
adapters/orchestration/assets.py     3 assets → cockpit_forward deps

Universe screener (TiingoUniverseSource)

top_n(n=300) -> list[str]: load Tiingo supported-tickers (cache the zip), filter to US common stock + active, rank by trailing ~30d dollar-volume (close×volume from EOD over the candidate set), return the top-N. Refreshed at each monthly rebalance. (Default N=300; configurable 100500.) To bound API cost, the volume rank uses a coarse pre-filter (e.g. the exchange-listed common-stock subset) before pulling EOD.

Factor computation (domain/strategies/equity_factor.py, pure)

Per rebalance, for the universe cross-section:

  • Value = z(peRatio) + z(pbRatio) (cheap = high score; guard non-positive/None).
  • Quality = z(piotroskiFScore) + z(roe) + z(debtEquity) + z(grossMargin).
  • Momentum = z(12-1 month price return) (skip most recent month).
  • Each raw factor is winsorized (e.g. ±3σ) then cross-sectional z-scored; missing inputs → neutral 0 for that factor (a name needs ≥2 of 3 families present to be scored).
  • Composite = equal-weight mean of the three family z-scores. Pure functions: zscore(values), winsorize(values, k), composite(value_z, quality_z, momentum_z), quantile_weights(scores, construction).

Three construction variants (the construction param)

  • eqfactor_long — top-quintile, equal-weight long, weights sum to 1.0.
  • eqfactor_ls — long top-quintile / short bottom-quintile, equal-weight each leg → market-neutral (Σw=0, gross=2.0). Realized return nets a borrow cost on the short leg (default 1%/yr, applied per-day).
  • eqfactor_tilt — long-only budget, rank-weighted overweight/underweight (weights ∝ max(0, rank-centered score), normalized to sum 1.0). No shorts.

Live-booking ForwardStrategy services

Three services (one per construction) share a common _factor_targets(as_of) that calls the universe + fundamentals + price clients and returns target weights via the domain. Each advance(last_date, extra) (the live-booking pattern, like funding/poc):

  • reads prior positions (+ last_rebal) from extra,
  • marks the held book to today's prices → daily book return (minus borrow cost on shorts for ls),
  • rebalances monthly (today.month != last_rebal.month or first run): recompute targets, update positions,
  • returns ([(today_iso, book_return)], updated_extra); the ForwardTracker books forward-only (inception freeze on first run), emits the days-list state the cockpit ingests.

Registry + orchestration

Three STRATEGY_REGISTRY entries (state_file basenames eqfactor_long_state / eqfactor_ls_state / eqfactor_tilt_state; gate_spec e.g. min_days=60, min_total_return=0, min_sharpe=0.3). Three Dagster @assets (no from __future__ import annotations) mirroring the B1 paper assets, added to cockpit_forward deps + Definitions/job. They fetch live HTTP (no DuckDB) → run in parallel.

Testing (deterministic; this is the "test correctly" answer)

  • Domain (no network): fixed factor-input cross-section → assert exact z-scores (winsorized), composite, quantile membership, and each construction's weights (long Σ=1; ls Σ=0 & gross=2; tilt Σ=1, monotone in score); book-return = Σ(weight×asset-return); borrow-cost applied to ls shorts.
  • Universe screener: fake supported-tickers + volumes → assert filter + top-N by dollar-volume.
  • Services: fake FundamentalsClient/DailyBarClient/UniverseSource (deterministic fixtures, no network) → drive each construction through ForwardTracker → round-trip via ForwardStateReader (days-list summary); assert monthly-rebalance triggers + extra positions; ls books a borrow cost.
  • No lookahead: forward-only (we only use data known as-of now) — inherent; documented.
  • strict mypy, full suite green, two-stage review per task (spec then code-quality), faithful to B1's discipline.

Discipline / caveats

  • Factor premia are partly crowded/decayed — no edge is claimed; the forward-gate + DSR decide. Realistic small weighting expected.
  • Long-short realism: borrow cost modeled (1%/yr default); assumes shortability of liquid large-caps (reasonable for top-N).
  • Point-in-time: forward-only, so a reporting-lag lookahead cannot occur live; the future backtest layer MUST handle fundamentals reporting lag (flagged for that increment).
  • Data cost: Tiingo Power limits (100k req/day, 108k symbols/mo) comfortably cover a 300-name monthly rebalance.

Defaults (adjust if desired)

universe top-300 by dollar-volume · monthly rebalance · short borrow 1%/yr · winsor ±3σ · quintile cut-offs.

Build order (subagent-driven, next via writing-plans)

  1. Ports: FundamentalsClient + UniverseSource.
  2. TiingoFundamentalsClient (daily + statements) + fake.
  3. TiingoUniverseSource (supported-tickers + dollar-volume rank) + fake.
  4. Domain equity_factor.py (z-score/composite/winsor/quantile + 3 constructions + book return) — heavy unit tests.
  5. Three ForwardStrategy services (long/ls/tilt) sharing factor compute.
  6. Registry (3 entries) + 3 Dagster assets + cockpit_forward deps + Definitions.
  7. Full gate + deploy + verify all 3 in cockpit.