File size: 3,539 Bytes
18ee127
b02bd01
 
 
c54e392
18ee127
c54e392
 
 
 
 
 
 
 
 
 
 
 
18ee127
c54e392
48663c9
d9c4174
 
 
 
 
 
 
 
 
 
 
 
785660b
 
c54e392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b02bd01
c54e392
 
e44a316
c54e392
 
 
 
 
 
 
 
 
 
 
 
785660b
c54e392
 
785660b
c54e392
 
 
e44a316
c54e392
b02bd01
c54e392
 
 
 
 
 
785660b
 
 
 
c54e392
 
 
 
785660b
b02bd01
18ee127
c54e392
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
import gradio as gr
import yfinance as yf
import numpy as np
import pandas as pd
from scipy.optimize import minimize

TICKERS = [
    'AAPL', 'MSFT', 'NVDA', 'AVGO', 'ADBE',
    'AMZN', 'TSLA', 'HD',
    'PG', 'COST',
    'UNH', 'JNJ', 'LLY',
    'JPM', 'GS', 'V',
    'CAT', 'UNP', 'GE',
    'XOM', 'NEE',
    'D',
    'GOOGL', 'META', 'CMCSA',
    'PLD'
]

def optimize_portfolio(years, target_return):
    try:
        data = yf.download(TICKERS, period=f"{years}y", interval="1mo", group_by="ticker", auto_adjust=True)

        if isinstance(data.columns, pd.MultiIndex):
            try:
                prices = pd.concat([data[ticker]['Close'] for ticker in TICKERS], axis=1)
                prices.columns = TICKERS
            except Exception:
                return pd.DataFrame(), "Error: Failed to extract Close prices from multi-index data.", "", "", ""
        else:
            prices = data.get("Adj Close")
            if prices is None or prices.empty:
                return pd.DataFrame(), "Error: 'Adj Close' data not found or empty.", "", "", ""

        returns = prices.pct_change().dropna()
        mean_returns = returns.mean() * 12
        cov_matrix = returns.cov() * 12

        num_assets = len(TICKERS)
        init_weights = np.ones(num_assets) / num_assets

        def portfolio_volatility(weights):
            return np.sqrt(weights @ cov_matrix @ weights)

        constraints = [
            {"type": "eq", "fun": lambda w: np.sum(w) - 1},
            {"type": "eq", "fun": lambda w: w @ mean_returns - target_return}
        ]

        bounds = tuple((0, 1) for _ in range(num_assets))

        result = minimize(
            portfolio_volatility,
            init_weights,
            method="SLSQP",
            bounds=bounds,
            constraints=constraints
        )

        if not result.success:
            return pd.DataFrame(), "Optimization failed. Try adjusting inputs.", "", "", ""

        weights = result.x
        port_return = weights @ mean_returns
        port_vol = np.sqrt(weights @ cov_matrix @ weights)
        risk_free_rate = 0.045
        sharpe_ratio = (port_return - risk_free_rate) / port_vol

        df = pd.DataFrame({
            "Ticker": TICKERS,
            "Weight (%)": np.round(weights * 100, 2)
        }).sort_values("Weight (%)", ascending=False).reset_index(drop=True)

        return df, "", f"{port_return*100:.2f}%", f"{port_vol*100:.2f}%", f"{sharpe_ratio:.2f}"

    except Exception as e:
        return pd.DataFrame(), f"Error: {str(e)}", "", "", ""

with gr.Blocks() as demo:
    gr.Markdown("# πŸ“ˆ Modern Portfolio Optimizer (MPT)")
    gr.Markdown("Optimize a portfolio of 25 S&P 500 stocks for **minimum risk** with a target return.")

    with gr.Row():
        years_slider = gr.Slider(1, 10, value=5, step=1, label="Years of Historical Data")
        return_slider = gr.Slider(1.0, 15.0, value=5.0, step=0.1, label="Target Annual Return (%)")

    run_button = gr.Button("Optimize Portfolio")

    output_table = gr.Dataframe(headers=["Ticker", "Weight (%)"], label="Optimal Allocation")
    error_box = gr.Textbox(label="Message", lines=1)
    ret_text = gr.Textbox(label="Expected Return")
    vol_text = gr.Textbox(label="Expected Volatility")
    sharpe_text = gr.Textbox(label="Sharpe Ratio")

    run_button.click(
        fn=lambda years, target: optimize_portfolio(years, target / 100),
        inputs=[years_slider, return_slider],
        outputs=[output_table, error_box, ret_text, vol_text, sharpe_text]
    )

demo.launch()