Py.Cafe

mojebap195/

streamlit-on-pycafe

Streamlit on Py.cafe

DocsPricing
  • app.py
  • requirements.txt
app.py
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
import streamlit as st
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from datetime import datetime

# --- DASHBOARD CONFIG ---
st.set_page_config(page_title="Professional Cash Out Strategy Simulator", layout="wide")
st.title("πŸš€ Professional Cash Out Strategy Simulator")

# --- SESSION STATE INITIALIZATION ---
if 'manual_mean' not in st.session_state:
    st.session_state.manual_mean = 22.87
if 'manual_vol' not in st.session_state:
    st.session_state.manual_vol = 41.53
if 'manual_start_price' not in st.session_state:
    st.session_state.manual_start_price = 95000.0
if 'debug_log' not in st.session_state:
    st.session_state.debug_log = []

# --- DATA INPUT SECTION ---
st.sidebar.header("1. Data Input")

uploaded_file = st.sidebar.file_uploader("Upload Historical CSV", type="csv")

if uploaded_file:
    try:
        uploaded_file.seek(0)
        df_hist = pd.read_csv(uploaded_file)
        
        # FIX: Case-insensitive column detection
        col = None
        # Convert all columns to lowercase for a reliable check
        cols_lower = [c.lower() for c in df_hist.columns]
        
        if 'price' in cols_lower:
            # Find the original name that matched 'price'
            col = df_hist.columns[cols_lower.index('price')]
        elif 'open' in cols_lower:
            col = df_hist.columns[cols_lower.index('open')]
        elif 'close' in cols_lower:
            col = df_hist.columns[cols_lower.index('close')]
        
        if col:
            valid_series = df_hist[col].dropna()
            if not valid_series.empty:
                # Get the last valid price
                new_p0 = float(valid_series.iloc[-1])
                
                # Math for 365-day annualization
                hist_returns = np.log(valid_series / valid_series.shift(1)).dropna()
                new_mu = float(hist_returns.mean() * 365 * 100)
                new_sigma = float(hist_returns.std() * np.sqrt(365) * 100)

                # Update Session State and Trigger Refresh
                if (abs(new_p0 - st.session_state.manual_start_price) > 0.001):
                    st.session_state.manual_start_price = new_p0
                    st.session_state.manual_mean = new_mu
                    st.session_state.manual_vol = new_sigma
                    st.rerun() 

            st.sidebar.success(f"""
            βœ… **File Loaded Successfully!**
            - **Start Price:** ${st.session_state.manual_start_price:,.2f}
            - **Mean Return:** {st.session_state.manual_mean:.2f}%
            """)
        else:
            st.sidebar.error(f"Required column not found. Found: {list(df_hist.columns)}")

    except Exception as e:
        st.sidebar.error(f"Error parsing CSV: {e}")
        st.session_state.debug_log.append(f"Exception: {str(e)}")

# Optional Debug Log in sidebar
if st.sidebar.checkbox("Show Upload Debug Log"):
    for log in st.session_state.debug_log[-5:]:
        st.sidebar.text(log)

st.sidebar.header("2. Decay Settings")
enable_decay = st.sidebar.toggle("Enable Return/Volatility Decay", value=True)
decay_strength = st.sidebar.slider("Decay Strength (Ξ»)", 0.0, 1.0, 0.1)

st.sidebar.header("3. Market & Setup")
# UI UPDATE: Widgets now strictly reflect the session state
mean_return = st.sidebar.number_input("Annual Mean Return (%)", 
                                      value=st.session_state.manual_mean,
                                      key="input_mean") / 100

volatility = st.sidebar.number_input("Annual Volatility (%)", 
                                     value=st.session_state.manual_vol,
                                     key="input_vol") / 100

start_asset_price = st.sidebar.number_input("Starting Asset Price ($)", 
                                            value=st.session_state.manual_start_price,
                                            key="input_start_price")

starting_capital = st.sidebar.number_input("Initial Portfolio Value ($)", value=100000.0)
horizon_years = st.sidebar.slider("Time Horizon (Years)", 1, 100, 10)
num_simulations = st.sidebar.number_input("Number of Simulations", value=1000)

