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:

  1. provider abstraction: provider.py
  2. yfinance implementation: yfinance_provider.py
  3. 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

http://localhost:40401/docs

Autogenerated FastAPI docs for the symbols endpoints

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

Autogenerated FastAPI docs with the macro endpoints added

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:

  1. 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.
  2. 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:

  1. 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
  2. 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”

  1. New endpoint: GET /api/symbols/QQQ/sector returns: {"symbol": "QQQ", "sector": "Technology", "industry": "Broad Market Tech ETF", "is_etf": true}

  2. Cache for 7 days because a company’s sector basically never changes.

  3. is_etf flag tells frontend whether it’s

    real sector data (from yfinance) vs. category tag (from the cheat sheet)