CoolFace
Apppublic

SohaVaidya/forex_Analyzer

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py658 linesDownload Raw Back to root
1import os2import gradio as gr3import requests4import pandas as pd5import matplotlib.pyplot as plt6from matplotlib.backends.backend_pdf import PdfPages7from ta.trend import EMAIndicator, ADXIndicator8from ta.momentum import RSIIndicator, StochRSIIndicator9from ta.volatility import AverageTrueRange, BollingerBands10 11# === Supabase Auth (NEW) ===12from supabase import create_client, Client13import datetime14import time  # for polite delays during batched fetches15 16# -- Configure Supabase from secrets17SUPABASE_URL = os.getenv("SUPABASE_URL")18SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")19 20def _validate_supabase_url(url: str):21    if not url:22        raise ValueError("SUPABASE_URL is empty.")23    if "supabase.com/dashboard" in url or "/settings/" in url:24        raise ValueError("SUPABASE_URL points to dashboard, use https://<ref>.supabase.co")25    if not url.endswith(".supabase.co"):26        raise ValueError(f"SUPABASE_URL looks unusual: {url}")27 28_validate_supabase_url(SUPABASE_URL)29 30def get_supabase() -> Client:31    if not SUPABASE_URL or not SUPABASE_ANON_KEY:32        raise ValueError("❌ Supabase credentials missing. Set SUPABASE_URL and SUPABASE_ANON_KEY as secrets.")33    return create_client(SUPABASE_URL, SUPABASE_ANON_KEY)34 35sb = get_supabase()36 37def safe_upsert_profile(payload: dict):38    """Upsert profile but don't crash if schema cache lags."""39    try:40        sb.table("profiles").upsert(payload, on_conflict="id").execute()41    except Exception as e:42        if "PGRST205" not in str(e):43            print("Profile upsert error:", e)44 45def upsert_profile(auth_response):46    """Create/Update user profile row and last_login timestamp."""47    user = getattr(auth_response, "user", None)48    if not user:49        return50    email = user.email51    uid = user.id52    now = datetime.datetime.utcnow().isoformat()53    safe_upsert_profile({"id": uid, "email": email, "last_login": now})54 55# === Trading app constants (yours) ===56TEMP_PDF_PATH = "investment_growth_summary.pdf"57TEMP_TRADE_CSV = "trade_log.csv"58 59# Realistic Cost Parameters60slippage = 0.001  # 0.1%61spread = 0.001    # 0.1%62commission = 0.001  # 0.1% per trade63 64# Estimated candles per day for hints/estimates (not used to force API now)65CANDLES_PER_DAY = {66    "1min": 24*60,67    "5min": 24*12,68    "15min": 24*4,69    "30min": 24*2,70    "45min": int(24*1.3333),71    "1h": 24,72    "2h": 12,73    "4h": 6,74    "8h": 3,75    "1day": 1,76    "1week": 1/7,77    "1month": 1/3078}79 80# Map timeframe to a pandas offset so we can step back exactly one bar per batch loop81INTERVAL_TO_PANDAS_OFFSET = {82    "1min": "1min", "5min": "5min", "15min": "15min", "30min": "30min", "45min": "45min",83    "1h": "1h", "2h": "2h", "4h": "4h", "8h": "8h",84    "1day": "1D", "1week": "7D", "1month": "30D"  # rough for week/month, good enough for paging85}86 87# === Data fetch ===88def get_api_key():89    api_key = os.getenv("TWELVE_DATA_API_KEY")90    if not api_key:91        raise ValueError("❌ API Key not found in environment.")92    return api_key93 94def _step_back(ts: pd.Timestamp, interval: str):95    """Return a timestamp just before ts by one bar of the given interval."""96    offset = INTERVAL_TO_PANDAS_OFFSET.get(interval, "1D")97    return (ts - pd.tseries.frequencies.to_offset(offset)).to_pydatetime()98 99def fetch_data(100    symbol,101    interval,102    outputsize=3000,       # used only for the "no dates" single-call path103    only_2025=True,104    start_date=None,105    end_date=None,106    max_batches=10,        # increase if you need to go further back107    sleep_secs=0.2         # be gentle to the API; tweak if rate-limited108):109    """110    Batched fetch that honors start_date/end_date windows beyond the 5000-bar cap.111    - If no dates: single call for the latest window (like before).112    - If dates: iterate in 5000-candle chunks going back via end_date paging.113    """114    api_key = get_api_key()115 116    # === Single-call path (latest window), like before ===117    if not start_date and not end_date:118        # respect TwelveData's cap119        osize = min(max(1, int(outputsize or 3000)), 5000)120        url = "https://api.twelvedata.com/time_series"121        params = {122            "symbol": symbol,123            "interval": interval,124            "outputsize": osize,125            "apikey": api_key126        }127        resp = requests.get(url, params=params)128        data = resp.json()129        if "values" not in data:130            raise ValueError(f"❌ API error: {data.get('message', 'Unknown error')}")131        df = pd.DataFrame(data["values"])132        if df.empty:133            return df134        df["datetime"] = pd.to_datetime(df["datetime"])135        df = df.sort_values("datetime").set_index("datetime")136        for col in ['open', 'high', 'low', 'close']:137            df[col] = df[col].astype(float)138        # Apply 'only_2025' only if user didn't specify custom dates139        if only_2025:140            df = df[df.index.year == 2025]141        return df.dropna()142 143    # === Batched path (dates provided) ===144    target_start = pd.to_datetime(start_date) if start_date else pd.Timestamp("1970-01-01", tz=None)145    target_end = pd.to_datetime(end_date) if end_date else pd.Timestamp.utcnow()146    if target_start > target_end:147        raise ValueError("Start Date must be before End Date.")148 149    frames = []150    current_end = target_end.to_pydatetime()151    batches = 0152    url = "https://api.twelvedata.com/time_series"153 154    while batches < max_batches:155        params = {156            "symbol": symbol,157            "interval": interval,158            "outputsize": 5000,  # max allowed159            "apikey": api_key,160            "end_date": current_end.strftime("%Y-%m-%d %H:%M:%S"),161        }162        resp = requests.get(url, params=params)163        data = resp.json()164 165        if "values" not in data:166            # bail on API error167            raise ValueError(f"❌ API error: {data.get('message', 'Unknown error')}")168 169        batch = pd.DataFrame(data["values"])170        if batch.empty:171            break172 173        batch["datetime"] = pd.to_datetime(batch["datetime"])174        batch = batch.sort_values("datetime").set_index("datetime")175        for col in ['open', 'high', 'low', 'close']:176            batch[col] = batch[col].astype(float)177 178        # Keep only up to current_end (paranoia)179        batch = batch[batch.index <= pd.to_datetime(current_end)]180        if batch.empty:181            break182 183        frames.append(batch)184 185        earliest = batch.index.min()186        # stop if we've reached or crossed the target_start187        if earliest <= target_start:188            break189 190        # step back just before the earliest bar we received191        current_end = _step_back(earliest, interval)192        batches += 1193        if sleep_secs:194            time.sleep(sleep_secs)195 196    if not frames:197        return pd.DataFrame()198 199    df = pd.concat(frames, axis=0).sort_values("datetime")200    # de-duplicate in case of one-bar overlaps between batches201    df = df[~df.index.duplicated(keep="first")]202 203    # final filter to requested window204    df = df[(df.index >= target_start) & (df.index <= target_end)]205 206    # Do NOT apply 'only_2025' here (you provided dates). If you really want to, uncomment:207    # if only_2025:208    #     df = df[df.index.year == 2025]209 210    return df.dropna()211 212# === Strategy / PnL logic (yours) ===213def get_rolling_profits(df, lot_size, capital,214                        use_ema, ema1, ema2,215                        use_rsi, rsi, rsi_thresh,216                        use_adx, adx, adx_thresh,217                        use_atr, atr_window,218                        use_stoch, stoch, stoch_thresh,219                        use_bb, bb,220                        signal_trigger_thresh,221                        sl_percent, tp_percent):222    if use_ema:223        df['ema_short'] = EMAIndicator(df['close'], window=ema1).ema_indicator()224        df['ema_long'] = EMAIndicator(df['close'], window=ema2).ema_indicator()225    if use_rsi:226        df['rsi'] = RSIIndicator(df['close'], window=rsi).rsi()227    if use_adx:228        df['adx'] = ADXIndicator(df['high'], df['low'], df['close'], window=adx).adx()229    if use_atr:230        df['atr'] = AverageTrueRange(df['high'], df['low'], df['close'], window=atr_window).average_true_range()231    if use_stoch:232        df['stoch'] = StochRSIIndicator(df['close'], window=stoch).stochrsi()233    if use_bb:234        bb_indicator = BollingerBands(df['close'], window=bb, window_dev=2)235        df['bb_upper'] = bb_indicator.bollinger_hband()236        df['bb_lower'] = bb_indicator.bollinger_lband()237 238    df['position'] = 0239    if use_ema:240        df.loc[df['ema_short'] > df['ema_long'], 'position'] += 1241    if use_rsi:242        df.loc[df['rsi'] < rsi_thresh, 'position'] += 1243    if use_adx:244        df.loc[df['adx'] > adx_thresh, 'position'] += 1245    if use_stoch:246        df.loc[df['stoch'] < stoch_thresh, 'position'] += 1247    if use_bb:248        df.loc[df['close'] < df['bb_lower'], 'position'] += 1249 250    df['signal'] = df['position'].apply(lambda x: 1 if x >= signal_trigger_thresh else 0)251 252    position = 0253    equity = capital254    equity_curve = [capital]255    trades = []256 257    entry_time = None258    entry_price = 0259    units = 0260    indicators = ""261    stop_loss_price = 0262    take_profit_price = 0263 264    for i in range(len(df)):265        if i == 0:266            continue267 268        row = df.iloc[i]269        price = row['close']270        high_price = row['high']271        low_price = row['low']272 273        # If a position is open, check for SL/TP hit274        if position > 0:275            # Stop Loss276            if low_price <= stop_loss_price:277                exit_time = df.index[i]278                exit_price = stop_loss_price * (1 - slippage - spread)279                proceeds = position * exit_price * (1 - commission)280                pnl = proceeds - (units * entry_price)281                trades.append({282                    "Entry Time": entry_time,283                    "Exit Time": exit_time,284                    "Entry Price": round(entry_price, 4),285                    "Exit Price": round(exit_price, 4),286                    "Units": round(units, 4),287                    "PnL ($)": round(pnl, 2),288                    "Indicators Triggered": indicators,289                    "Exit Reason": "Stop Loss"290                })291                equity = proceeds292                position = 0293                stop_loss_price = 0294                take_profit_price = 0295            # Take Profit296            elif high_price >= take_profit_price:297                exit_time = df.index[i]298                exit_price = take_profit_price * (1 - slippage - spread)299                proceeds = position * exit_price * (1 - commission)300                pnl = proceeds - (units * entry_price)301                trades.append({302                    "Entry Time": entry_time,303                    "Exit Time": exit_time,304                    "Entry Price": round(entry_price, 4),305                    "Exit Price": round(exit_price, 4),306                    "Units": round(units, 4),307                    "PnL ($)": round(pnl, 2),308                    "Indicators Triggered": indicators,309                    "Exit Reason": "Take Profit"310                })311                equity = proceeds312                position = 0313                stop_loss_price = 0314                take_profit_price = 0315 316        # Entry317        if df['signal'].iloc[i] == 1 and position == 0:318            entry_time = df.index[i]319            entry_price = price * (1 + slippage + spread)320            units = equity / (entry_price * (1 + commission))321            equity -= units * entry_price * commission322            position = units323            indicators = ", ".join(324                [ind for ind, cond in zip(325                    ['EMA', 'RSI', 'ADX', 'STOCH', 'BB'],326                    [use_ema, use_rsi, use_adx, use_stoch, use_bb]327                ) if cond]328            )329            stop_loss_price = entry_price * (1 - sl_percent)330            take_profit_price = entry_price * (1 + tp_percent)331 332        # Signal exit (if not already exited by SL/TP)333        elif df['signal'].iloc[i] == 0 and position > 0:334            exit_time = df.index[i]335            exit_price = price * (1 - slippage - spread)336            proceeds = position * exit_price * (1 - commission)337            pnl = proceeds - (units * entry_price)338            trades.append({339                "Entry Time": entry_time,340                "Exit Time": exit_time,341                "Entry Price": round(entry_price, 4),342                "Exit Price": round(exit_price, 4),343                "Units": round(units, 4),344                "PnL ($)": round(pnl, 2),345                "Indicators Triggered": indicators,346                "Exit Reason": "Signal Change"347            })348            equity = proceeds349            position = 0350            stop_loss_price = 0351            take_profit_price = 0352 353        current_value = equity if position == 0 else position * price354        equity_curve.append(current_value)355 356    # Close any open position at the end357    if position > 0:358        last_price = df['close'].iloc[-1]359        exit_time = df.index[-1]360        exit_price = last_price * (1 - slippage - spread)361        proceeds = position * exit_price * (1 - commission)362        pnl = proceeds - (units * entry_price)363        trades.append({364            "Entry Time": entry_time,365            "Exit Time": exit_time,366            "Entry Price": round(entry_price, 4),367            "Exit Price": round(exit_price, 4),368            "Units": round(units, 4),369            "PnL ($)": round(pnl, 2),370            "Indicators Triggered": indicators,371            "Exit Reason": "End of Data (Forced Close)"372        })373        equity = proceeds374 375    df = df.iloc[1:].copy()376    df['cumulative'] = equity_curve[1:]377 378    # Dynamic grouping: weekly for short spans (<60 days), monthly otherwise379    try:380        span_days = (df.index[-1] - df.index[0]).days381    except Exception:382        span_days = 0383    bucket = "M" if span_days >= 60 else "W"  # month vs week384    df['bucket'] = df.index.to_series().dt.to_period(bucket)385 386    grouped = df.groupby("bucket")387    result = []388    for i, (name, group) in enumerate(grouped, 1):389        principal_for_bucket = group['cumulative'].iloc[0] if len(group['cumulative']) > 0 else capital390        profit = group['cumulative'].iloc[-1] - principal_for_bucket if len(group['cumulative']) > 0 else 0391        total = group['cumulative'].iloc[-1] if len(group['cumulative']) > 0 else principal_for_bucket392        # Keep column header as "Month" for UI compatibility (even if bucket is weekly)393        result.append([str(name), round(principal_for_bucket, 2), round(lot_size * 100, 2), round(profit, 2), round(total, 2)])394 395    trade_log_df = pd.DataFrame(trades)396    trade_log_df.to_csv(TEMP_TRADE_CSV, index=False)397    table = pd.DataFrame(result, columns=["Month", "Principal (P)", "Lots * 0.01", "Monthly Profit (T)", "P + T"])398    return df.dropna(), table, trade_log_df399 400def generate_growth_analysis(df_growth, capital):401    final_equity = df_growth['cumulative'].iloc[-1]402    total_return = (final_equity / capital) - 1403    investment_value = final_equity404    num_trades = len(df_growth[df_growth['signal'].diff() == 1])  # Count entries based on signal change405    summary = (406        f"Initial Capital: ${capital:.2f}\n"407        f"Final Investment Value: ${investment_value:.2f}\n"408        f"Total Return: {total_return * 100:.2f}%\n"409        f"Total Trades Taken: {int(num_trades)}"410    )411    return summary412 413def build_investment_chart(df_growth, capital, table_df):414    fig, ax = plt.subplots(figsize=(10, 6))415    ax.plot(table_df['Month'], table_df['Principal (P)'], label='Principal (P)', marker='o')416    ax.plot(table_df['Month'], table_df['Monthly Profit (T)'], label='Monthly Profit (T)', marker='x')417    ax.plot(table_df['Month'], table_df['P + T'], label='P + T', linestyle='--', marker='s')418    ax.set_title('Investment Growth (Monthly/Weekly)')419    ax.set_xlabel('Period')420    ax.set_ylabel('Value ($)')421    ax.legend()422    ax.grid(True)423 424    summary = generate_growth_analysis(df_growth, capital)425    with PdfPages(TEMP_PDF_PATH) as pdf:426        pdf.savefig(fig)  # DO NOT close the fig you return427 428    return table_df, fig, TEMP_PDF_PATH, summary429 430currency_pairs = ["EUR/USD", "USD/JPY", "GBP/USD", "AUD/USD", "USD/CAD", "USD/CHF", "NZD/USD", "EUR/GBP", "EUR/JPY", "USD/CNY", "XAU/USD", "XAG/USD"]431timeframes = ["1min", "5min", "15min", "30min", "45min", "1h", "2h", "4h", "8h", "1day", "1week", "1month"]432 433with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as app:434    gr.Markdown("# 🐂 Bullrun Capital | Investment Growth App")435 436    # === Auth state (NEW) ===437    user_state = gr.State(value=None)   # holds session dict438    email_state = gr.State(value=None)  # convenience email label439 440    # === Account (NEW) ===441    with gr.Tab("🔐 Account"):442        with gr.Row():443            with gr.Column():444                gr.Markdown("### Sign up")445                su_email = gr.Textbox(label="Email")446                su_password = gr.Textbox(label="Password", type="password")447                su_name = gr.Textbox(label="Full name (optional)")448                btn_signup = gr.Button("Create account")449                out_signup = gr.Markdown()450 451            with gr.Column():452                gr.Markdown("### Log in")453                li_email = gr.Textbox(label="Email")454                li_password = gr.Textbox(label="Password", type="password")455                btn_login = gr.Button("Log in")456                out_login = gr.Markdown()457 458        with gr.Row():459            btn_logout = gr.Button("Log out", variant="secondary")460            whoami = gr.Markdown("Not logged in.")461 462    # === Auth handlers (NEW) ===463    def do_signup(email, password, full_name):464        try:465            auth = sb.auth.sign_up({"email": email, "password": password})466            if auth.user:467                safe_upsert_profile({"id": auth.user.id, "email": email, "full_name": full_name or None})468            return "✅ Check your inbox to confirm your email before logging in."469        except Exception as e:470            return f"❌ Sign up failed: {e}"471 472    def do_login(email, password):473        try:474            session = sb.auth.sign_in_with_password({"email": email, "password": password})475            if not session.user:476                return gr.update(value="❌ Login failed."), None, None477            upsert_profile(session)478            return gr.update(value="✅ Login successful."), session.model_dump(), session.user.email479        except Exception as e:480            return gr.update(value=f"❌ Login failed: {e}"), None, None481 482    def do_logout():483        try:484            sb.auth.sign_out()485        except Exception:486            pass487        return None, None, "👋 Logged out."488 489    def show_whoami(session, email):490        if session and email:491            return f"🔒 Session active for **{email}**"492        return "Not logged in."493 494    btn_signup.click(do_signup, [su_email, su_password, su_name], [out_signup])495    btn_login.click(do_login, [li_email, li_password], [out_login, user_state, email_state])496    btn_logout.click(lambda: do_logout(), None, [user_state, email_state, whoami])497    user_state.change(show_whoami, [user_state, email_state], whoami)498 499    # === Protected tabs (wrapped in Groups so we can toggle visibility) ===500 501    with gr.Tab("Strategy Inputs"):502        with gr.Group(visible=False) as strategy_group:503            symbol = gr.Dropdown(currency_pairs, label="Symbol", value="XAU/USD")504            interval = gr.Dropdown(timeframes, label="Timeframe", value="1h")505            lot_size = gr.Slider(0.01, 1.0, value=0.1, label="Lot Size")506            capital = gr.Number(value=1000, label="Initial Capital ($)")507            only_2025 = gr.Checkbox(label="Only 2025 Data")508            start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)", placeholder="Optional")509            end_date = gr.Textbox(label="End Date (YYYY-MM-DD)", placeholder="Optional")510 511    with gr.Tab("Indicator Settings"):512        with gr.Group(visible=False) as indicator_group:513            use_ema = gr.Checkbox(label="Use EMA", value=True)514            ema_short = gr.Slider(5, 50, value=10, step=1, label="EMA Short", interactive=True)515            ema_long = gr.Slider(10, 100, value=50, step=1, label="EMA Long", interactive=True)516 517            use_rsi = gr.Checkbox(label="Use RSI", value=True)518            rsi_val = gr.Slider(7, 21, value=14, step=1, label="RSI Period", interactive=True)519            rsi_thresh = gr.Slider(10, 50, value=30, step=1, label="RSI Threshold", interactive=True)520 521            use_adx = gr.Checkbox(label="Use ADX", value=True)522            adx_val = gr.Slider(7, 21, value=14, step=1, label="ADX Period", interactive=True)523            adx_thresh = gr.Slider(10, 50, value=25, step=1, label="ADX Threshold", interactive=True)524 525            use_stoch = gr.Checkbox(label="Use Stochastic RSI", value=True)526            stochrsi_window = gr.Slider(10, 50, value=14, step=1, label="Stochastic RSI Window", interactive=True)527            stoch_thresh = gr.Slider(0.0, 1.0, value=0.2, step=0.01, label="Stochastic RSI Threshold", interactive=True)528 529            use_atr = gr.Checkbox(label="Use ATR", value=True)530            atr_window = gr.Slider(7, 21, value=14, step=1, label="ATR Window", interactive=True)531 532            use_bb = gr.Checkbox(label="Use Bollinger Bands", value=True)533            bb_window = gr.Slider(10, 50, value=20, step=1, label="Bollinger Bands Window", interactive=True)534 535            signal_trigger_thresh = gr.Slider(1, 6, value=2, step=1, label="Signal Trigger Threshold")536 537    with gr.Tab("Risk Settings"):538        with gr.Group(visible=False) as risk_group:539            sl_percent = gr.Slider(0.001, 0.1, value=0.01, step=0.001, label="Stop Loss (%) of Entry Price")  # 1% SL540            tp_percent = gr.Slider(0.001, 0.2, value=0.02, step=0.001, label="Take Profit (%) of Entry Price")  # 2% TP541            run_button = gr.Button("Generate Chart")542 543    with gr.Tab("Investment Growth"):544        with gr.Group(visible=False) as growth_group:545            table_output = gr.Dataframe()546            chart_output = gr.Plot()547            file_output = gr.File()548            summary_output = gr.Textbox(label="📋 Investment Analysis Summary", lines=6)549 550    with gr.Tab("📘 Trade Log History"):551        with gr.Group(visible=False) as log_group:552            trade_log_df = gr.Dataframe(label="🧾 Trades Executed")553            trade_csv_file = gr.File(label="⬇️ Download Trade Log (CSV)")554 555    with gr.Tab("📊 Bull Run Capital Chart"):556        with gr.Group(visible=False) as promo_group:557            with gr.Row():558                with gr.Column():559                    gr.Image(560                        value="https://huggingface.co/spaces/BullRunCapital/Forex_Testing/resolve/main/Table.png",561                        label="📈 Monthly Growth Table",562                        show_label=True,563                        interactive=True564                    )565                    gr.File(566                        value="https://huggingface.co/spaces/BullRunCapital/Forex_Testing/resolve/main/Table.png",567                        label="⬇️ Download Table Image",568                        file_types=[".png"]569                    )570 571    # === UI helpers (yours) ===572    def toggle_slider(use):573        return gr.update(interactive=use)574 575    use_ema.change(lambda u: (toggle_slider(u), toggle_slider(u)), inputs=use_ema, outputs=[ema_short, ema_long])576    use_rsi.change(lambda u: (toggle_slider(u), toggle_slider(u)), inputs=use_rsi, outputs=[rsi_val, rsi_thresh])577    use_adx.change(lambda u: (toggle_slider(u), toggle_slider(u)), inputs=use_adx, outputs=[adx_val, adx_thresh])578    use_stoch.change(lambda u: (toggle_slider(u), toggle_slider(u)), inputs=use_stoch, outputs=[stochrsi_window, stoch_thresh])579    use_atr.change(toggle_slider, inputs=use_atr, outputs=[atr_window])580    use_bb.change(toggle_slider, inputs=use_bb, outputs=[bb_window])581 582    # === Main run function (yours) with guards ===583    def run_growth_app(sym, tf, lot, cap, only_2025,584                       use_ema, ema1, ema2,585                       use_rsi, rsi, rsi_thresh,586                       use_adx, adx, adx_thresh,587                       use_atr, atr_win,588                       use_stoch, stoch, stoch_thresh,589                       use_bb, bb,590                       signal_thresh,591                       sl_p, tp_p,592                       sd, ed):593        try:594            sd = pd.to_datetime(sd).strftime('%Y-%m-%d') if sd else None595            ed = pd.to_datetime(ed).strftime('%Y-%m-%d') if ed else None596            df = fetch_data(sym, tf, start_date=sd, end_date=ed, only_2025=only_2025, max_batches=12, sleep_secs=0.15)597 598            if df is None or df.empty:599                msg = f"No data for {sym} at {tf} with the selected dates. Try narrowing dates, increasing max_batches, or a higher timeframe."600                return pd.DataFrame({"Error": [msg]}), None, None, msg, pd.DataFrame(), None601 602            df_growth, table_df, trade_log = get_rolling_profits(603                df, lot, cap,604                use_ema, ema1, ema2,605                use_rsi, rsi, rsi_thresh,606                use_adx, adx, adx_thresh,607                use_atr, atr_win,608                use_stoch, stoch, stoch_thresh,609                use_bb, bb,610                signal_thresh,611                sl_p, tp_p612            )613 614            if table_df is None or table_df.empty:615                msg = "No trades generated with current indicators/thresholds. Try lowering the signal threshold or widening dates."616                return pd.DataFrame({"Error": [msg]}), None, None, msg, trade_log, None617 618            table, chart, file, summary = build_investment_chart(df_growth, cap, table_df)619            return table, chart, file, summary, trade_log, TEMP_TRADE_CSV620 621        except Exception as e:622            err = f"Error: {str(e)}"623            return pd.DataFrame({"Error": [err]}), None, None, err, pd.DataFrame(), None624 625    run_button.click(626        run_growth_app,627        inputs=[symbol, interval, lot_size, capital, only_2025,628                use_ema, ema_short, ema_long,629                use_rsi, rsi_val, rsi_thresh,630                use_adx, adx_val, adx_thresh,631                use_atr, atr_window,632                use_stoch, stochrsi_window, stoch_thresh,633                use_bb, bb_window,634                signal_trigger_thresh,635                sl_percent, tp_percent,636                start_date, end_date],637        outputs=[table_output, chart_output, file_output, summary_output, trade_log_df, trade_csv_file]638    )639 640    # === Gate all protected groups based on login (NEW) ===641    def gate_visibility(session):642        visible = session is not None643        return [gr.update(visible=visible)] * 6  # six groups below644 645    user_state.change(646        gate_visibility,647        inputs=[user_state],648        outputs=[strategy_group, indicator_group, risk_group, growth_group, log_group, promo_group]649    )650 651 652if __name__ == "__main__":653    app.queue().launch(654        server_name="0.0.0.0",655        server_port=int(os.environ.get("PORT", "7860")),656        show_error=True657    )658