summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-05-17-beta-short-interest-fallbacks-design.md
blob: 5366032def7fd2ec8888c1b9a44ac3c8c56fa34c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# Design: Beta Computed Fallback + Short Interest FMP Fallback

**Date:** 2026-05-17  
**Scope:** `backend/app/services/data_service.py` only — no schema changes, no frontend changes.

---

## Problem

Two fields in the Overview payload silently return `null` for certain tickers (e.g. NFLX):

- `stats.beta` — read exclusively from `yfinance.Ticker.info["beta"]`, which is sparse.
- `short_interest.*` — all four fields read exclusively from `info`, which returns nothing for some tickers.

The frontend already handles null gracefully (hides rows / hides card), but the data should be present whenever it can be computed or fetched.

---

## Beta: Computed Fallback from Price History

### Approach

When `info["beta"]` is `None`, compute beta from 2 years of weekly price returns against SPY.

**Formula:** `β = cov(r_ticker, r_spy) / var(r_spy)`

### Implementation

1. Add `"2y"` to `PERIODS` and `YF_PERIOD_MAP` (`"2y": "2y"`).
2. Add `compute_beta(symbol: str) -> float | None` cached in `RATIO_CACHE` (TTL=3600):
   - Calls `get_price_history(symbol, "2y")` and `get_price_history("SPY", "2y")` — reuses existing cache.
   - Converts close prices to weekly returns (resample to week-end, pct_change).
   - Aligns on date index, drops NaN rows. Requires ≥ 52 observations; returns `None` if sparse.
   - Caps result to `[-3.0, 3.0]` — extreme values are data artifacts.
3. In `_build_quote_and_stats`, change line 714:
   ```python
   # before
   beta = _safe_float(_field(source_map, field_sources, "stats.beta", ("info", "beta")))
   # after
   beta = _safe_float(_field(source_map, field_sources, "stats.beta", ("info", "beta")))
   if beta is None:
       beta = compute_beta(sym)
       if beta is not None:
           field_sources["stats.beta"] = "computed"
   ```
   `sym` is already threaded through `get_ticker_overview` → `_build_quote_and_stats`.

### Trade-offs

- No new API keys required.
- SPY history fetch is cached; cold path adds one extra `HISTORY_CACHE` lookup.
- Computed beta is a trailing market beta, not an adjusted/predicted beta. Acceptable for an overview display.

---

## Short Interest: FMP Fallback

### Approach

When all four `info`-sourced short interest fields are `None`, call FMP `/v4/short-of-float-symbol`.

### Implementation

1. Add `SHORT_CACHE = TTLCache(maxsize=256, ttl=3600)` alongside the other caches at the top of the file.
2. Add `get_fmp_short_interest(symbol: str) -> dict[str, Any]` cached in `SHORT_CACHE`:
   - Reads `FMP_API_KEY` from env; returns `{}` immediately if absent.
   - `GET https://financialmodelingprep.com/api/v4/short-of-float-symbol?symbol=SYMBOL&apikey=KEY`
   - Extracts from first row of response array: `shortPercent` → `short_percent_of_float`, `shortRatio` → `short_ratio`, `shortsVolume` → `shares_short`.
   - Returns `{}` on any exception or unexpected response shape.
3. In `get_ticker_overview`, after building `short_interest` (lines 812–823):
   ```python
   if all(v is None for v in short_interest.values()):
       fmp_short = get_fmp_short_interest(sym)
       if fmp_short:
           short_interest.update(fmp_short)
   ```
4. `shares_short_prior_month` and `shares_short_delta_pct` are not available from this FMP endpoint and stay `None`.

### Trade-offs

- Costs FMP quota (250 req/day free tier) only when yfinance returns nothing — not on every request.
- `shares_short_delta_pct` unavailable from FMP; partial data is acceptable (frontend handles it).
- Guard on "all fields None" avoids unnecessary FMP calls when yfinance returns partial data.

---

## What Does Not Change

- Response schema (`schemas.py`) — `ShortInterest` and `TickerStats` shapes unchanged.
- Frontend — no changes required.
- All existing tests — monkeypatching `data_service` functions continues to work.
- No new backend routes.

---

## Testing

Backend tests should:
- Verify `compute_beta` returns a float when given valid history for both ticker and SPY.
- Verify `compute_beta` returns `None` when history is too sparse (< 52 observations).
- Verify `compute_beta` caps extreme values at ±3.
- Verify `get_fmp_short_interest` returns mapped fields when FMP responds correctly.
- Verify `get_fmp_short_interest` returns `{}` when FMP key is absent or request fails.
- Verify `get_ticker_overview` uses computed beta when `info["beta"]` is None (monkeypatch).
- Verify `get_ticker_overview` populates short interest from FMP when all info fields are None (monkeypatch).