Stock Portfolio Analyzer: Architecture
A Streamlit app (app.py) that only draws screens, on top of a portfolio/ package that does all the calculations. It runs locally with uv and Python 3.12.
portfolio/ package. app.py only passes data between modules and draws the results.
Yahoo Finance (orange) is the only network call. If it fails, prices.py falls back to each stock's last trade price, so the app still works offline.
performance.py also reads the dated transactions to build XIRR cash flows and the daily value series.
How data flows
- Upload. In Tab 1 you upload a CSV, load the sample or fill in the manual form.
- Validate.
loader.normalize_transactionsmaps column names (for exampletickertosymbol), checks every row and reports bad CSV lines. - Save. Valid rows go into
st.session_state.txand are written todata/transactions.csv, so they survive a restart. - Replay trades.
holdings.compute_positionsgoes through the trades in date order and works out the quantity, average cost and realized profit for each stock. - Price.
prices.current_pricesgets live closing prices from yfinance (cached for 15 minutes). For any stock it can't price, it uses the last trade price. - Measure.
performance.performance_summaryworks out the lifetime totals andxirr.value_over_timecombines daily holdings with the price history (cached for 60 minutes). - Render. Tab 2 shows the allocation pie and the Stock Price Breakdown table. Tab 3 shows the six metrics and the trend line.
Modules
| File | Responsibility | Key functions |
|---|---|---|
app.py | Sidebar and three tabs; session state; formatting | save_transactions, add_transactions, money, pct |
portfolio/loader.py | Read and check the CSV; save and load the transactions file | normalize_transactions, read_transactions_csv, load_saved, save |
portfolio/holdings.py | Replay trades into positions | compute_positions, open_positions, last_trade_prices |
portfolio/prices.py | yfinance calls, cached with st.cache_data | fetch_live_prices, fetch_price_history, current_prices, clear_cache |
portfolio/performance.py | Lifetime metrics, XIRR, daily value series | performance_summary, xirr, value_over_time |
tests/ | Offline checks against hand-worked sample numbers | test_loader.py, test_holdings.py, test_performance.py |
pyproject.toml, uv.lock | uv project: Streamlit, yfinance, pandas, plotly; pytest for development | uv run streamlit run app.py, uv run pytest |
Key rules
- Average-cost method: a sale removes cost at the average cost per share, and realized profit = (sale price − average cost) × shares sold. A sale larger than the shares held is reduced to the shares held.
- Price fallback: if there's no live price, the last trade price is used. The holdings table shows which source each price came from.
- Consistency check: Total Return = realized profit + unrealized profit (Lifetime Proceeds − Total Lifetime Investment). A test checks this.