Upload folder using huggingface_hub
Browse files- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/rebalance_engine.cpython-312.pyc +0 -0
- app.py +69 -14
- rebalance_engine.py +112 -0
- requirements.txt +3 -0
__pycache__/app.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
|
|
|
__pycache__/rebalance_engine.cpython-312.pyc
ADDED
|
Binary file (7.92 kB). View file
|
|
|
app.py
CHANGED
|
@@ -14,6 +14,7 @@ import gradio as gr
|
|
| 14 |
import pandas as pd
|
| 15 |
|
| 16 |
import alpaca_client as ac
|
|
|
|
| 17 |
|
| 18 |
try:
|
| 19 |
import certifi
|
|
@@ -106,6 +107,42 @@ def refresh():
|
|
| 106 |
f"_Last updated {now} · auto-refreshes every 30 min_")
|
| 107 |
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
with gr.Blocks(title="DeepQuant — live paper monitor", theme=gr.themes.Soft()) as demo:
|
| 110 |
gr.Markdown(
|
| 111 |
"# 🤖 DeepQuant\n"
|
|
@@ -121,22 +158,40 @@ with gr.Blocks(title="DeepQuant — live paper monitor", theme=gr.themes.Soft())
|
|
| 121 |
"higher Sharpe, smaller drawdown.\n"
|
| 122 |
"<sub>Phase 0 — live paper-account monitor below; autonomous weekly execution rolls out next. "
|
| 123 |
"Backtested results, paper trading — not investment advice.</sub>")
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
timer = gr.Timer(1800)
|
| 135 |
|
| 136 |
-
|
| 137 |
-
demo.load(refresh, None,
|
| 138 |
-
btn.click(refresh, None,
|
| 139 |
-
timer.tick(refresh, None,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
if __name__ == "__main__":
|
| 142 |
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
|
|
|
|
| 14 |
import pandas as pd
|
| 15 |
|
| 16 |
import alpaca_client as ac
|
| 17 |
+
import rebalance_engine as eng
|
| 18 |
|
| 19 |
try:
|
| 20 |
import certifi
|
|
|
|
| 107 |
f"_Last updated {now} · auto-refreshes every 30 min_")
|
| 108 |
|
| 109 |
|
| 110 |
+
def rebalance_preview():
|
| 111 |
+
tok = os.environ.get("HF_TOKEN")
|
| 112 |
+
if not tok:
|
| 113 |
+
return "_Set the `HF_TOKEN` secret so DeepQuant can read the MarketFM target._", pd.DataFrame(), pd.DataFrame()
|
| 114 |
+
try:
|
| 115 |
+
tgt = eng.get_target(tok)
|
| 116 |
+
acct = ac.get_account()
|
| 117 |
+
eq = float(acct["equity"])
|
| 118 |
+
positions = {p["symbol"]: float(p["market_value"]) for p in ac.get_positions()}
|
| 119 |
+
orders = eng.compute_orders(tgt["weights"], positions, eq)
|
| 120 |
+
except Exception as e:
|
| 121 |
+
return f"### ⚠️ Could not build preview\n`{type(e).__name__}: {str(e)[:180]}`", pd.DataFrame(), pd.DataFrame()
|
| 122 |
+
reg = tgt["regime"]
|
| 123 |
+
mode = "🟢 OFFENSE" if reg["offense"] else "🛡️ DEFENSE"
|
| 124 |
+
sma = (f"SPY 10-wk {reg['sma10']:,.0f} vs 40-wk {reg['sma40']:,.0f}" if reg.get("sma10") else reg.get("warn", ""))
|
| 125 |
+
mix = "50 stocks · 100% equity" if reg["offense"] else "60% stocks + 20% AGG + 20% GLD"
|
| 126 |
+
nb = sum(1 for o in orders if o["side"] == "BUY")
|
| 127 |
+
ns = sum(1 for o in orders if o["side"] == "SELL")
|
| 128 |
+
traded = sum(abs(o["delta"]) for o in orders)
|
| 129 |
+
summ = (f"**{len(orders)} orders** — {nb} buys, {ns} sells · gross traded **${traded:,.0f}** "
|
| 130 |
+
f"({traded / eq * 100:.0f}% turnover) · no-trade band skips deltas < ${0.005 * eq:,.0f}"
|
| 131 |
+
if orders else "**No trades needed** — the account already matches the target within the no-trade band.")
|
| 132 |
+
banner = (f"## {mode}\n**Regime:** {sma} · equity **${eq:,.0f}** · target: {mix}\n\n{summ}\n\n"
|
| 133 |
+
f"<sub>Preview only — **no orders are placed**. This is exactly what the agent would submit at the next "
|
| 134 |
+
f"Monday-after-open rebalance (notional market orders, sells before buys).</sub>")
|
| 135 |
+
tdf = pd.DataFrame([{"Ticker": r["ticker"], "Sector": r["sector"], "Target %": f'{r["weight"] * 100:.1f}%',
|
| 136 |
+
"Target $": f'${r["weight"] * eq:,.0f}'} for r in tgt["rows"]])
|
| 137 |
+
if orders:
|
| 138 |
+
odf = pd.DataFrame([{"Symbol": o["symbol"], "Side": ("🔻 SELL" if o["side"] == "SELL" else "🟢 BUY"),
|
| 139 |
+
"Trade $": f'${abs(o["delta"]):,.0f}', "Now": f'{o["cur_w"] * 100:.1f}%',
|
| 140 |
+
"→ Target": f'{o["tgt_w"] * 100:.1f}%'} for o in orders])
|
| 141 |
+
else:
|
| 142 |
+
odf = pd.DataFrame([{"Symbol": "— already at target —"}])
|
| 143 |
+
return banner, tdf, odf
|
| 144 |
+
|
| 145 |
+
|
| 146 |
with gr.Blocks(title="DeepQuant — live paper monitor", theme=gr.themes.Soft()) as demo:
|
| 147 |
gr.Markdown(
|
| 148 |
"# 🤖 DeepQuant\n"
|
|
|
|
| 158 |
"higher Sharpe, smaller drawdown.\n"
|
| 159 |
"<sub>Phase 0 — live paper-account monitor below; autonomous weekly execution rolls out next. "
|
| 160 |
"Backtested results, paper trading — not investment advice.</sub>")
|
| 161 |
+
with gr.Tabs():
|
| 162 |
+
with gr.Tab("📊 Live account"):
|
| 163 |
+
stats = gr.Markdown()
|
| 164 |
+
gr.Markdown("### Holdings")
|
| 165 |
+
tbl = gr.Dataframe(wrap=True, interactive=False)
|
| 166 |
+
gr.Markdown("### Equity vs S&P 500")
|
| 167 |
+
plot = gr.LinePlot(x="date", y="value", color="series", height=320,
|
| 168 |
+
y_title="Account value ($)", x_title="")
|
| 169 |
+
note = gr.Markdown(visible=False)
|
| 170 |
+
with gr.Row():
|
| 171 |
+
updated = gr.Markdown("_loading…_")
|
| 172 |
+
btn = gr.Button("↻ Refresh now", scale=0)
|
| 173 |
+
with gr.Tab("🔄 Rebalance preview"):
|
| 174 |
+
gr.Markdown("What the agent **would** trade at the next rebalance — computed live from the MarketFM target "
|
| 175 |
+
"and your current account. **Nothing is placed** (Phase 1, dry-run).")
|
| 176 |
+
pv_banner = gr.Markdown()
|
| 177 |
+
pbtn = gr.Button("⟳ Recompute preview", scale=0)
|
| 178 |
+
with gr.Row():
|
| 179 |
+
with gr.Column():
|
| 180 |
+
gr.Markdown("**Target portfolio**")
|
| 181 |
+
tgt_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 182 |
+
with gr.Column():
|
| 183 |
+
gr.Markdown("**Proposed orders** — sells first, then buys")
|
| 184 |
+
ord_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 185 |
timer = gr.Timer(1800)
|
| 186 |
|
| 187 |
+
acct_outs = [stats, tbl, plot, note, updated]
|
| 188 |
+
demo.load(refresh, None, acct_outs)
|
| 189 |
+
btn.click(refresh, None, acct_outs)
|
| 190 |
+
timer.tick(refresh, None, acct_outs)
|
| 191 |
+
|
| 192 |
+
pv_outs = [pv_banner, tgt_tbl, ord_tbl]
|
| 193 |
+
demo.load(rebalance_preview, None, pv_outs)
|
| 194 |
+
pbtn.click(rebalance_preview, None, pv_outs)
|
| 195 |
|
| 196 |
if __name__ == "__main__":
|
| 197 |
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
|
rebalance_engine.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DeepQuant — deterministic rebalance engine (Phase 1: compute only, no execution).
|
| 2 |
+
|
| 3 |
+
Reproduces the MarketFM Tactical target portfolio and diffs it against the live account in
|
| 4 |
+
DOLLARS, trading only the change. No order is placed here — compute_orders returns the list
|
| 5 |
+
the agent *would* submit. Execution (Phase 2) consumes this same output, guardrailed.
|
| 6 |
+
"""
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import ssl
|
| 10 |
+
import urllib.request
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
from huggingface_hub import hf_hub_download
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
import certifi
|
| 18 |
+
_CTX = ssl.create_default_context(cafile=certifi.where())
|
| 19 |
+
except Exception:
|
| 20 |
+
_CTX = ssl.create_default_context()
|
| 21 |
+
|
| 22 |
+
PANEL_REPO = os.environ.get("MARKETFM_REPO", "s-ttp/marketfm-panel")
|
| 23 |
+
AV = os.environ.get("ALPHAVANTAGE_API_KEY", "")
|
| 24 |
+
SEG_K, SECTOR_CAP, SIZE, MAX_W = 1000, 5, 50, 0.10 # top-1000 by mktcap, ≤5/sector, 50 names, 10% cap
|
| 25 |
+
DEF_EQ, DEF_AGG, DEF_GLD = 0.60, 0.20, 0.20 # defense: 60% stocks + 20% bonds + 20% gold
|
| 26 |
+
ETF = {"AGG": ("iShares Core US Aggregate Bond ETF", "Bonds (ETF)"),
|
| 27 |
+
"GLD": ("SPDR Gold Shares", "Gold (ETF)")}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def cap_weights(w, mw=MAX_W):
|
| 31 |
+
w = np.asarray(w, float)
|
| 32 |
+
for _ in range(30):
|
| 33 |
+
over = w > mw + 1e-9
|
| 34 |
+
if not over.any():
|
| 35 |
+
break
|
| 36 |
+
excess = (w[over] - mw).sum()
|
| 37 |
+
w[over] = mw
|
| 38 |
+
under = ~over
|
| 39 |
+
if w[under].sum() <= 0:
|
| 40 |
+
break
|
| 41 |
+
w[under] += excess * w[under] / w[under].sum()
|
| 42 |
+
return w
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def detect_regime():
|
| 46 |
+
try:
|
| 47 |
+
u = f"https://www.alphavantage.co/query?function=TIME_SERIES_WEEKLY_ADJUSTED&symbol=SPY&apikey={AV}"
|
| 48 |
+
ts = json.load(urllib.request.urlopen(u, timeout=25, context=_CTX))["Weekly Adjusted Time Series"]
|
| 49 |
+
s = pd.Series({k: float(v["5. adjusted close"]) for k, v in ts.items()}).sort_index()
|
| 50 |
+
sma10, sma40 = s.rolling(10).mean().iloc[-1], s.rolling(40).mean().iloc[-1]
|
| 51 |
+
return {"offense": bool(sma10 >= sma40), "sma10": float(sma10), "sma40": float(sma40), "spy": float(s.iloc[-1])}
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return {"offense": True, "sma10": None, "sma40": None, "spy": None,
|
| 54 |
+
"warn": f"SPY fetch failed ({type(e).__name__}); defaulting to OFFENSE"}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def equity_sleeve(token):
|
| 58 |
+
p = hf_hub_download(PANEL_REPO, "marketfm_snapshot.parquet", repo_type="dataset", token=token)
|
| 59 |
+
df = pd.read_parquet(p)
|
| 60 |
+
df = df[df["marketcap"].notna() & (df["marketcap"] > 0)].copy()
|
| 61 |
+
df = df.sort_values("marketcap", ascending=False).head(SEG_K)
|
| 62 |
+
df["score"] = df["n_mom_12_1"].fillna(0) + df["n_roe"].fillna(0)
|
| 63 |
+
df = df.sort_values("score", ascending=False)
|
| 64 |
+
picked, sec = [], {}
|
| 65 |
+
for _, r in df.iterrows():
|
| 66 |
+
if sec.get(r["sector"], 0) >= SECTOR_CAP:
|
| 67 |
+
continue
|
| 68 |
+
sec[r["sector"]] = sec.get(r["sector"], 0) + 1
|
| 69 |
+
picked.append(r)
|
| 70 |
+
if len(picked) >= SIZE:
|
| 71 |
+
break
|
| 72 |
+
mc = np.array([r["marketcap"] for r in picked], float)
|
| 73 |
+
w = cap_weights(mc / mc.sum())
|
| 74 |
+
return [{"ticker": r["ticker"], "name": r.get("name"), "sector": r["sector"], "weight": float(wi)}
|
| 75 |
+
for r, wi in zip(picked, w)]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def get_target(token):
|
| 79 |
+
reg = detect_regime()
|
| 80 |
+
sleeve = equity_sleeve(token)
|
| 81 |
+
if reg["offense"]:
|
| 82 |
+
rows = [dict(x) for x in sleeve]
|
| 83 |
+
else:
|
| 84 |
+
rows = [dict(x, weight=DEF_EQ * x["weight"]) for x in sleeve]
|
| 85 |
+
for sym, (nm, sct) in ETF.items():
|
| 86 |
+
rows.append({"ticker": sym, "name": nm, "sector": sct,
|
| 87 |
+
"weight": DEF_AGG if sym == "AGG" else DEF_GLD})
|
| 88 |
+
weights = {r["ticker"]: r["weight"] for r in rows}
|
| 89 |
+
return {"regime": reg, "weights": weights, "rows": rows}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def compute_orders(weights, positions, equity, band_pct=0.005):
|
| 93 |
+
"""positions: {symbol: market_value $}. Returns order list (sells first), trading only the delta."""
|
| 94 |
+
band = band_pct * equity
|
| 95 |
+
orders = []
|
| 96 |
+
for sym in set(weights) | set(positions):
|
| 97 |
+
tw = weights.get(sym, 0.0)
|
| 98 |
+
tgt = tw * equity
|
| 99 |
+
cur = positions.get(sym, 0.0)
|
| 100 |
+
delta = tgt - cur
|
| 101 |
+
cw = (cur / equity) if equity else 0.0
|
| 102 |
+
if tw == 0 and cur > 0: # dropped from basket → full exit (always)
|
| 103 |
+
orders.append({"symbol": sym, "side": "SELL", "delta": -cur, "cur_w": cw, "tgt_w": 0.0, "exit": True})
|
| 104 |
+
elif cur == 0 and tgt > 1.0: # establish a new target position (always; band can't block it)
|
| 105 |
+
orders.append({"symbol": sym, "side": "BUY", "delta": tgt, "cur_w": 0.0, "tgt_w": tw, "exit": False})
|
| 106 |
+
elif abs(delta) < band: # churn guard: skip small adjustments to EXISTING positions
|
| 107 |
+
continue
|
| 108 |
+
else:
|
| 109 |
+
orders.append({"symbol": sym, "side": "BUY" if delta > 0 else "SELL",
|
| 110 |
+
"delta": delta, "cur_w": cw, "tgt_w": tw, "exit": False})
|
| 111 |
+
orders.sort(key=lambda o: (0 if o["side"] == "SELL" else 1, -abs(o["delta"]))) # sells first
|
| 112 |
+
return orders
|
requirements.txt
CHANGED
|
@@ -1,3 +1,6 @@
|
|
| 1 |
gradio==5.50.0
|
| 2 |
pandas
|
|
|
|
|
|
|
|
|
|
| 3 |
certifi
|
|
|
|
| 1 |
gradio==5.50.0
|
| 2 |
pandas
|
| 3 |
+
numpy
|
| 4 |
+
pyarrow
|
| 5 |
+
huggingface_hub
|
| 6 |
certifi
|