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