Spaces:
Running
Running
Download risk_screen.py from s-ttp/slipstream: direct link, hf CLI and curl.
- Browser
- Download file 9.03 kB
-
https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/risk_screen.py
- Command line
-
hf download hf://spaces/s-ttp/slipstream/risk_screen.py
-
curl -L -o risk_screen.py https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/risk_screen.py
9.03 kB
| """Slipstream — AI event-risk overlay (second-stage exclusion on the deterministic book). | |
| The 20 holdings are chosen by the systematic quant rule. This overlay then scans the candidate | |
| names for RECENT, CONCRETE, MATERIALLY-ADVERSE EVENTS the price/fundamentals may not have caught | |
| yet — legal, regulatory/FDA, geopolitical/trade, operational, competitive — and HARD-EXCLUDES any | |
| with a CRITICAL event only (since Oct 2026; "high" events are shown as watch flags and held). It is deliberately NOT a subjective "risk score": it only fires on a | |
| discrete event backed by a real headline, so ongoing/priced risks (general competition, valuation) | |
| can never trip it. Excluded names are replaced deterministically by the next-ranked candidate. | |
| Disciplined by design: capped exclusions, every exclusion cites a headline, cached once per UTC day, | |
| and it degrades to NO exclusions if news or the LLM is unavailable (the fund must always trade). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import re | |
| from datetime import datetime, timezone | |
| import config | |
| MAX_EXCLUSIONS = int(os.getenv("SLIPSTREAM_MAX_EXCLUSIONS", "3")) | |
| ENABLED = os.getenv("SLIPSTREAM_RISK_SCREEN", "1") == "1" | |
| CACHE = config.DATA_DIR / "risk_screen_cache.json" | |
| _SEV_RANK = {"none": 0, "medium": 1, "high": 2, "critical": 3} | |
| # Only events at or above this severity are excluded. "critical" = very high risk only: the backtested | |
| # rule has no overlay, so the live fund should deviate from it only for events that threaten the company. | |
| EXCLUDE_AT = os.getenv("SLIPSTREAM_EXCLUDE_SEVERITY", "critical").lower() | |
| # Deterministic backstop for the prompt's "analyst actions are never events" rule — the model | |
| # still returned `NFLX / medium / "Analyst target price cut and downgrade…"` (2026-09), citing a | |
| # Wells Fargo note. A flag whose triggering headline is analyst commentary is dropped unless the | |
| # same text also names a hard event. | |
| _ANALYST = re.compile( | |
| r"\b(analyst|downgrad\w*|upgrad\w*|price target|target price|rating|slash\w* (its )?target|" | |
| r"cuts? (its )?target|initiat\w* coverage|overweight|underweight|outperform|underperform)\b", re.I) | |
| _HARD_EVENT = re.compile( | |
| r"\b(FDA|SEC|DOJ|FTC|EU|indict\w*|verdict|injunction|consent decree|recall|sanction\w*|entity list|" | |
| r"export (ban|control)|tariff|bankrupt\w*|going.concern|restat\w*|resign\w*|steps? down|breach|" | |
| r"withdr\w* guidance|clinical hold|complete response|delist\w*|nationali[sz]\w*)\b", re.I) | |
| _PROMPT = """You are a risk officer screening a concentrated equity portfolio. For EACH ticker below, | |
| review its recent news headlines and decide whether a DISCRETE, RECENT, MATERIALLY-ADVERSE EVENT has | |
| occurred — something the market may not have fully priced. Only these event types count: | |
| • legal: SEC/DOJ action, indictment, adverse verdict/settlement, major patent loss | |
| • regulatory/FDA: complete response letter, clinical hold, adcom rejection, recall, license loss | |
| • geopolitical/trade: sanctions, entity-list addition, export ban, nationalization, tariff naming the company | |
| • operational: guidance withdrawal, going-concern, bankruptcy, major customer/contract loss, cyber-breach, | |
| accounting restatement, abrupt CEO/CFO exit | |
| • competitive: a SPECIFIC rival action (approval/launch) that directly displaces this company's product | |
| Do NOT flag ongoing or already-known risks (general competition, high valuation, cyclicality, macro, | |
| "analysts are cautious"). Analyst actions of ANY kind — rating changes, downgrades, price-target cuts, | |
| "worrying trends" commentary — are NEVER events: severity "none", even if the concern sounds | |
| operational. If there is no concrete adverse event in the headlines, severity is "none". | |
| Severity: none | medium | high | critical. Use high/critical only for events that plausibly impair | |
| value materially. | |
| MATERIALITY MUST BE JUDGED RELATIVE TO COMPANY SIZE (use your knowledge of each company's | |
| approximate market capitalization): | |
| • "high" requires plausible exposure of roughly >5% of the company's market value, OR a threat to | |
| the core business model / license to operate. | |
| • A civil lawsuit or regulatory complaint at the FILING stage against a large company, where the | |
| claimed amount is under ~2% of its market value, is at most "medium" (monitor, do not exclude) — | |
| a $20B claim is existential for a $30B company but a rounding error for a $2T one, and mega-cap | |
| antitrust filings historically resolve over years without impairing 12-month returns. | |
| • Adverse VERDICTS, injunctions, consent decrees, criminal indictments, or forced structural | |
| changes CAN be "high" regardless of company size. | |
| • "critical" is reserved for VERY HIGH risk — an event that threatens the company's survival or a large | |
| part of its value: bankruptcy / going-concern warning, fraud or accounting restatement, criminal | |
| indictment of the company or its CEO/CFO, loss of the license to operate (e.g. a key drug's approval | |
| withdrawn, delisting), sanctions/export bans cutting off a major share of revenue, or plausible | |
| exposure above ~20% of market value. When in doubt between "high" and "critical", choose "high". | |
| • If headlines show the stock already fell only modestly (under ~5%) on the news, the market has | |
| likely priced it — lean "medium" unless the event meets the tests above. | |
| Return ONLY a JSON object mapping each ticker to: | |
| {{"severity": "...", "category": "...", "event": "<one factual line>", "headline": "<the triggering headline, verbatim>"}} | |
| HEADLINES: | |
| {blocks}""" | |
| def _load_cache(): | |
| try: | |
| return json.loads(CACHE.read_text()) | |
| except Exception: | |
| return {} | |
| def _classify(pool: list[str], news_by_tkr: dict) -> dict: | |
| """One batched Gemini call → {ticker: {severity, category, event, headline}}.""" | |
| from services.llm import _call_gemini | |
| blocks = [] | |
| for t in pool: | |
| hls = news_by_tkr.get(t, []) | |
| if not hls: | |
| continue | |
| lines = "\n".join(f" - {h['headline']}" for h in hls[:8]) | |
| blocks.append(f"{t}:\n{lines}") | |
| if not blocks: | |
| return {} | |
| raw = _call_gemini(_PROMPT.format(blocks="\n\n".join(blocks))) | |
| m = raw[raw.find("{"): raw.rfind("}") + 1] | |
| data = json.loads(m) | |
| out = {} | |
| for t, v in data.items(): | |
| if isinstance(v, dict) and str(v.get("severity", "none")).lower() != "none": | |
| text = f"{v.get('headline', '')} {v.get('event', '')}" | |
| if _ANALYST.search(text) and not _HARD_EVENT.search(text): | |
| continue # analyst commentary ≠ event | |
| out[t.upper()] = {"severity": str(v.get("severity", "none")).lower(), | |
| "category": str(v.get("category", ""))[:40], | |
| "event": str(v.get("event", ""))[:200], | |
| "headline": str(v.get("headline", ""))[:200]} | |
| return out | |
| def screen(pool: list[str]) -> dict: | |
| """Return {excluded: set, meta: {...}} for a candidate pool. Cached per UTC day; safe on failure.""" | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| pool = [t for t in pool if t] | |
| key = today + "|" + EXCLUDE_AT + "|" + ",".join(sorted(pool)) | |
| if not (ENABLED and config.GEMINI_API_KEY): | |
| return {"excluded": set(), "meta": {"enabled": False, "scanned": 0, "cleared": 0, "excluded": [], "as_of": today}} | |
| cache = _load_cache() | |
| if cache.get("key") == key: | |
| c = cache["meta"] | |
| return {"excluded": set(x["ticker"] for x in c["excluded"]), "meta": c} | |
| excluded_list, flags = [], {} | |
| try: | |
| import alpaca_client as alpaca | |
| news_by_tkr = alpaca.news(pool) | |
| flags = _classify(pool, news_by_tkr) | |
| ranked = sorted(flags.items(), key=lambda kv: -_SEV_RANK.get(kv[1]["severity"], 0)) | |
| for t, f in ranked: | |
| if _SEV_RANK.get(f["severity"], 0) >= _SEV_RANK.get(EXCLUDE_AT, 3) and len(excluded_list) < MAX_EXCLUSIONS: | |
| excluded_list.append({"ticker": t, **f}) | |
| except Exception as e: | |
| # any failure (no news, LLM error, bad JSON) → no exclusions, fund still trades | |
| meta = {"enabled": True, "scanned": len(pool), "cleared": len(pool), "excluded": [], | |
| "as_of": today, "note": f"screen unavailable ({type(e).__name__}) — no exclusions applied"} | |
| return {"excluded": set(), "meta": meta} | |
| meta = {"enabled": True, "scanned": len(pool), "cleared": len(pool) - len(excluded_list), | |
| "excluded": excluded_list, "exclude_at": EXCLUDE_AT, | |
| "flags_medium": [{"ticker": t, **f} for t, f in sorted(flags.items(), key=lambda kv: -_SEV_RANK.get(kv[1]["severity"], 0)) | |
| if 0 < _SEV_RANK.get(f["severity"], 0) < _SEV_RANK.get(EXCLUDE_AT, 3)][:5], | |
| "as_of": today} | |
| try: | |
| CACHE.write_text(json.dumps({"key": key, "meta": meta})) | |
| except Exception: | |
| pass | |
| return {"excluded": set(x["ticker"] for x in excluded_list), "meta": meta} | |