diff --git a/services/mcp-macro/src/mcp_macro/cot.py b/services/mcp-macro/src/mcp_macro/cot.py index 2d0a373..a18645f 100644 --- a/services/mcp-macro/src/mcp_macro/cot.py +++ b/services/mcp-macro/src/mcp_macro/cot.py @@ -35,3 +35,57 @@ def classify_extreme(percentile: float | None, threshold: float = 5.0) -> Extrem 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")), + } diff --git a/services/mcp-macro/tests/test_cot.py b/services/mcp-macro/tests/test_cot.py index 9ed678e..0151718 100644 --- a/services/mcp-macro/tests/test_cot.py +++ b/services/mcp-macro/tests/test_cot.py @@ -42,3 +42,74 @@ def test_classify_extreme_neutral(): def test_classify_extreme_none_input(): assert classify_extreme(None) == "neutral" + + +from mcp_macro.cot import parse_disagg_row, parse_tff_row + + +# Payload Socrata reale (subset campi rilevanti, valori arbitrari per test) +TFF_SOCRATA_ROW = { + "report_date_as_yyyy_mm_dd": "2026-04-22T00:00:00.000", + "dealer_positions_long_all": "12345", + "dealer_positions_short_all": "23456", + "asset_mgr_positions_long": "654321", + "asset_mgr_positions_short": "200000", + "lev_money_positions_long": "100000", + "lev_money_positions_short": "350000", + "other_rept_positions_long": "50000", + "other_rept_positions_short": "50000", + "open_interest_all": "2500000", +} + +DISAGG_SOCRATA_ROW = { + "report_date_as_yyyy_mm_dd": "2026-04-22T00:00:00.000", + "prod_merc_positions_long_all": "100000", + "prod_merc_positions_short_all": "300000", + "swap_positions_long_all": "50000", + "swap_positions_short_all": "60000", + "m_money_positions_long_all": "200000", + "m_money_positions_short_all": "80000", + "other_rept_positions_long_all": "10000", + "other_rept_positions_short_all": "10000", + "open_interest_all": "1500000", +} + + +def test_parse_tff_row_extracts_all_fields(): + row = parse_tff_row(TFF_SOCRATA_ROW) + assert row["report_date"] == "2026-04-22" + assert row["dealer_long"] == 12345 + assert row["dealer_short"] == 23456 + assert row["dealer_net"] == 12345 - 23456 + assert row["asset_mgr_long"] == 654321 + assert row["asset_mgr_net"] == 654321 - 200000 + assert row["lev_funds_long"] == 100000 + assert row["lev_funds_short"] == 350000 + assert row["lev_funds_net"] == 100000 - 350000 + assert row["other_long"] == 50000 + assert row["other_net"] == 0 + assert row["open_interest"] == 2500000 + + +def test_parse_tff_row_handles_missing_field(): + payload = {"report_date_as_yyyy_mm_dd": "2026-04-22T00:00:00.000"} + row = parse_tff_row(payload) + assert row["report_date"] == "2026-04-22" + assert row["dealer_long"] == 0 + assert row["dealer_net"] == 0 + + +def test_parse_disagg_row_extracts_all_fields(): + row = parse_disagg_row(DISAGG_SOCRATA_ROW) + assert row["report_date"] == "2026-04-22" + assert row["producer_long"] == 100000 + assert row["producer_short"] == 300000 + assert row["producer_net"] == -200000 + assert row["swap_long"] == 50000 + assert row["swap_net"] == -10000 + assert row["managed_money_long"] == 200000 + assert row["managed_money_short"] == 80000 + assert row["managed_money_net"] == 120000 + assert row["other_long"] == 10000 + assert row["other_net"] == 0 + assert row["open_interest"] == 1500000