now this is where it gets interesting
design:
-
have small abstraction layer between app ←→ yahoo finance, etc.
-
then i can switch data sources later if necessary since i dont know which one is best for me
-
start with yfinance (yahoo finance) today
finance background:
OHLCV: 5 numbers that describe a stock’s trading day:
- Open — price when the market opened
- High — highest price during the day
- Low — lowest price during the day
- Close — price when the market closed
- Volume — how many shares were traded
FRED: Federal Reserve Economic Data
free public database run by the St. Louis Fed with thousands of economic data series
- Fed funds rate — the “risk-free rate” used to calculate Sharpe ratio (basically: “how much better is this investment than just parking your money in a risk-free asset?”)
- VIX — the market’s “fear gauge,” measures expected volatility of the S&P 500
- Treasury yield curves — useful for stress scenarios later
stuff to do:
within /backend
- uv add yfinance
- create
/backend/market
New files:
backend/
├── market/
│ ├── __init__.py # public API
│ ├── models.py # OHLCV + Quote dataclasses
│ ├── provider.py # DataProvider protocol (the abstraction)
│ ├── yfinance_provider.py # concrete yfinance implementation
│ ├── cache.py # TTL cache
│ └── fetcher.py # PriceFetcher (provider + cache combined)
├── routers/
│ ├── __init__.py
│ └── symbols.py # API endpoints
└── tests/
├── test_cache.py # 7 tests
├── test_fetcher.py # 5 tests
└── test_api_symbols.py # 4 tests
To switch providers later:
change one line in fetcher.py — swap YFinanceProvider() for TwelveDataProvider() (or whatever you build). Nothing else changes.
What I built — 3 layers:
- provider abstraction:
provider.py - yfinance implementation:
yfinance_provider.py - fetcher for rest of app:
fetcher.py
API endpoints:
- GET /api/symbols/search?q=qqq — symbol autocomplete
- GET /api/symbols/{ticker}/quote — current price snapshot
- GET /api/symbols/{ticker}/history?days=365 — daily OHLCV bars
test
run backend:
➜ falconup26 git:(data) cd backend
➜ backend git:(data)
➜ backend git:(data) uv run uvicorn main:app --reload --port 40401
autogenerated api docs