# --- SIMULATION ENGINE ---
st.sidebar.header("4. Strategy & Visualization")
cash_out_rate_annual = st.sidebar.slider("Annual Cash-Out Rate (%)", 0.0, 100.0, 5.0) / 100
ath_trigger_pct = st.sidebar.slider("Trigger (% of ATH)", 0, 100, 90) / 100
frequency = st.sidebar.selectbox("Frequency", ["Daily", "Weekly", "Monthly", "Yearly"])
show_individual = st.sidebar.checkbox("Show Individual Path Toggles", value=True)
num_paths_to_show = st.sidebar.slider("Num Paths", 1, 10, 3) if show_individual else 0

freq_steps = {"Daily": 365, "Weekly": 52, "Monthly": 12, "Yearly": 1}
steps_per_year = freq_steps[frequency]
total_steps = int(horizon_years * steps_per_year)
dt = 1 / steps_per_year
date_range = pd.date_range(start=datetime.now(), periods=total_steps + 1, freq='D' if frequency=="Daily" else frequency[0])
plot_dates = date_range.to_numpy()

@st.cache_data
def run_master_simulation(start_p, start_cap, sims, mu_init, sigma_init, rate, trigger, decay_val, t_steps, d_enabled):
    prices = np.zeros((t_steps + 1, sims))
    prices[0] = start_p
    portfolio = np.zeros((t_steps + 1, sims))
    portfolio[0] = start_cap
    cum_cash = np.zeros((t_steps + 1, sims))
    ath_tracker = np.full(sims, float(start_p))
    cash_out_events = np.zeros(sims)
    survived = np.ones(sims, dtype=bool)

    for t in range(1, t_steps + 1):
        price_multiple = prices[t-1] / start_p
        current_mu = mu_init * np.exp(-decay_val * price_multiple) if d_enabled else mu_init
        current_sigma = sigma_init * np.exp(-decay_val * price_multiple) if d_enabled else sigma_init
        shocks = np.random.normal(0, 1, sims); drift = (current_mu - 0.5 * current_sigma**2) * dt
        diffusion = current_sigma * np.sqrt(dt) * shocks
        prices[t] = prices[t-1] * np.exp(drift + diffusion); ath_tracker = np.maximum(ath_tracker, prices[t])
        at_high = (prices[t] / ath_tracker) >= trigger; cash_out_events += at_high.astype(int)
        withdrawal = np.where((at_high) & (portfolio[t-1] > 100), portfolio[t-1] * (rate * dt), 0.0)
        portfolio[t] = np.maximum(0, (portfolio[t-1] - withdrawal) * (prices[t] / prices[t-1]))
        cum_cash[t] = cum_cash[t-1] + withdrawal; survived = (portfolio[t] >= 100) & survived
    return prices, portfolio, cum_cash, survived, (cash_out_events / t_steps)

prices, portfolio, cum_cash, survived_mask, cash_out_freqs = run_master_simulation(
    start_asset_price, starting_capital, int(num_simulations), 
    mean_return, volatility, cash_out_rate_annual, ath_trigger_pct, decay_strength, total_steps, enable_decay
)

# --- STATS HELPERS & CHARTS ---
def get_pct_stats(data):
    return {"p10": np.percentile(data, 10, axis=1), "p50": np.percentile(data, 50, axis=1), 
            "p90": np.percentile(data, 90, axis=1), "mean": np.mean(data, axis=1)}

price_stats = get_pct_stats(prices); port_stats = get_pct_stats(portfolio); cash_stats = get_pct_stats(cum_cash)



