Spaces:
Running
Running
Download app.py from s-ttp/slipstream: direct link, hf CLI and curl.
- Browser
- Download file 8.19 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
8.19 kB
| """Slipstream β HF Space app. | |
| Serves the fund dashboard: the Slipstream paper portfolio + performance vs SPY + the | |
| current target book, plus the informational "Top AI Picks of the Month" panels. The | |
| monthly rebalance is computed on the Space (book.py) and executed to Alpaca paper via a | |
| token-guarded, dry-run-by-default endpoint. | |
| """ | |
| from datetime import datetime, timezone | |
| from fastapi import FastAPI, Header, HTTPException | |
| from fastapi.responses import FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from starlette.concurrency import run_in_threadpool | |
| import config | |
| import alpaca_client as alpaca | |
| import book | |
| import services.superinvestor as superinvestor | |
| import services.insider as insider | |
| import services.news as news | |
| app = FastAPI(title="Slipstream") | |
| PANELS = {"superinvestor": superinvestor.top_picks, "insider": insider.top_picks, "news": news.top_picks} | |
| 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 quotes(symbols: str): | |
| syms = [s for s in symbols.split(",") if s.strip()][:60] | |
| if not syms: | |
| raise HTTPException(400, "no symbols") | |
| try: | |
| return await run_in_threadpool(alpaca.quotes, syms) | |
| except alpaca.AlpacaError as e: | |
| raise HTTPException(503, str(e)) | |
| async def picks(panel: str, refresh: bool = False, limit: int = 5): | |
| if panel not in PANELS: | |
| raise HTTPException(404, f"unknown panel {panel!r}") | |
| limit = max(1, min(int(limit), 20)) # homepage uses 5; the dedicated pages use 20 | |
| try: | |
| return await run_in_threadpool(PANELS[panel], refresh, limit) | |
| except Exception as e: # noqa: BLE001 β surface data/scrape failures to the UI | |
| raise HTTPException(502, str(e)) | |
| 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(): | |
| """Dedicated top-20 page for one category (?panel=superinvestor|insider|news).""" | |
| return FileResponse(config.APP_DIR / "static" / "picks.html", headers=_NO_CACHE) | |
| app.mount("/static", StaticFiles(directory=config.APP_DIR / "static"), name="static") | |