diff --git a/services/mcp-macro/src/mcp_macro/fetchers.py b/services/mcp-macro/src/mcp_macro/fetchers.py index 1bfe095..7790bd4 100644 --- a/services/mcp-macro/src/mcp_macro/fetchers.py +++ b/services/mcp-macro/src/mcp_macro/fetchers.py @@ -664,3 +664,43 @@ async def fetch_cot_tff(symbol: str, lookback_weeks: int = 52) -> dict[str, Any] _COT_CACHE[key] = out _COT_CACHE_TS[key] = now return out + + +async def fetch_cot_disaggregated(symbol: str, lookback_weeks: int = 52) -> dict[str, Any]: + """Fetch COT Disaggregated report per simbolo commodity. Returns ASC by date.""" + import time + + symbol = symbol.upper() + if symbol not in SYMBOL_TO_CFTC_CODE_DISAGG: + return {"error": "unknown_symbol", "available": ALL_DISAGG_SYMBOLS} + + key = (symbol, "disaggregated", lookback_weeks) + now = time.monotonic() + if key in _COT_CACHE and (now - _COT_CACHE_TS[key]) < _COT_TTL: + return _COT_CACHE[key] + + code = SYMBOL_TO_CFTC_CODE_DISAGG[symbol] + url = f"{CFTC_BASE_URL}/{DISAGG_DATASET_ID}.json" + async with async_client(timeout=10.0) as client: + resp = await client.get( + url, + params={ + "cftc_contract_market_code": code, + "$order": "report_date_as_yyyy_mm_dd DESC", + "$limit": str(lookback_weeks), + }, + ) + if resp.status_code != 200: + return {"symbol": symbol, "report_type": "disaggregated", "rows": [], "error": "cftc_unavailable"} + raw_rows = resp.json() or [] + parsed = [parse_disagg_row(r) for r in raw_rows] + parsed.sort(key=lambda r: r["report_date"]) + out = { + "symbol": symbol, + "report_type": "disaggregated", + "rows": parsed, + "data_timestamp": datetime.now(UTC).isoformat(), + } + _COT_CACHE[key] = out + _COT_CACHE_TS[key] = now + return out diff --git a/services/mcp-macro/tests/test_fetchers.py b/services/mcp-macro/tests/test_fetchers.py index 6bb279c..919b94a 100644 --- a/services/mcp-macro/tests/test_fetchers.py +++ b/services/mcp-macro/tests/test_fetchers.py @@ -318,3 +318,46 @@ async def test_fetch_cot_tff_unknown_symbol(): assert out.get("error") == "unknown_symbol" assert "ES" in out.get("available", []) + +@pytest.mark.asyncio +async def test_fetch_cot_disagg_happy_path(httpx_mock: pytest_httpx.HTTPXMock): + from mcp_macro.fetchers import fetch_cot_disaggregated + httpx_mock.add_response( + url=httpx.URL( + "https://publicreporting.cftc.gov/resource/72hh-3qpy.json", + params={ + "cftc_contract_market_code": "067651", + "$order": "report_date_as_yyyy_mm_dd DESC", + "$limit": "52", + }, + ), + json=[ + { + "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", + }, + ], + ) + out = await fetch_cot_disaggregated("CL", lookback_weeks=52) + assert out["symbol"] == "CL" + assert out["report_type"] == "disaggregated" + assert len(out["rows"]) == 1 + assert out["rows"][0]["managed_money_net"] == 120000 + assert out["rows"][0]["producer_net"] == -200000 + + +@pytest.mark.asyncio +async def test_fetch_cot_disagg_unknown_symbol(): + from mcp_macro.fetchers import fetch_cot_disaggregated + out = await fetch_cot_disaggregated("XYZ", lookback_weeks=52) + assert out.get("error") == "unknown_symbol" + assert "CL" in out.get("available", []) +