Download qt3/performance.py from s-ttp/updraft: direct link, hf CLI and curl.
- Browser
- Download file 6.91 kB
-
https://huggingface.co/spaces/s-ttp/updraft/resolve/main/qt3/performance.py
- Command line
-
hf download hf://spaces/s-ttp/updraft/qt3/performance.py
-
curl -L -o performance.py https://huggingface.co/spaces/s-ttp/updraft/resolve/main/qt3/performance.py
6.91 kB
| """Live performance from the Alpaca account: time-weighted return (deposits / withdrawals removed), drawdown, | |
| and the S&P 500 (SPY) over the same days. | |
| Each point is tagged: "alpaca" (a daily close from the account's own history), "rebuilt" (a trading day Alpaca's | |
| paper history has not posted yet — the account now, rolled back through every later fill and deposit, valued at | |
| that day's closing prices) or "live" (today's equity from the account).""" | |
| from __future__ import annotations | |
| from datetime import date, datetime, timedelta, timezone | |
| from typing import Optional | |
| from zoneinfo import ZoneInfo | |
| from . import config | |
| from .broker import Alpaca | |
| def twr(dates: list[str], equity: list[float], flows: dict[str, float]) -> list[float]: | |
| """Growth of $1 (time-weighted): each day's return excludes that day's net deposit.""" | |
| out, g = [], 1.0 | |
| for i, (d, e) in enumerate(zip(dates, equity)): | |
| if i > 0 and equity[i - 1] > 0: | |
| g *= (e - flows.get(d, 0.0)) / equity[i - 1] | |
| out.append(g) | |
| return out | |
| def bench_value(equity: list[float], spy_px: list, dates: list[str], flows: dict[str, float]) -> list: | |
| """Dollar value of the S&P 500 holding the same money: starts at the account's first value and receives the | |
| same deposits / withdrawals on the same days.""" | |
| out, v, prev = [], None, None | |
| for e, px, d in zip(equity, spy_px, dates): | |
| if v is None: | |
| v = e | |
| elif px and prev: | |
| v = v * px / prev + flows.get(d, 0.0) | |
| out.append(round(v, 2) if v is not None else None) | |
| prev = px or prev | |
| return out | |
| def rebuild(days: list[str], cash_now: float, qty_now: dict[str, float], fills: list[dict], | |
| flows: dict[str, float], closes: dict[str, dict[str, float]]) -> dict[str, float]: | |
| """Close-of-day equity for days missing from Alpaca's history. Holdings and cash on day d are today's minus | |
| every fill and deposit dated after d; they are valued at d's closing prices. A day with a held stock lacking a | |
| close is skipped.""" | |
| out = {} | |
| for d in days: | |
| qty, cash = dict(qty_now), cash_now - sum(v for k, v in flows.items() if k > d) | |
| for f in fills: | |
| if f["date"] > d: | |
| sgn = 1 if f["side"] == "buy" else -1 | |
| qty[f["symbol"]] = qty.get(f["symbol"], 0.0) - sgn * f["qty"] | |
| cash += sgn * f["qty"] * f["price"] | |
| held = {s: q for s, q in qty.items() if abs(q) > 1e-9} | |
| if all(d in closes.get(s, {}) for s in held): | |
| out[d] = round(cash + sum(q * closes[s][d] for s, q in held.items()), 2) | |
| return out | |
| def _fills(a: Alpaca, since: str, ny: ZoneInfo) -> list[dict]: | |
| after = datetime.fromisoformat(since).replace(tzinfo=ny) - timedelta(days=7) | |
| out = [] | |
| for o in a.orders(after=after, status="all"): | |
| q, px, at = float(o.get("filled_qty") or 0), o.get("filled_avg_price"), o.get("filled_at") | |
| if q > 0 and px and at: | |
| when = datetime.fromisoformat(at.replace("Z", "+00:00")).astimezone(ny).date().isoformat() | |
| out.append({"date": when, "symbol": o["symbol"], "side": o["side"], "qty": q, "price": float(px)}) | |
| return out | |
| def max_drawdown(g: list[float]) -> float: | |
| peak, worst = 0.0, 0.0 | |
| for x in g: | |
| peak = max(peak, x) | |
| worst = min(worst, x / peak - 1 if peak else 0.0) | |
| return worst | |
| def series(a: Alpaca, inception: Optional[str] = None) -> dict: | |
| ny = ZoneInfo("America/New_York") | |
| h = a.portfolio_history() | |
| hist = {} | |
| for t, e in zip(h.get("timestamp") or [], h.get("equity") or []): | |
| if e is not None and float(e) > 0: | |
| hist[datetime.fromtimestamp(t, ny).date().isoformat()] = float(e) | |
| start = inception or config.INCEPTION | |
| if not start: | |
| first = [o for o in a.orders(status="all") if str(o.get("client_order_id", "")).startswith(config.TAG + "-")] | |
| start = min((o["created_at"][:10] for o in first), default=None) | |
| now = datetime.now(ny) | |
| today = now.date().isoformat() | |
| pts = sorted((d, e, "alpaca") for d, e in hist.items() if d < today) # today comes from the live account | |
| if start: # base = the last close before the first trade (all cash) | |
| before = [p for p in pts if p[0] < start] | |
| pts = before[-1:] + [p for p in pts if p[0] >= start] | |
| acct = a.account() | |
| flows = {} | |
| for f in a.cash_flows(): | |
| flows[f["date"]] = flows.get(f["date"], 0.0) + f["amount"] | |
| first_day = pts[0][0] if pts else (start or today) | |
| bars = {b["t"]: b["c"] for b in a.daily_bars("SPY", date.fromisoformat(first_day) - timedelta(days=7))} | |
| have = {p[0] for p in pts} | |
| gaps = [d for d in sorted(bars) if pts and pts[0][0] < d < today and d not in have] | |
| if gaps: # Alpaca's paper history can lag a day or more behind | |
| pos, fills = a.positions(), _fills(a, gaps[0], ny) | |
| syms = set(pos) | {f["symbol"] for f in fills if f["date"] > gaps[0]} | |
| closes = {s: {b["t"]: b["c"] for b in a.daily_bars(s, date.fromisoformat(gaps[0]), adjustment="raw")} | |
| for s in syms} | |
| got = rebuild(gaps, acct["cash"], {s: p["qty"] for s, p in pos.items()}, fills, flows, closes) | |
| pts = sorted(pts + [(d, e, "rebuilt") for d, e in got.items()]) | |
| if acct["equity"] > 0 and now.weekday() < 5: | |
| pts.append((today, acct["equity"], "live")) | |
| if len(pts) < 1: | |
| return {"start": start, "dates": [], "strategy": [], "spy": [], "stats": {}} | |
| dates, eq, src = [p[0] for p in pts], [p[1] for p in pts], [p[2] for p in pts] | |
| g = twr(dates, eq, flows) | |
| spy_now = a.latest_prices(["SPY"]).get("SPY") | |
| if spy_now and dates[-1] == today: | |
| bars[today] = spy_now | |
| closes = sorted(bars.items()) # latest S&P close on or before each date | |
| spy, j, last = [], 0, None | |
| for d in dates: | |
| while j < len(closes) and closes[j][0] <= d: | |
| last = closes[j][1] | |
| j += 1 | |
| spy.append(last) | |
| base = next((x for x in spy if x), None) | |
| bench = bench_value(eq, spy, dates, flows) # the same money (and deposits) in the S&P | |
| spy = [round(x / base, 6) if (x and base) else None for x in spy] | |
| days = (date.fromisoformat(dates[-1]) - date.fromisoformat(dates[0])).days | |
| tot = g[-1] - 1 | |
| spy_tot = (spy[-1] - 1) if spy and spy[-1] else None | |
| ann = (g[-1] ** (365.25 / days) - 1) if days >= 365 else None | |
| return {"start": start or dates[0], "base": dates[0], "dates": dates, "src": src, | |
| "value": [round(x, 2) for x in eq], "bench_value": bench, "strategy": [round(x, 6) for x in g], "spy": spy, | |
| "flows_today": flows.get(today, 0.0), | |
| "stats": {"return": tot, "spy_return": spy_tot, "annualised": ann, "max_drawdown": max_drawdown(g), | |
| "days": days, "equity": eq[-1]}} | |