Spaces:
Running
Running
Download app.py from s-ttp/slipstream: direct link, hf CLI and curl.
- Browser
- Download file 7.47 kB
-
https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/app.py
- Command line
-
hf download hf://spaces/s-ttp/slipstream/app.py
-
curl -L -o app.py https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/app.py
7.47 kB
| """Slipstream β HF Space app. | |
| Serves the fund dashboard: the Slipstream paper portfolio + performance vs SPY + the | |
| current target book. The monthly rebalance is computed on the Space (book.py) and executed | |
| to Alpaca paper via a token-guarded, dry-run-by-default endpoint. | |
| The informational "Top AI Picks of the Month" panels moved to the Signalflow Space | |
| (SIGNALFLOW_URL); the old /picks page redirects there. | |
| """ | |
| from datetime import datetime, timezone | |
| from fastapi import FastAPI, Header, HTTPException | |
| from fastapi.responses import FileResponse, RedirectResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from starlette.concurrency import run_in_threadpool | |
| import config | |
| import alpaca_client as alpaca | |
| import book | |
| app = FastAPI(title="Slipstream") | |
| def health(): | |
| return {"fund": "Slipstream", | |
| "alpaca_configured": bool(config.ALPACA_API_KEY and config.ALPACA_SECRET_KEY), | |
| "alpaca_paper": config.alpaca_is_paper(), | |
| "rebalance_enabled": bool(config.REBALANCE_TOKEN)} | |
| async def status(): | |
| try: | |
| mkt = await run_in_threadpool(alpaca.market_status) | |
| return {"market": mkt, | |
| "last_order": await run_in_threadpool(alpaca.last_order_date), | |
| "rebalanced_this_month": await run_in_threadpool(alpaca.rebalanced_this_month), | |
| "rebalance_enabled": bool(config.REBALANCE_TOKEN)} | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(503, str(e)) | |
| async def fund(): | |
| try: | |
| return await run_in_threadpool(alpaca.portfolio) | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(503, str(e)) | |
| async def target(): | |
| return await run_in_threadpool(book.compute_book) | |
| async def nav(): | |
| try: | |
| from zoneinfo import ZoneInfo | |
| ET = ZoneInfo("America/New_York") | |
| hist = await run_in_threadpool(alpaca.nav_history) | |
| raw = list(hist["points"]) | |
| # ββ Date-join fund and SPY in MARKET time ββββββββββββββββββββββββββββββββββββββ | |
| # Alpaca portfolio-history epochs and SPY daily bars carry different timestamp | |
| # conventions for the same session, and extending the two lines independently let | |
| # them diverge (SPY silently missing the newest point). Join on the ET calendar | |
| # date, keep only dates BOTH have, and stamp both at that day's 16:00 ET close β | |
| # the chart can then never show skewed or ragged lines. | |
| fund_by: dict = {} | |
| for pt in raw: | |
| d = datetime.fromtimestamp(pt["t"], ET).date() | |
| fund_by[d] = pt["equity"] # last write per session wins | |
| start = min(fund_by).strftime("%Y-%m-%d") if fund_by else "2024-01-01" | |
| spy_raw = await run_in_threadpool(alpaca.spy_series, start) | |
| spy_by: dict = {} | |
| for b in spy_raw: | |
| try: | |
| d = datetime.fromisoformat(str(b["t"]).replace("Z", "+00:00")).astimezone(ET).date() | |
| spy_by[d] = b["c"] | |
| except (ValueError, KeyError): | |
| continue | |
| days = sorted(set(fund_by) & set(spy_by)) | |
| def _close_ts(d): | |
| return datetime(d.year, d.month, d.day, 16, 0, tzinfo=ET) | |
| pts = [{"t": int(_close_ts(d).timestamp()), "equity": float(fund_by[d])} for d in days] | |
| spy = [{"t": _close_ts(d).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| "c": float(spy_by[d])} for d in days] | |
| # Live "now" extension β appended as a PAIR or not at all, so the lines always | |
| # terminate together and match the live Day% KPI during market hours. | |
| try: | |
| acct = await run_in_threadpool(alpaca.account) | |
| eq = acct.get("equity") | |
| q = (await run_in_threadpool(alpaca.quotes, ["SPY"])).get("SPY") or {} | |
| now_et = datetime.now(ET) | |
| if (eq and q.get("price") and days and now_et.date() > days[-1] | |
| and (abs(float(eq) - pts[-1]["equity"]) > 0.01)): | |
| ts = int(now_et.timestamp()) | |
| pts.append({"t": ts, "equity": float(eq)}) | |
| spy.append({"t": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| "c": float(q["price"])}) | |
| except Exception: | |
| pass | |
| return {"fund": pts, "base_value": hist["base_value"], "spy": spy} | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(status_code=502, detail=str(e)) | |
| def api_supervisor(force: int = 0): | |
| """Daily health & drift monitor (flag-only, never trades) β tracking vs SPY, | |
| guardrails (paper/margin/exposure/caps), data & rebalance freshness.""" | |
| from services import supervisor | |
| return supervisor.report(force=bool(force)) | |
| async def rebalance(dry: int = 1, force: int = 0, x_rebalance_token: str = Header(default="")): | |
| """Rebalance the paper account to the Slipstream target book. Dry-run by default; a real | |
| submit needs dry=0 AND the matching REBALANCE_TOKEN. Executes at most once per calendar | |
| month (Slipstream is monthly) and only while the market is open β so a dumb daily cron is | |
| safe: it no-ops until the first market-open day of the month. `force=1` overrides the | |
| once-a-month guard.""" | |
| if dry: | |
| tgt = (await run_in_threadpool(book.compute_book))["holdings"] # memoised book | |
| try: | |
| return {"dry_run": True, **await run_in_threadpool(alpaca.plan_rebalance, tgt)} | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(503, str(e)) | |
| if not config.REBALANCE_TOKEN or x_rebalance_token != config.REBALANCE_TOKEN: | |
| raise HTTPException(403, "rebalance disabled or bad token (set REBALANCE_TOKEN, send X-Rebalance-Token)") | |
| if not await run_in_threadpool(alpaca.is_market_open): | |
| return {"dry_run": False, "executed": False, "reason": "market closed β retry during market hours"} | |
| if not force and await run_in_threadpool(alpaca.rebalanced_this_month): | |
| return {"dry_run": False, "executed": False, "reason": "already rebalanced this month (force=1 to override)"} | |
| tgt = (await run_in_threadpool(book.compute_book, True, 0))["holdings"] # real money: fresh book | |
| try: | |
| res = await run_in_threadpool(alpaca.execute_rebalance, tgt) | |
| return {"dry_run": False, "executed": True, **res} | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(400, str(e)) | |
| # HTML pages must never be cached stale: browsers held the old page across deploys | |
| # (assets are versioned with ?v=, but the HTML that references them was cacheable). | |
| _NO_CACHE = {"Cache-Control": "no-cache, must-revalidate"} | |
| def index(): | |
| return FileResponse(config.APP_DIR / "static" / "index.html", headers=_NO_CACHE) | |
| def picks_page(panel: str = ""): | |
| """The top-20 picks pages moved to Signalflow; keep old links working.""" | |
| target = config.SIGNALFLOW_URL.rstrip("/") + ("/picks?panel=" + panel if panel.isalpha() else "/") | |
| return RedirectResponse(target, status_code=301) | |
| def picks_moved(panel: str): | |
| raise HTTPException(410, f"the picks panels moved to {config.SIGNALFLOW_URL}") | |
| app.mount("/static", StaticFiles(directory=config.APP_DIR / "static"), name="static") | |