Skip to content

Rolling Beta & Market-Factor Exposure Reporting #352

Description

@devsimze

Problem Statement

src/analytics/metrics.ts computes a single beta "vs an exogenous benchmark series," but nothing supplies that benchmark, so beta is effectively always null. Meanwhile src/analytics/attribution.ts already defines a v1 "market" — the equal-weighted average of available ProtocolRate APY history — and proves a Brinson identity against it. This issue connects those: a rolling beta and market-factor exposure report that measures how much of a portfolio's yield movement is explained by the DeFi-yield "market factor" versus idiosyncratic protocol selection, computed on a rolling window so a user can see their exposure changing over time, not just a point estimate.

Current State

  • src/analytics/metrics.tscomputeAllMetrics accepts an optional benchmark and returns beta (currently unfed); inferPeriodsPerYear; null-on-degenerate contract.
  • src/analytics/attribution.ts — v1 benchmark = equal-weighted mean of available ProtocolRate APY per day; accepts RawProtocolRatePoint[] so a real index can be swapped in; Cariño multi-period linking; reconciliation tolerance.
  • src/analytics/estimation.tsbuildDailyRateSeries / aligned daily grid / MIN_ALIGNED_OBSERVATIONS.
  • src/analytics/riskService.ts — DB glue, 90d retention, PortfolioRiskAggregate.
  • src/jobs/attribution.ts — the DB-reading job for attribution.
  • docs/PERFORMANCE_ATTRIBUTION.md, docs/STRATEGY_MARKETPLACE.md §2.

Proposed Solution

1. Canonical benchmark series (src/analytics/benchmark.ts, new)

  • buildMarketFactorSeries(rawRates, { universe?, weighting: 'equal' | 'tvl' }) → a daily benchmark return series (not level) on the aligned grid, reusing estimation.ts alignment and the attribution.ts equal-weight definition as the default. This is the single place the "market" is defined for all of src/analytics/; attribution.ts is refactored to import it so there is one definition, not two.
  • Pluggable weighting; tvl weighting uses poolMetrics/liquidity snapshots when available, else falls back to equal with a flag.

2. Pure core (src/analytics/factorExposure.ts)

  • rollingBeta(portfolioReturns, marketReturns, windowSize, step)[{ windowEndMs, beta, alpha, rSquared, sampleCount }]; each window uses OLS of portfolio return on market return; null entries for windows with < MIN_SAMPLES or zero market variance.
  • factorDecomposition(portfolioReturns, marketReturns) over the full window → { beta, alpha (annualized), rSquared, idiosyncraticVolShare } — "78% of your yield variance tracks the DeFi-yield market; 22% is your protocol selection."
  • Deterministic, zero I/O, fixture-tested (independent series → beta ~0; portfolio = market → beta ~1, R² ~1; degenerate → null).

3. DB glue + API

  • Portfolio return series from YieldSnapshot value buckets (principal + yield), same as riskService.ts; market series from benchmark.ts over ProtocolRate.
  • GET /api/v1/analytics/factor-exposure?window=90d&rollingWindow=30d{ rolling: [...], summary: { beta, alpha, rSquared, idiosyncraticVolShare }, benchmark: { weighting, universeSize }, insufficientHistory, caveats }.
  • Fixed caveat: "The 'market' is the equal-weighted average of tracked protocol APY series, not a traded index. Beta here measures yield co-movement, not price beta."
  • Owner-scoped, rate-limited; docs/openapi.yaml + a section in docs/PERFORMANCE_ATTRIBUTION.md.

Good first issue candidate: the pure rollingBeta / OLS core is a well-scoped, well-specified unit-testable piece; the benchmark unification and wiring can follow.

Edge Cases & Failure Modes

  • Retention: 90d cap; rollingWindow must be < window; a rollingWindow that leaves < 2 windows returns the summary only with a caveat.
  • Market variance ~0 (all protocols moved together, or forward-fill dominated): window beta = null; a metric flags degenerate windows.
  • Portfolio with one protocol: beta vs. an equal-weighted market that includes that protocol is still meaningful but trends toward the protocol's own weight in the benchmark; documented, not hidden.
  • NaN safety: OLS denominator zero → null; no Infinity/NaN ever returned (contract inherited from metrics.ts).
  • Alignment: portfolio and market series must be on the same daily grid with the same days present; mismatched days are dropped, not zero-filled, and sampleCount reflects the intersection.
  • Determinism: protocols sorted; asOf explicit; input-snapshot hash returned.
  • attribution.ts refactor: importing the shared benchmark must not change existing attribution outputs — a golden test locks current attribution results before/after.

Security & Privacy Considerations

  • Reads the caller's YieldSnapshot-derived series + public ProtocolRate; no cross-user data.
  • Benchmark universe is public protocols only; no per-user or per-strategy universe leakage.
  • Owner-scoped, sub-account VIEW gated, rate-limited.

Out of Scope

  • Multi-factor models (size/momentum/quality analogues) — single market factor here.
  • A real tradeable index product.
  • Price beta (no price series; this is yield co-movement, stated plainly).
  • Using beta as an agent input.

Suggested Implementation Plan

  1. src/analytics/benchmark.tsbuildMarketFactorSeries; refactor attribution.ts to import it; golden test locking attribution outputs.
  2. src/analytics/factorExposure.tsrollingBeta (OLS) + factorDecomposition; fixture unit tests.
  3. DB glue: portfolio + market return series on a shared grid.
  4. GET /api/v1/analytics/factor-exposure + validator (rollingWindow < window) + fixed caveat.
  5. docs/PERFORMANCE_ATTRIBUTION.md section + docs/openapi.yaml; degenerate-window metric.

Acceptance Criteria

  • One canonical buildMarketFactorSeries defines "the market" for all of src/analytics/; attribution.ts imports it with a golden test proving unchanged attribution output
  • rollingBeta returns per-window { beta, alpha, rSquared, sampleCount } via OLS, null for under-sampled or zero-variance windows
  • factorDecomposition returns full-window beta/alpha/R²/idiosyncratic-vol-share
  • GET /api/v1/analytics/factor-exposure is owner-scoped, retention-bounded, rollingWindow < window enforced, with a mandatory yield-co-movement caveat
  • Series are intersected on a shared daily grid (never zero-filled); output deterministic with an input-snapshot hash
  • Pure cores are zero-I/O and fixture-tested (beta ~0 / ~1 / null cases)
  • docs/PERFORMANCE_ATTRIBUTION.md + docs/openapi.yaml updated; unit + integration tests green

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Stellar WaveIssues in the Stellar wave program

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions