File size: 13,135 Bytes
e6b8a0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1f25c9
 
 
e6b8a0f
f1f25c9
 
cb0e852
e6b8a0f
 
f1f25c9
 
e6b8a0f
f1f25c9
 
e6b8a0f
f1f25c9
 
 
 
 
 
3036bb1
f1f25c9
 
 
 
 
 
 
 
 
 
e6b8a0f
 
 
 
 
f1f25c9
bd3f2a3
f1f25c9
e6b8a0f
f1f25c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6b8a0f
 
 
 
bd3f2a3
 
 
 
 
 
 
 
 
 
e6b8a0f
bd3f2a3
e6b8a0f
 
 
 
 
 
 
 
 
 
 
 
 
bd3f2a3
e6b8a0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3036bb1
e6b8a0f
 
 
3036bb1
e6b8a0f
 
 
3036bb1
e6b8a0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
"""
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 our modules
import ocr_parser
import portfolio_calculator
import formula_generator


# Page configuration
st.set_page_config(
    page_title="Portfolio Volatility Analyzer",
    page_icon="๐Ÿ“Š",
    layout="wide",
    initial_sidebar_state="expanded"
)


# Initialize session state
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


# Main title and description
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()


# ========================================
# Section 1: Portfolio Input
# ========================================

st.header("1๏ธโƒฃ Portfolio Input")

# Create two columns for upload and manual entry
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 = []

        # Process each uploaded file
        for idx, uploaded_file in enumerate(uploaded_files, 1):
            st.markdown(f"### Screenshot {idx}")

            # Display uploaded image
            image = Image.open(uploaded_file)
            st.image(image, caption=f"Screenshot {idx}: {uploaded_file.name}")

            # OCR processing
            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))

            # Parse portfolio
            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")

        # Show all extracted texts in an expander
        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}")

        # Merge all portfolios
        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
    """)

    # Get initial JSON value
    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:
        # Default example
        initial_json = json.dumps({
            "AAPL": 5000,
            "GOOGL": 3000,
            "MSFT": 2000
        }, indent=2)

    # Editable text area
    edited_json = st.text_area(
        "Portfolio (JSON format)",
        value=initial_json,
        height=250,
        help="Edit the portfolio in JSON format: {\"TICKER\": amount, ...}"
    )

    # Validate button
    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()


# ========================================
# Section 2: Portfolio Analysis
# ========================================

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

    # Display current portfolio
    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")

    # Fetch data and calculate metrics
    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()

    # Store metrics in session state
    st.session_state.metrics = metrics

    st.success("โœ… Analysis complete!")

    st.divider()

    # ========================================
    # Section 3: Data Display
    # ========================================

    st.header("3๏ธโƒฃ Historical Data")

    # Portfolio Weights
    st.subheader("๐Ÿ“Š Portfolio Weights")
    weights_df = [(ticker, f"{weight*100:.2f}%") for ticker, weight in metrics['weights'].items()]
    st.table(weights_df)

    # Historical Prices
    st.subheader("๐Ÿ“ˆ Historical Prices (Last 5 Days)")
    st.dataframe(metrics['prices'].tail())

    # Returns
    with st.expander("๐Ÿ“‰ Daily Log Returns (Last 5 Days)"):
        st.dataframe(metrics['returns'].tail())

    # Covariance Matrix
    st.subheader("๐Ÿ”ข Covariance Matrix (Annualized)")
    st.dataframe(metrics['cov_matrix'] * 252)

    st.divider()

    # ========================================
    # Section 4: Mathematical Formulas
    # ========================================

    st.header("4๏ธโƒฃ Mathematical Formulas")

    # Generate all 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']
    )

    # Weight Formulas
    st.subheader("โš–๏ธ Portfolio Weights")
    st.markdown("**Symbolic Formula:**")
    st.latex(formulas['weights_symbolic'])
    st.markdown("**Numerical Calculation:**")
    st.latex(formulas['weights_numerical'])

    # Covariance Matrix
    st.subheader("๐Ÿ“Š Covariance Matrix (Annualized)")
    st.latex(formulas['covariance_matrix'])

    # Correlation Matrix
    with st.expander("๐Ÿ”— Correlation Matrix"):
        st.latex(formulas['correlation_matrix'])

    # Variance Formula
    st.subheader("๐Ÿ“ Portfolio Variance")
    st.markdown("**Symbolic Formula:**")
    st.latex(formulas['variance_symbolic'])

    st.markdown("**Detailed Expansion:**")
    st.latex(formulas['variance_expanded'])

    # Toggle for full expansion
    if st.checkbox("๐Ÿ” Show all variance terms (no truncation)", value=False):
        st.markdown("**Complete Expansion (All Terms):**")
        st.latex(formulas['variance_expanded_full'])

    # Volatility Formula
    st.subheader("๐Ÿ“Š Portfolio Volatility")
    st.markdown("**Symbolic Formula:**")
    st.latex(formulas['volatility_symbolic'])
    st.markdown("**Numerical Result:**")
    st.latex(formulas['volatility_numerical'])

    st.divider()

    # ========================================
    # Section 5: Final Results
    # ========================================

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

    # ========================================
    # Section 6: Interactive Rebalancing
    # ========================================

    st.header("6๏ธโƒฃ Interactive Portfolio Rebalancing")

    st.markdown("""
    **Adjust portfolio amounts** using the sliders below to see how volatility changes in real-time.
    """)

    # Create sliders for each ticker
    new_amounts = {}
    slider_cols = st.columns(min(len(tickers), 3))  # Max 3 columns

    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,  # Allow up to 3x original
                value=original_amount,
                step=100.0,
                format="$%.0f",
                key=f"slider_{ticker}"
            )
            new_amounts[ticker] = new_amount

    # Check if amounts changed
    amounts_changed = any(new_amounts[t] != portfolio[t] for t in tickers)

    if amounts_changed:
        st.subheader("๐Ÿ”„ Recalculated Metrics")

        # Recalculate with new amounts
        with st.spinner("Recalculating..."):
            new_metrics, error = portfolio_calculator.get_portfolio_metrics(new_amounts, period="1y")

        if error:
            st.error(f"โŒ {error}")
        else:
            # Display new results
            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"  # Lower volatility is better
                )

else:
    # Show instructions if portfolio not validated
    st.info("๐Ÿ‘† Please upload a portfolio screenshot or enter portfolio data above, then click 'Validate Portfolio' to begin analysis.")

st.divider()

# ========================================
# Footer
# ========================================

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)