88bd4e7bde
- exchanges/macro: cot.py + cot_contracts.py + fetchers.py copiati 1:1 con rewrite import mcp_common -> cerbero_mcp.common, mcp_macro -> cerbero_mcp.exchanges.macro - nuovo MacroClient stateless wrapper: trasporta solo fred_api_key/finnhub_api_key, niente HTTP session (i fetchers usano async_client ad-hoc) - tools.py: 11 tool (get_treasury_yields, get_yield_curve_slope, get_breakeven_inflation, get_economic_indicators, get_macro_calendar, get_market_overview, get_equity_futures, get_asset_price, get_cot_tff, get_cot_disaggregated, get_cot_extreme_positioning) — niente write, niente leverage_cap - routers/macro.py: prefix /mcp-macro, 11 route POST /tools/* - builder branch macro: stesse credenziali per testnet/mainnet (env ignorato); registry istanzia 2 entry, costo trascurabile (wrapper stateless) - test migrati: test_cot.py + test_fetchers.py (test_server_acl.py skippato V1-only) - nuovo test test_build_client_macro_no_env_distinction in test_exchanges_builder.py Suite: 224 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
"""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")),
|
|
}
|