fetch data:
# Current price of QQQ
curl localhost:40401/api/symbols/QQQ/quote
{"symbol":"QQQ","price":718.72,"change":-5.64,"change_percent":-0.78,"timestamp":"2026-07-02T15:41:16.531274+00:00"
# Last 30 days of price history
curl "localhost:40401/api/symbols/QQQ/history?days=30"
[{"date":"2026-06-02","open":741.59,"high":745.62,"low":738.42,"close":745.34,"volume":30085200},{"date":"2026-06-03","open":746.49,"high":747.83,"low":740.2,"close":743.39,"volume":40270200},{"date":"2026-06-04","open":734.67,"high":742.68,"low":731.82,"close":739.8,"volume":40782600},{"date":"2026-06-05","open":729.26,"high":730.89,"low":703.55,"close":704.29,"volume":99606600},{"date":"2026-06-08","open":717.02,"high":722.24,"low":712.29,"close":715.28,"volume":47401500},{"date":"2026-06-09","open":722.19,"high":724.86,"low":685.62,"close":707.05,"volume":91932200},{"date":"2026-06-10","open":700.89,"high":710.5,"low":692.17,"close":692.93,"volume":65334300},{"date":"2026-06-11","open":698.52,"high":717.58,"low":694.24,"close":716.33,"volume":71798900},{"date":"2026-06-12","open":716.82,"high":723.22,"low":710.5,"close":720.55,"volume":51168400},{"date":"2026-06-15","open":737.29,"high":743.94,"low":736.57,"close":743.18,"volume":46710200},{"date":"2026-06-16","open":741.44,"high":743.4,"low":728.84,"close":729.06,"volume":45348700},{"date":"2026-06-17","open":734.38,"high":734.87,"low":720.06,"close":721.72,"volume":51669300},{"date":"2026-06-18","open":736.39,"high":741.01,"low":731.71,"close":739.81,"volume":50154600},{"date":"2026-06-22","open":742.02,"high":745.45,"low":734.39,"close":737.95,"volume":43518600},{"date":"2026-06-23","open":715.74,"high":723.61,"low":712.11,"close":713.65,"volume":53354200},{"date":"2026-06-24","open":715.37,"high":719.93,"low":704.45,"close":710.62,"volume":52480400},{"date":"2026-06-25","open":725.9,"high":726.83,"low":705.3,"close":716.38,"volume":49668000},{"date":"2026-06-26","open":707.13,"high":715.56,"low":702.81,"close":706.52,"volume":47081800},{"date":"2026-06-29","open":713.99,"high":724.58,"low":705.17,"close":724.08,"volume":44939100},{"date":"2026-06-30","open":724.18,"high":737.62,"low":723.91,"close":736.4,"volume":42121800},{"date":"2026-07-01","open":729.19,"high":731.92,"low":724.6,"close":725.17,"volume":40258000}]%
# Search for a symbol
curl "localhost:40401/api/symbols/search?q=apple"
[{"symbol":"AAPL","name":"Apple Inc.","type":"EQUITY","exchange":"NMS"},{"symbol":"APLE","name":"Apple Hospitality REIT, Inc.","type":"EQUITY","exchange":"NYQ"},{"symbol":"APC.F","name":"Apple Inc. R","type":"EQUITY","exchange":"FRA"},{"symbol":"AAPL19.BK","name":"AAPL19_DR AAPL#YUANTA","type":"EQUITY","exchange":"SET"},{"symbol":"AAPL.SW","name":"APPLE INC","type":"EQUITY","exchange":"EBS"}]%
next things to do: FRED integration
FRED pulls economy-level (macro) data
uv add fredapi
New files:
-
market/fred.py— FRED provider with caching- fed funds rate,
- VIX,
- 4 treasury yields (3mo, 2y, 10y, 30y)
-
routers/macro.py— API endpoints for macros.(separate router for symbol data)
4 fastapi endpoints
-
- tests
How it fits together:
.env (FRED_API_KEY)
→ fred.py (fetches from FRED API, caches 1hr)
→ macro.py router (exposes as /api/macro/* endpoints)
→ frontend will eventually display these
new autogenerated api docs

i need a FRED api key:
https://fred.stlouisfed.org/docs/api/api_key.html
https://fredaccount.stlouisfed.org/apikey
API Key Describe the application or program you intend to write:
Personal portfolio risk analysis tool that fetches macro indicators (fed funds rate, treasury yields, VIX) for investment research.
then it hands you a key → straight into .env as FRED_API_KEY (never into the repo)
to check FRED
start server
cd backend
uv run uvicorn main:app --reload --port 40401
# all macro indicators at once
curl localhost:40401/api/macro/snapshot
{"fed_funds_rate":3.63,"vix":16.59,"treasury_3mo":3.85,"treasury_2y":4.17,"treasury_10y":4.48,"treasury_30y":4.97,"as_of":"2026-07-03"}%
# just the fed funds rate
curl localhost:40401/api/macro/risk-free-rate
{"rate":3.63,"source":"FRED DFF (Daily Federal Funds Effective Rate)"}%
# just VIX
curl localhost:40401/api/macro/vix
{"vix":16.59,"source":"FRED VIXCLS (CBOE Volatility Index)"}%
# historical VIX for last 30 days
curl "localhost:40401/api/macro/history/vix?days=30"
[{"date":"2026-06-03","value":16.06},{"date":"2026-06-04","value":15.4},{"date":"2026-06-05","value":21.51},{"date":"2026-06-08","value":18.92},{"date":"2026-06-09","value":19.87},{"date":"2026-06-10","value":22.22},{"date":"2026-06-11","value":19.44},{"date":"2026-06-12","value":17.68},{"date":"2026-06-15","value":16.2},{"date":"2026-06-16","value":16.41},{"date":"2026-06-17","value":18.44},{"date":"2026-06-18","value":16.4},{"date":"2026-06-19","value":16.78},{"date":"2026-06-22","value":17.28},{"date":"2026-06-23","value":19.49},{"date":"2026-06-24","value":18.63},{"date":"2026-06-25","value":18.89},{"date":"2026-06-26","value":18.41},{"date":"2026-06-29","value":17.65},{"date":"2026-06-30","value":16.45},{"date":"2026-07-01","value":16.59}]%
If your FRED_API_KEY is set correctly, you’ll get back JSON with real numbers.
If the key is missing or wrong, you’ll get an error message telling you.
last thing to do: sector/industry data
Two approaches:
- Simple — just store sector for individual stocks, and tag ETFs with a broad category (e.g., “broad market”, “leveraged tech”, “semiconductor”). Good enough to flag “hey, 80% of your portfolio is tech-adjacent.” Fast to build.
- Detailed — pull actual ETF holdings/sector weights. yfinance has some of this via
Ticker.funds_data, but it’s spotty. More accurate concentration analysis, but more work and potentially unreliable data.
option 1 first, upgrade to option 2 later.
What was added:
- A hardcoded cheat sheet for ETFs — quick lookup table: QQQ → Technology, Broad Market Tech ETF SOXL → Technology, Leveraged Semiconductor ETF SPY → Broad Market, S&P 500 ETF GLD → Commodities, Gold ETF …17 total
- For individual stocks, ask yfinance for sector
! The logic is: check the cheat sheet first → if not there, ask yfinance → if yfinance doesn’t know either, return “Unknown”
-
New endpoint:
GET /api/symbols/QQQ/sectorreturns:{"symbol": "QQQ", "sector": "Technology", "industry": "Broad Market Tech ETF", "is_etf": true} -
Cache for 7 days because a company’s sector basically never changes.
-
is_etfflag tells frontend whether it’sreal sector data (from yfinance) vs. category tag (from the cheat sheet)