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).
|