def create_master_chart(stats, title, ylabel, color_rgba, raw_data=None):
    fig = go.Figure(); x_list = list(plot_dates)
    y_90, y_10, y_50 = list(stats["p90"]), list(stats["p10"]), list(stats["p50"])
    fig.add_trace(go.Scatter(x=x_list, y=y_90, mode='lines', line=dict(width=0), showlegend=False))
    fig.add_trace(go.Scatter(x=x_list, y=y_10, mode='lines', fill='tonexty', fillcolor=f'rgba{color_rgba}', line=dict(width=0), name="10th-90th Pct"))
    fig.add_trace(go.Scatter(x=x_list, y=y_50, mode='lines', line=dict(color='white', width=2), name="Median"))
    if raw_data is not None and show_individual:
        for i in range(min(num_paths_to_show, raw_data.shape[1])):
            fig.add_trace(go.Scatter(x=x_list, y=list(raw_data[:, i]), mode='lines', line=dict(dash='dot', width=1), name=f"Path {i+1}"))
    fig.update_layout(title=title, template="plotly_dark", xaxis_title="Date", yaxis_title=ylabel)
    return fig

# Grid Layout
r1c1, r1c2 = st.columns(2)
with r1c1:
    st.plotly_chart(create_master_chart(price_stats, "Asset Price Projections", "Price ($)", (0, 176, 246, 0.2), prices), use_container_width=True)
    st.plotly_chart(create_master_chart(cash_stats, "Total Extracted Cash", "USD", (0, 255, 127, 0.2), cum_cash), use_container_width=True)
with r1c2:
    st.plotly_chart(create_master_chart(port_stats, "Strategy Portfolio Balance", "USD", (138, 43, 226, 0.2), portfolio), use_container_width=True)
    hist_fig = go.Figure(data=[go.Histogram(x=list(cash_out_freqs * 100), marker_color='#00ff7f', nbinsx=20)])
    hist_fig.update_layout(title="Cash-Out Frequency Distribution (%)", xaxis_title="% Steps at Trigger", template="plotly_dark")
    st.plotly_chart(hist_fig, use_container_width=True)

# --- STRATEGIC SUMMARY (RESTORED FROM IMAGE) ---
st.header("🏁 Strategic Summary")
sc1, sc2, sc3 = st.columns(3)

with sc1:
    # Survival Rate: paths where portfolio stayed above $100
    st.metric("Survival Rate (>$100)", f"{np.mean(survived_mask):.2%}")
    # Mean of the final step of all portfolio paths
    st.metric("Mean Final Portfolio", f"${port_stats['mean'][-1]:,.2f}")

with sc2:
    # 50th percentile of total cash extracted by the end
    st.metric("Median Total Extracted", f"${cash_stats['p50'][-1]:,.2f}")
    # 50th percentile of remaining portfolio balance
    st.metric("Median Final Value", f"${port_stats['p50'][-1]:,.2f}")

with sc3:
    # Total extracted divided by years
    avg_annual_income = cash_stats['p50'][-1] / max(1, horizon_years)
    st.metric("Avg Annual Income", f"${avg_annual_income:,.2f}")
    # Median percentage of time spent in the cash-out trigger zone
    st.metric("Median Time Cashing Out", f"{np.median(cash_out_freqs):.1%}")

# --- FULL-WIDTH COMPREHENSIVE TABLE ---
st.header("πŸ“… Strategy Breakdown by Scenario")
view_mode = st.radio("Select Table Scenario", ["Median (50th)", "Worst Case (10th)", "Best Case (90th)"], horizontal=True)
map_key = {"Median (50th)": "p50", "Worst Case (10th)": "p10", "Best Case (90th)": "p90"}
k = map_key[view_mode]
step_indices = np.arange(0, total_steps + 1, steps_per_year)
step_indices = step_indices[step_indices < len(price_stats[k])]
df_breakdown = pd.DataFrame({
    "Date": [d.strftime('%Y-%m-%d') for d in date_range[step_indices]],
    "Asset Price": price_stats[k][step_indices],
    "Portfolio Remaining": port_stats[k][step_indices],
    "Total Realized Cash": cash_stats[k][step_indices]
})
st.dataframe(df_breakdown, use_container_width=True, hide_index=True,
             column_config={"Asset Price": st.column_config.NumberColumn(format="$%.2f"),
                            "Portfolio Remaining": st.column_config.NumberColumn(format="$%.2f"),
                            "Total Realized Cash": st.column_config.NumberColumn(format="$%.2f")})