|
|
""" |
|
|
Portfolio Volatility Analyzer - Main Streamlit Application |
|
|
|
|
|
Features: |
|
|
- OCR parsing of portfolio screenshots |
|
|
- Editable portfolio JSON |
|
|
- Financial calculations (weights, returns, covariance, variance, volatility) |
|
|
- Beautiful LaTeX formula displays for all calculations |
|
|
- Interactive sliders for portfolio rebalancing |
|
|
- Real-time recalculation |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
from PIL import Image |
|
|
import json |
|
|
|
|
|
|
|
|
import ocr_parser |
|
|
import portfolio_calculator |
|
|
import formula_generator |
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Portfolio Volatility Analyzer", |
|
|
page_icon="๐", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if 'portfolio_data' not in st.session_state: |
|
|
st.session_state.portfolio_data = None |
|
|
if 'portfolio_validated' not in st.session_state: |
|
|
st.session_state.portfolio_validated = False |
|
|
if 'metrics' not in st.session_state: |
|
|
st.session_state.metrics = None |
|
|
if 'show_all_terms' not in st.session_state: |
|
|
st.session_state.show_all_terms = False |
|
|
|
|
|
|
|
|
|
|
|
st.title("๐ Portfolio Volatility Analyzer with OCR") |
|
|
st.markdown(""" |
|
|
Analyze your investment portfolio risk using **modern portfolio theory**. |
|
|
|
|
|
**Features:** |
|
|
- ๐ธ Upload portfolio screenshot for automatic OCR parsing |
|
|
- โ๏ธ Edit portfolio data as JSON |
|
|
- ๐ Fetch historical price data automatically |
|
|
- ๐งฎ Calculate portfolio volatility with detailed mathematical formulas |
|
|
- ๐๏ธ Interactive sliders for real-time portfolio rebalancing |
|
|
""") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("1๏ธโฃ Portfolio Input") |
|
|
|
|
|
|
|
|
col1, col2 = st.columns([1, 1]) |
|
|
|
|
|
with col1: |
|
|
st.subheader("๐ธ Upload Screenshots") |
|
|
uploaded_files = st.file_uploader( |
|
|
"Upload one or more portfolio screenshots (PNG, JPG, JPEG)", |
|
|
type=["png", "jpg", "jpeg"], |
|
|
help="Upload screenshots of your portfolio. Multiple screenshots will be combined automatically.", |
|
|
accept_multiple_files=True, |
|
|
key="portfolio_uploader" |
|
|
) |
|
|
|
|
|
if uploaded_files: |
|
|
st.info(f"๐ค Processing {len(uploaded_files)} screenshot(s)...") |
|
|
|
|
|
all_portfolios = [] |
|
|
all_texts = [] |
|
|
|
|
|
|
|
|
for idx, uploaded_file in enumerate(uploaded_files, 1): |
|
|
st.markdown(f"### Screenshot {idx}") |
|
|
|
|
|
|
|
|
image = Image.open(uploaded_file) |
|
|
st.image(image, caption=f"Screenshot {idx}: {uploaded_file.name}") |
|
|
|
|
|
|
|
|
with st.spinner(f"Extracting text from screenshot {idx}..."): |
|
|
text, error = ocr_parser.extract_text_from_image(image) |
|
|
|
|
|
if error: |
|
|
st.error(f"โ Screenshot {idx}: {error}") |
|
|
continue |
|
|
|
|
|
all_texts.append((idx, text)) |
|
|
|
|
|
|
|
|
portfolio = ocr_parser.parse_portfolio(text) |
|
|
|
|
|
if portfolio: |
|
|
st.success(f"โ
Screenshot {idx}: Found {len(portfolio)} tickers: {', '.join(portfolio.keys())}") |
|
|
st.json(portfolio) |
|
|
all_portfolios.append(portfolio) |
|
|
else: |
|
|
st.warning(f"โ ๏ธ Screenshot {idx}: No valid tickers found") |
|
|
|
|
|
|
|
|
if all_texts: |
|
|
with st.expander("๐ View All Extracted Text"): |
|
|
for idx, text in all_texts: |
|
|
st.markdown(f"**Screenshot {idx}:**") |
|
|
st.text_area(f"OCR Output {idx}", text, height=100, disabled=True, key=f"ocr_text_{idx}") |
|
|
|
|
|
|
|
|
if all_portfolios: |
|
|
merged_portfolio = ocr_parser.merge_portfolios(all_portfolios) |
|
|
st.success(f"โ
**Combined Portfolio:** {len(merged_portfolio)} unique tickers") |
|
|
st.json(merged_portfolio) |
|
|
st.session_state.portfolio_data = merged_portfolio |
|
|
else: |
|
|
st.warning("โ ๏ธ **No valid tickers found in any screenshot.**") |
|
|
st.info(""" |
|
|
**Possible reasons:** |
|
|
- Tickers are not in uppercase (e.g., 'aapl' instead of 'AAPL') |
|
|
- Company names instead of ticker symbols (e.g., 'Apple Inc.' instead of 'AAPL') |
|
|
- Unusual formatting or layout |
|
|
- Poor image quality |
|
|
|
|
|
**Solution:** Please manually enter your portfolio in the JSON editor below. |
|
|
""") |
|
|
st.session_state.portfolio_data = {} |
|
|
|
|
|
with col2: |
|
|
st.subheader("โ๏ธ Edit Portfolio (JSON)") |
|
|
|
|
|
st.info(""" |
|
|
**Format:** `{"TICKER": amount, ...}` |
|
|
|
|
|
**Important:** |
|
|
- Use **ticker symbols** (e.g., AAPL, GOOGL, MSFT) |
|
|
- NOT company names (e.g., โ "Apple Inc.") |
|
|
- Tickers must be UPPERCASE |
|
|
- Amounts in your portfolio currency |
|
|
""") |
|
|
|
|
|
|
|
|
if st.session_state.portfolio_data is not None and len(st.session_state.portfolio_data) > 0: |
|
|
initial_json = ocr_parser.format_portfolio_json(st.session_state.portfolio_data) |
|
|
else: |
|
|
|
|
|
initial_json = json.dumps({ |
|
|
"AAPL": 5000, |
|
|
"GOOGL": 3000, |
|
|
"MSFT": 2000 |
|
|
}, indent=2) |
|
|
|
|
|
|
|
|
edited_json = st.text_area( |
|
|
"Portfolio (JSON format)", |
|
|
value=initial_json, |
|
|
height=250, |
|
|
help="Edit the portfolio in JSON format: {\"TICKER\": amount, ...}" |
|
|
) |
|
|
|
|
|
|
|
|
if st.button("โ
Validate Portfolio", type="primary"): |
|
|
is_valid, portfolio, error = ocr_parser.validate_portfolio_json(edited_json) |
|
|
|
|
|
if is_valid: |
|
|
st.session_state.portfolio_data = portfolio |
|
|
st.session_state.portfolio_validated = True |
|
|
st.success(f"โ
Portfolio validated! {len(portfolio)} tickers ready for analysis.") |
|
|
else: |
|
|
st.error(f"โ {error}") |
|
|
st.session_state.portfolio_validated = False |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if st.session_state.portfolio_validated and st.session_state.portfolio_data: |
|
|
|
|
|
st.header("2๏ธโฃ Portfolio Analysis") |
|
|
|
|
|
portfolio = st.session_state.portfolio_data |
|
|
tickers = list(portfolio.keys()) |
|
|
|
|
|
|
|
|
st.subheader("Current Portfolio") |
|
|
col1, col2, col3 = st.columns(3) |
|
|
with col1: |
|
|
st.metric("Tickers", len(tickers)) |
|
|
with col2: |
|
|
total_value = sum(portfolio.values()) |
|
|
st.metric("Total Value", f"${total_value:,.2f}") |
|
|
with col3: |
|
|
st.metric("Data Period", "1 year") |
|
|
|
|
|
|
|
|
with st.spinner("๐ Fetching historical data and calculating metrics..."): |
|
|
metrics, error = portfolio_calculator.get_portfolio_metrics(portfolio, period="1y") |
|
|
|
|
|
if error: |
|
|
st.error(f"โ {error}") |
|
|
st.stop() |
|
|
|
|
|
|
|
|
st.session_state.metrics = metrics |
|
|
|
|
|
st.success("โ
Analysis complete!") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("3๏ธโฃ Historical Data") |
|
|
|
|
|
|
|
|
st.subheader("๐ Portfolio Weights") |
|
|
weights_df = [(ticker, f"{weight*100:.2f}%") for ticker, weight in metrics['weights'].items()] |
|
|
st.table(weights_df) |
|
|
|
|
|
|
|
|
st.subheader("๐ Historical Prices (Last 5 Days)") |
|
|
st.dataframe(metrics['prices'].tail()) |
|
|
|
|
|
|
|
|
with st.expander("๐ Daily Log Returns (Last 5 Days)"): |
|
|
st.dataframe(metrics['returns'].tail()) |
|
|
|
|
|
|
|
|
st.subheader("๐ข Covariance Matrix (Annualized)") |
|
|
st.dataframe(metrics['cov_matrix'] * 252) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("4๏ธโฃ Mathematical Formulas") |
|
|
|
|
|
|
|
|
formulas = formula_generator.generate_all_formulas( |
|
|
amounts=portfolio, |
|
|
weights=metrics['weights'], |
|
|
cov_matrix=metrics['cov_matrix'], |
|
|
variance=metrics['variance'], |
|
|
volatility=metrics['volatility'], |
|
|
variance_breakdown=metrics['variance_breakdown'] |
|
|
) |
|
|
|
|
|
|
|
|
st.subheader("โ๏ธ Portfolio Weights") |
|
|
st.markdown("**Symbolic Formula:**") |
|
|
st.latex(formulas['weights_symbolic']) |
|
|
st.markdown("**Numerical Calculation:**") |
|
|
st.latex(formulas['weights_numerical']) |
|
|
|
|
|
|
|
|
st.subheader("๐ Covariance Matrix (Annualized)") |
|
|
st.latex(formulas['covariance_matrix']) |
|
|
|
|
|
|
|
|
with st.expander("๐ Correlation Matrix"): |
|
|
st.latex(formulas['correlation_matrix']) |
|
|
|
|
|
|
|
|
st.subheader("๐ Portfolio Variance") |
|
|
st.markdown("**Symbolic Formula:**") |
|
|
st.latex(formulas['variance_symbolic']) |
|
|
|
|
|
st.markdown("**Detailed Expansion:**") |
|
|
st.latex(formulas['variance_expanded']) |
|
|
|
|
|
|
|
|
if st.checkbox("๐ Show all variance terms (no truncation)", value=False): |
|
|
st.markdown("**Complete Expansion (All Terms):**") |
|
|
st.latex(formulas['variance_expanded_full']) |
|
|
|
|
|
|
|
|
st.subheader("๐ Portfolio Volatility") |
|
|
st.markdown("**Symbolic Formula:**") |
|
|
st.latex(formulas['volatility_symbolic']) |
|
|
st.markdown("**Numerical Result:**") |
|
|
st.latex(formulas['volatility_numerical']) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("5๏ธโฃ Final Results") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
st.metric( |
|
|
label="Portfolio Variance", |
|
|
value=f"{metrics['variance']:.6f}", |
|
|
help="Annualized portfolio variance" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.metric( |
|
|
label="Portfolio Volatility", |
|
|
value=f"{metrics['volatility']:.4f}", |
|
|
help="Annualized portfolio standard deviation (ฯ)" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
st.metric( |
|
|
label="Volatility (%)", |
|
|
value=f"{metrics['volatility']*100:.2f}%", |
|
|
help="Annualized volatility as percentage" |
|
|
) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("6๏ธโฃ Interactive Portfolio Rebalancing") |
|
|
|
|
|
st.markdown(""" |
|
|
**Adjust portfolio amounts** using the sliders below to see how volatility changes in real-time. |
|
|
""") |
|
|
|
|
|
|
|
|
new_amounts = {} |
|
|
slider_cols = st.columns(min(len(tickers), 3)) |
|
|
|
|
|
for idx, ticker in enumerate(tickers): |
|
|
col_idx = idx % len(slider_cols) |
|
|
with slider_cols[col_idx]: |
|
|
original_amount = portfolio[ticker] |
|
|
new_amount = st.slider( |
|
|
f"{ticker}", |
|
|
min_value=0.0, |
|
|
max_value=original_amount * 3, |
|
|
value=original_amount, |
|
|
step=100.0, |
|
|
format="$%.0f", |
|
|
key=f"slider_{ticker}" |
|
|
) |
|
|
new_amounts[ticker] = new_amount |
|
|
|
|
|
|
|
|
amounts_changed = any(new_amounts[t] != portfolio[t] for t in tickers) |
|
|
|
|
|
if amounts_changed: |
|
|
st.subheader("๐ Recalculated Metrics") |
|
|
|
|
|
|
|
|
with st.spinner("Recalculating..."): |
|
|
new_metrics, error = portfolio_calculator.get_portfolio_metrics(new_amounts, period="1y") |
|
|
|
|
|
if error: |
|
|
st.error(f"โ {error}") |
|
|
else: |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.markdown("**New Portfolio Weights:**") |
|
|
for ticker, weight in new_metrics['weights'].items(): |
|
|
st.write(f"{ticker}: {weight*100:.2f}%") |
|
|
|
|
|
with col2: |
|
|
st.markdown("**New Volatility:**") |
|
|
st.metric( |
|
|
label="Updated Volatility", |
|
|
value=f"{new_metrics['volatility']*100:.2f}%", |
|
|
delta=f"{(new_metrics['volatility'] - metrics['volatility'])*100:.2f}%", |
|
|
delta_color="inverse" |
|
|
) |
|
|
|
|
|
else: |
|
|
|
|
|
st.info("๐ Please upload a portfolio screenshot or enter portfolio data above, then click 'Validate Portfolio' to begin analysis.") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown(""" |
|
|
<div style='text-align: center; color: gray;'> |
|
|
<p>Built with โค๏ธ using Streamlit | Powered by Modern Portfolio Theory</p> |
|
|
<p><small>Data source: Yahoo Finance (yfinance) | OCR: Tesseract</small></p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|