Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import yfinance as yf | |
| import plotly.graph_objects as go | |
| import tempfile | |
| import os | |
| # --- Helpers --- | |
| def parse_tickers(tickers_text, uploaded_csv): | |
| if uploaded_csv is not None: | |
| try: | |
| df = pd.read_csv(uploaded_csv.name if hasattr(uploaded_csv, "name") else uploaded_csv) | |
| except Exception as e: | |
| raise ValueError(f"Failed to read CSV: {e}") | |
| if "ticker" in df.columns: | |
| tickers = [str(t).strip().upper() for t in df["ticker"].dropna().unique()] | |
| else: | |
| # try first column | |
| tickers = [str(t).strip().upper() for t in df.iloc[:, 0].dropna().unique()] | |
| return tickers | |
| else: | |
| tickers = [t.strip().upper() for t in (tickers_text or "").split(",") if t.strip()] | |
| return tickers | |
| def fetch_price_dfs(tickers, period, interval): | |
| if not tickers: | |
| return {} | |
| data = yf.download(tickers, period=period, interval=interval, group_by="ticker", progress=False, threads=True) | |
| result = {} | |
| # multi-index case | |
| if isinstance(data.columns, pd.MultiIndex): | |
| for t in tickers: | |
| if t in data.columns.levels[0]: | |
| df = data[t].dropna(how="all").reset_index() | |
| if not df.empty: | |
| result[t] = df | |
| else: | |
| df = data.dropna(how="all").reset_index() | |
| if not df.empty: | |
| if len(tickers) == 1: | |
| result[tickers[0]] = df | |
| else: | |
| # fallback: assign same df to first ticker | |
| result[tickers[0]] = df | |
| return result | |
| def compute_universe_metrics(price_dfs): | |
| rows = [] | |
| for t, df in price_dfs.items(): | |
| close = df["Close"].astype(float) | |
| if close.shape[0] < 2: | |
| recent_return = 0.0 | |
| vol = 0.0 | |
| else: | |
| recent_return = (close.iloc[-1] / close.iloc[0]) - 1 | |
| daily_rets = close.pct_change().dropna() | |
| vol = float(daily_rets.std(ddof=0) * np.sqrt(252)) if not daily_rets.empty else 0.0 | |
| rows.append({"ticker": t, "last_price": float(close.iloc[-1]) if not close.empty else 0.0, | |
| "recent_return": float(recent_return), "vol_annual": float(vol)}) | |
| dfm = pd.DataFrame(rows).set_index("ticker") | |
| n = max(len(dfm), 1) | |
| # normalize by ranks to 0..1 | |
| if len(dfm) > 1: | |
| dfm["ret_rank"] = dfm["recent_return"].rank(ascending=False, method="average") | |
| dfm["vol_rank"] = dfm["vol_annual"].rank(ascending=True, method="average") | |
| dfm["ret_norm"] = (dfm["ret_rank"] - 1) / (n - 1) | |
| dfm["vol_norm"] = (dfm["vol_rank"] - 1) / (n - 1) | |
| # score: favor higher return and lower vol | |
| dfm["score"] = 0.6 * dfm["ret_norm"] + 0.4 * (1 - dfm["vol_norm"]) | |
| else: | |
| dfm["score"] = 0.5 | |
| return dfm.fillna(0) | |
| def heuristic_trade_idea(ticker, last_price, score, vol, risk_tolerance): | |
| """ | |
| risk_tolerance: 1 (low risk) .. 3 (high risk) | |
| score: 0..1 | |
| """ | |
| s = float(score) | |
| lp = float(last_price) if last_price else 0.0 | |
| vol = float(vol) if vol else 0.0 | |
| # action | |
| if s >= 0.66: | |
| action = "Long" | |
| target_mult = 1.12 # +12% | |
| stop_mult = 1 - 0.05 / (risk_tolerance) # tighter stop for low risk tolerance | |
| elif s >= 0.4: | |
| action = "Conditional Long (watch for confirmation)" | |
| target_mult = 1.08 | |
| stop_mult = 1 - 0.07 / (risk_tolerance) | |
| else: | |
| action = "Avoid / Monitor (not recommended for new long positions)" | |
| target_mult = 1 - 0.08 | |
| stop_mult = 1 + 0.04 | |
| target = round(lp * target_mult, 4) | |
| stop = round(lp * stop_mult, 4) | |
| confidence = f"{int(s*100)}%" | |
| rationale = f"{ticker} score={s:.2f}. Recent return and volatility imply " \ | |
| f"{'favorable' if s>=0.4 else 'weak'} risk-reward. Annual vol ~ {vol:.2%}." | |
| key_risks = [ | |
| "Earnings / news event could invalidate the idea", | |
| "Macro moves / liquidity shocks increase short-term risk" | |
| ] | |
| idea_text = ( | |
| f"{action} {ticker} at ~{lp}. Target: {target}. Stop-loss: {stop}.\n\n" | |
| f"Rationale: {rationale}\n" | |
| f"Key Risks: {', '.join(key_risks)}\n" | |
| f"Confidence: {confidence}\n" | |
| f"Generated deterministically (no external LLM)." | |
| ) | |
| structured = { | |
| "ticker": ticker, "action": action, "entry": lp, "target": target, | |
| "stop": stop, "rationale": rationale, "risks": key_risks, "confidence": confidence | |
| } | |
| return idea_text, structured | |
| def make_price_figure(price_dfs, tickers): | |
| fig = go.Figure() | |
| for t in tickers: | |
| if t in price_dfs: | |
| df = price_dfs[t] | |
| fig.add_trace(go.Scatter(x=df["Date"], y=df["Close"], name=t)) | |
| fig.update_layout(title="Price Chart", margin=dict(t=40)) | |
| return fig | |
| def save_text_file(text): | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", prefix="trade_idea_") | |
| tmp.write(text.encode("utf-8")) | |
| tmp.close() | |
| return tmp.name | |
| # --- Main app function --- | |
| def run_tool(tickers_text, uploaded_csv, period, interval, risk_tolerance, selected_ticker_for_idea): | |
| try: | |
| tickers = parse_tickers(tickers_text, uploaded_csv) | |
| except Exception as e: | |
| return f"Input error: {e}", None, None, None | |
| if not tickers: | |
| return "No tickers provided.", None, None, None | |
| try: | |
| price_dfs = fetch_price_dfs(tickers, period, interval) | |
| except Exception as e: | |
| return f"Fetch error: {e}", None, None, None | |
| if not price_dfs: | |
| return "No price data returned for given tickers/period.", None, None, None | |
| metrics = compute_universe_metrics(price_dfs) | |
| # HTML table for display | |
| html_table = metrics[["last_price", "recent_return", "vol_annual", "score"]].round(6).to_html(classes="table table-striped", border=0) | |
| # prepare price figure for the tickers | |
| fig = make_price_figure(price_dfs, tickers) | |
| # choose ticker for idea | |
| t_for_idea = selected_ticker_for_idea.strip().upper() if selected_ticker_for_idea else tickers[0] | |
| if t_for_idea not in metrics.index: | |
| # fallback to first ticker | |
| t_for_idea = metrics.index[0] | |
| last_price = metrics.loc[t_for_idea, "last_price"] | |
| score = metrics.loc[t_for_idea, "score"] | |
| vol = metrics.loc[t_for_idea, "vol_annual"] | |
| idea_text, structured = heuristic_trade_idea(t_for_idea, last_price, score, vol, risk_tolerance) | |
| # make downloadable file | |
| file_path = save_text_file(idea_text) | |
| # return: idea text, html table, plot, file path | |
| return idea_text, html_table, fig, file_path | |
| # --- Gradio UI --- | |
| with gr.Blocks(title="Trade Idea Generator") as demo: | |
| gr.Markdown("## 🔍 Trade Idea Generator \nUpload tickers or type comma-separated symbols. Tool generates simple, rule-based trade ideas and metrics.") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| tickers_in = gr.Textbox(label="Tickers (comma-separated)", value="AAPL, MSFT, NVDA") | |
| csv_up = gr.File(label="Or upload CSV (first column = tickers or column named 'ticker')", file_types=[".csv"]) | |
| period = gr.Dropdown(choices=["1mo", "3mo", "6mo", "1y", "2y", "5y"], value="6mo", label="Price period") | |
| interval = gr.Dropdown(choices=["1d", "1wk", "1mo"], value="1d", label="Price interval") | |
| risk_tolerance = gr.Slider(minimum=1, maximum=3, step=1, value=2, label="Risk tolerance (1 low — 3 high)") | |
| selected_ticker = gr.Textbox(label="Ticker to generate idea for (optional)", placeholder="e.g., AAPL") | |
| run_btn = gr.Button("Generate Trade Idea") | |
| with gr.Column(scale=3): | |
| idea_out = gr.Textbox(label="Trade Idea (deterministic)", interactive=False) | |
| table_out = gr.HTML() | |
| plot_out = gr.Plot() | |
| download_file = gr.File(label="Download idea as .txt") | |
| run_btn.click(fn=run_tool, inputs=[tickers_in, csv_up, period, interval, risk_tolerance, selected_ticker], outputs=[idea_out, table_out, plot_out, download_file]) | |
| if __name__ == "__main__": | |
| demo.launch() | |