"""DeepQuant — live paper-account monitor (Phase 0, READ-ONLY). Shows the Alpaca PAPER account: equity, cash, today's P&L, open positions, and an equity curve vs the S&P 500. No order code exists in this phase — pure observation. Auto-refreshes every 30 minutes. """ import datetime as dt import json import os import ssl import urllib.request from zoneinfo import ZoneInfo _ET = ZoneInfo("America/New_York") import gradio as gr import pandas as pd from huggingface_hub import hf_hub_download import alpaca_client as ac import rebalance_engine as eng try: import certifi _CTX = ssl.create_default_context(cafile=certifi.where()) except Exception: _CTX = ssl.create_default_context() AV = os.environ.get("ALPHAVANTAGE_API_KEY", "") def fetch_spy_daily(): if not AV: return None try: u = f"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=SPY&outputsize=compact&apikey={AV}" d = json.load(urllib.request.urlopen(u, timeout=25, context=_CTX)).get("Time Series (Daily)", {}) return {dt.date.fromisoformat(k): float(v["5. adjusted close"]) for k, v in d.items()} except Exception: return None def _curve_pts(h): ts, eqv = h.get("timestamp", []), h.get("equity", []) return [(dt.datetime.fromtimestamp(t, _ET).replace(tzinfo=None), v) for t, v in zip(ts, eqv) if v] def _mk_df(rows): df = pd.DataFrame(rows) df["date"] = pd.to_datetime(df["date"]).astype("datetime64[ns]") return df def fetch_spy_intraday(): if not AV: return None try: u = f"https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=SPY&interval=15min&outputsize=compact&apikey={AV}" d = json.load(urllib.request.urlopen(u, timeout=25, context=_CTX)).get("Time Series (15min)", {}) return {dt.datetime.strptime(k, "%Y-%m-%d %H:%M:%S"): float(v["4. close"]) for k, v in d.items()} except Exception: return None def build_curve(): """Returns (dataframe_or_None, note). Daily fund-vs-SPY once there's history; intraday today (both lines) for a young account.""" try: daily = _curve_pts(ac.get_portfolio_history("1M", "1D")) except Exception: daily = [] if len(daily) >= 4: # enough daily history → daily fund vs SPY base = daily[0][1] rows = [{"date": d, "series": "DeepQuant (paper)", "value": round(v, 2)} for d, v in daily] spy = fetch_spy_daily() if spy: sd, first = sorted(spy), None for d, _v in daily: prior = [x for x in sd if x <= d.date()] if not prior: continue px = spy[prior[-1]] first = first or px rows.append({"date": d, "series": "S&P 500 (SPY)", "value": round(base * px / first, 2)}) return _mk_df(rows), "" try: # young account → today's intraday, fund AND SPY intr = _curve_pts(ac.get_portfolio_history("1D", "15Min")) except Exception: intr = [] if len(intr) < 2: return None, "Equity curve builds once the account has trading history." base, lo, hi = intr[0][1], intr[0][0], intr[-1][0] rows = [{"date": d, "series": "DeepQuant (paper)", "value": round(v, 2)} for d, v in intr] spy = fetch_spy_intraday() if spy: inr = sorted(k for k in spy if lo - dt.timedelta(minutes=20) <= k <= hi + dt.timedelta(minutes=20)) if inr: first = spy[inr[0]] for k in inr: rows.append({"date": k, "series": "S&P 500 (SPY)", "value": round(base * spy[k] / first, 2)}) return _mk_df(rows), "Today's intraday — fund vs S&P 500. The multi-day curve builds over the coming sessions." def refresh(): now = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: a = ac.get_account() clk = ac.get_clock() pos = ac.get_positions() except Exception as e: return (f"### ⚠️ Could not reach Alpaca\n`{type(e).__name__}: {str(e)[:160]}`", pd.DataFrame(), gr.update(visible=False), gr.update(value="—", visible=True), f"_Last checked {now}_") eq, last = float(a["equity"]), float(a.get("last_equity", a["equity"])) cash = float(a["cash"]) dpl = eq - last dpct = (dpl / last * 100) if last else 0.0 mkt = "🟢 open" if clk.get("is_open") else "🔴 closed" nxt = str(clk.get("next_open", ""))[:16].replace("T", " ") invested = eq - cash summary = ( f"## ${eq:,.2f}\n" f"**Today's P&L** {dpl:+,.2f} ({dpct:+.2f}%) · account **{a.get('status')}** · market {mkt}" + ("" if clk.get("is_open") else f" (next open {nxt})") + "\n\n" f"| Cash | Invested | Open positions | Currency |\n|---|---|---|---|\n" f"| ${cash:,.2f} | ${invested:,.2f} | {len(pos)} | {a.get('currency','USD')} |\n\n" f"Long-only strategy: margin and short-selling are never used, regardless of account permissions." ) rows = [] for p in pos: qty, mv = float(p["qty"]), float(p["market_value"]) rows.append({ "Symbol": p["symbol"], "Qty": round(qty, 4), "Avg entry": f'${float(p["avg_entry_price"]):,.2f}', "Last": f'${float(p["current_price"]):,.2f}', "Mkt value": f"${mv:,.2f}", "Weight": f"{mv / eq * 100:.1f}%" if eq else "—", "Unrealized P&L": f'${float(p["unrealized_pl"]):,.2f} ({float(p["unrealized_plpc"]) * 100:+.1f}%)', }) df = pd.DataFrame(rows) if rows else pd.DataFrame([{"Symbol": "— no open positions —"}]) curve, cnote = build_curve() stamp = f"_Last updated {now} · auto-refreshes every 30 min_" if curve is None: return (summary, df, gr.update(visible=False), gr.update(value=f"_{cnote}_", visible=True), stamp) note_upd = gr.update(value=f"_{cnote}_", visible=True) if cnote else gr.update(visible=False) return (summary, df, gr.update(value=curve, visible=True), note_upd, stamp) def load_rebalance(): tok = os.environ.get("HF_TOKEN") if not tok: return "" try: log = json.load(open(hf_hub_download("s-ttp/marketfm-panel", "deepquant_log.json", repo_type="dataset", token=tok, force_download=True))) if not log: return "" e = log[-1] mode = "🟢 OFFENSE" if e.get("mode") == "OFFENSE" else "🛡️ DEFENSE" head = (f"### 🗓️ Latest rebalance — {e.get('date','')} · {mode}\n" f"{e.get('buys',0)} buys · {e.get('sells',0)} sells · {e.get('turnover_pct',0)}% turnover · " f"{e.get('n_positions','—')} holdings\n\n") if e.get("narration"): return head + f"> 💬 *{e['narration']}*\n\n— {e.get('narrated_by','MarketGuruLLM')}" return head + f"{e.get('summary','')}" except Exception: return "" def rebalance_preview(): tok = os.environ.get("HF_TOKEN") if not tok: return "_Set the `HF_TOKEN` secret so DeepQuant can read the MarketFM target._", pd.DataFrame(), pd.DataFrame() try: tgt = eng.get_target(tok) acct = ac.get_account() eq = float(acct["equity"]) positions = {p["symbol"]: float(p["market_value"]) for p in ac.get_positions()} orders = eng.compute_orders(tgt["weights"], positions, eq) except Exception as e: return f"### ⚠️ Could not build preview\n`{type(e).__name__}: {str(e)[:180]}`", pd.DataFrame(), pd.DataFrame() reg = tgt["regime"] mode = "🟢 OFFENSE" if reg["offense"] else "🛡️ DEFENSE" sma = (f"SPY 10-wk {reg['sma10']:,.0f} vs 40-wk {reg['sma40']:,.0f}" if reg.get("sma10") else reg.get("warn", "")) mix = "50 stocks · 100% equity" if reg["offense"] else "60% stocks + 20% AGG + 20% GLD" nb = sum(1 for o in orders if o["side"] == "BUY") ns = sum(1 for o in orders if o["side"] == "SELL") traded = sum(abs(o["delta"]) for o in orders) summ = (f"**{len(orders)} orders** — {nb} buys, {ns} sells · gross traded **${traded:,.0f}** " f"({traded / eq * 100:.0f}% turnover) · no-trade band skips deltas < ${0.005 * eq:,.0f}" if orders else "**No trades needed** — the account already matches the target within the no-trade band.") banner = (f"## {mode}\n**Regime:** {sma} · equity **${eq:,.0f}** · target: {mix}\n\n{summ}\n\n" f"Preview only — **no orders are placed**. This is exactly what the agent would submit at the next " f"Monday-after-open rebalance (notional market orders, sells before buys).") tdf = pd.DataFrame([{"Ticker": r["ticker"], "Sector": r["sector"], "Target %": f'{r["weight"] * 100:.1f}%', "Target $": f'${r["weight"] * eq:,.0f}'} for r in tgt["rows"]]) if orders: odf = pd.DataFrame([{"Symbol": o["symbol"], "Side": ("🔻 SELL" if o["side"] == "SELL" else "🟢 BUY"), "Trade $": f'${abs(o["delta"]):,.0f}', "Now": f'{o["cur_w"] * 100:.1f}%', "→ Target": f'{o["tgt_w"] * 100:.1f}%'} for o in orders]) else: odf = pd.DataFrame([{"Symbol": "— already at target —"}]) return banner, tdf, odf with gr.Blocks(title="DeepQuant — live paper monitor", theme=gr.themes.Soft()) as demo: gr.Markdown( "# 🤖 DeepQuant\n" "### An agent that autonomously **executes** a strategy backtested to beat the S&P 500 over 20+ years\n" "DeepQuant runs a regime-aware, **multi-asset** strategy end-to-end — using the **Alpaca API to both execute " "*and* monitor** every trade on a live paper account.\n\n" "**Under the hood:**\n" "- 🧠 **[MarketFM](https://huggingface.co/spaces/s-ttp/marketfm-ranker)** — a survivorship-free quant model — ranks the universe and sets the target portfolio\n" "- 💬 **[MarketGuruLLM](https://huggingface.co/s-ttp/qwen25-14b-fin-lora)** — a fine-tuned analyst LLM — reasons over and narrates every rebalance\n" "- ⚡ **Alpaca API** — the brokerage rails the **quant execution engine** drives to place orders and stream live fills, positions and P&L\n\n" "**The strategy:** large-cap **momentum + quality** while the trend is up; when it turns down it de-risks into a " "blend of **stocks + bonds + gold** (60 / 20 / 20). Backtested 2002–2026: **~14% / yr vs the S&P 500's ~11%**, " "higher Sharpe, smaller drawdown.\n" "Phase 0 — live paper-account monitor below; autonomous weekly execution rolls out next. " "Backtested results, paper trading — not investment advice.") with gr.Tabs(): with gr.Tab("📊 Live account"): stats = gr.Markdown() rebal = gr.Markdown() gr.Markdown("### Holdings") tbl = gr.Dataframe(wrap=True, interactive=False) gr.Markdown("### Equity vs S&P 500") plot = gr.LinePlot(x="date", y="value", color="series", height=320, y_title="Account value ($)", x_title="") note = gr.Markdown(visible=False) with gr.Row(): updated = gr.Markdown("_loading…_") btn = gr.Button("↻ Refresh now", scale=0) with gr.Tab("🔄 Rebalance preview"): gr.Markdown("What the agent **would** trade at the next rebalance — computed live from the MarketFM target " "and your current account. **Nothing is placed** (Phase 1, dry-run).") pv_banner = gr.Markdown() pbtn = gr.Button("⟳ Recompute preview", scale=0) with gr.Row(): with gr.Column(): gr.Markdown("**Target portfolio**") tgt_tbl = gr.Dataframe(interactive=False, wrap=True) with gr.Column(): gr.Markdown("**Proposed orders** — sells first, then buys") ord_tbl = gr.Dataframe(interactive=False, wrap=True) timer = gr.Timer(1800) acct_outs = [stats, tbl, plot, note, updated] demo.load(refresh, None, acct_outs) btn.click(refresh, None, acct_outs) timer.tick(refresh, None, acct_outs) demo.load(load_rebalance, None, rebal) btn.click(load_rebalance, None, rebal) timer.tick(load_rebalance, None, rebal) pv_outs = [pv_banner, tgt_tbl, ord_tbl] demo.load(rebalance_preview, None, pv_outs) pbtn.click(rebalance_preview, None, pv_outs) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)