"""Pure-logic helpers per COT report parsing e analytics. Niente HTTP qui — orchestrazione fetch sta in fetchers.py. Tutto testabile in isolamento. """ from __future__ import annotations from typing import Literal ExtremeSignal = Literal["extreme_short", "extreme_long", "neutral"] def compute_percentile(value: float, history: list[float]) -> float | None: """Percentile di `value` rispetto ad `history` (0-100, inclusive). Restituisce None se history vuoto. Clipped a [0, 100] se value fuori range. """ if not history: return None n = len(history) below_or_eq = sum(1 for h in history if h <= value) pct = 100.0 * below_or_eq / n return max(0.0, min(100.0, pct)) def classify_extreme(percentile: float | None, threshold: float = 5.0) -> ExtremeSignal: """Classifica un percentile come estremo short/long o neutral. threshold default 5 → flagga ≤ 5 come short, ≥ 100-5=95 come long. """ if percentile is None: return "neutral" if percentile <= threshold: return "extreme_short" if percentile >= 100.0 - threshold: return "extreme_long" return "neutral" def _to_int(v) -> int: try: return int(float(v)) except (TypeError, ValueError): return 0 def _date_only(s: str) -> str: """Estrae 'YYYY-MM-DD' da una data ISO con o senza timestamp.""" if not s: return "" return s.split("T", 1)[0] def parse_tff_row(raw: dict) -> dict: """Mappa una row Socrata TFF al formato API output.""" dl = _to_int(raw.get("dealer_positions_long_all")) ds = _to_int(raw.get("dealer_positions_short_all")) al = _to_int(raw.get("asset_mgr_positions_long")) as_ = _to_int(raw.get("asset_mgr_positions_short")) ll = _to_int(raw.get("lev_money_positions_long")) ls = _to_int(raw.get("lev_money_positions_short")) ol = _to_int(raw.get("other_rept_positions_long")) os_ = _to_int(raw.get("other_rept_positions_short")) return { "report_date": _date_only(raw.get("report_date_as_yyyy_mm_dd", "")), "dealer_long": dl, "dealer_short": ds, "dealer_net": dl - ds, "asset_mgr_long": al, "asset_mgr_short": as_, "asset_mgr_net": al - as_, "lev_funds_long": ll, "lev_funds_short": ls, "lev_funds_net": ll - ls, "other_long": ol, "other_short": os_, "other_net": ol - os_, "open_interest": _to_int(raw.get("open_interest_all")), } def parse_disagg_row(raw: dict) -> dict: """Mappa una row Socrata Disaggregated F&O combined al formato API output.""" pl = _to_int(raw.get("prod_merc_positions_long_all")) ps = _to_int(raw.get("prod_merc_positions_short_all")) sl = _to_int(raw.get("swap_positions_long_all")) ss = _to_int(raw.get("swap_positions_short_all")) ml = _to_int(raw.get("m_money_positions_long_all")) ms = _to_int(raw.get("m_money_positions_short_all")) ol = _to_int(raw.get("other_rept_positions_long_all")) os_ = _to_int(raw.get("other_rept_positions_short_all")) return { "report_date": _date_only(raw.get("report_date_as_yyyy_mm_dd", "")), "producer_long": pl, "producer_short": ps, "producer_net": pl - ps, "swap_long": sl, "swap_short": ss, "swap_net": sl - ss, "managed_money_long": ml, "managed_money_short": ms, "managed_money_net": ml - ms, "other_long": ol, "other_short": os_, "other_net": ol - os_, "open_interest": _to_int(raw.get("open_interest_all")), }