diff --git a/services/mcp-macro/src/mcp_macro/fetchers.py b/services/mcp-macro/src/mcp_macro/fetchers.py index f92bbe1..1bfe095 100644 --- a/services/mcp-macro/src/mcp_macro/fetchers.py +++ b/services/mcp-macro/src/mcp_macro/fetchers.py @@ -5,6 +5,16 @@ from typing import Any import httpx from mcp_common.http import async_client +from mcp_macro.cot import parse_disagg_row, parse_tff_row +from mcp_macro.cot_contracts import ( + ALL_DISAGG_SYMBOLS, + ALL_TFF_SYMBOLS, + CFTC_BASE_URL, + DISAGG_DATASET_ID, + SYMBOL_TO_CFTC_CODE_DISAGG, + SYMBOL_TO_CFTC_CODE_TFF, + TFF_DATASET_ID, +) FRED_BASE = "https://api.stlouisfed.org/fred/series/observations" FINNHUB_CALENDAR = "https://finnhub.io/api/v1/calendar/economic" @@ -609,3 +619,48 @@ async def fetch_market_overview() -> dict[str, Any]: _MARKET_CACHE["data"] = out _MARKET_CACHE["ts"] = now return out + + +_COT_TTL = 3600.0 # 1h +_COT_CACHE: dict[tuple[str, str, int], dict[str, Any]] = {} +_COT_CACHE_TS: dict[tuple[str, str, int], float] = {} + + +async def fetch_cot_tff(symbol: str, lookback_weeks: int = 52) -> dict[str, Any]: + """Fetch COT TFF report per simbolo equity/financial. Returns ASC by date.""" + import time + + symbol = symbol.upper() + if symbol not in SYMBOL_TO_CFTC_CODE_TFF: + return {"error": "unknown_symbol", "available": ALL_TFF_SYMBOLS} + + key = (symbol, "tff", 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_TFF[symbol] + url = f"{CFTC_BASE_URL}/{TFF_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": "tff", "rows": [], "error": "cftc_unavailable"} + raw_rows = resp.json() or [] + parsed = [parse_tff_row(r) for r in raw_rows] + parsed.sort(key=lambda r: r["report_date"]) # ASC by date + out = { + "symbol": symbol, + "report_type": "tff", + "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 e255cc2..6bb279c 100644 --- a/services/mcp-macro/tests/test_fetchers.py +++ b/services/mcp-macro/tests/test_fetchers.py @@ -260,3 +260,61 @@ async def test_breakeven_high_inflation(httpx_mock: pytest_httpx.HTTPXMock): out = await fetch_breakeven_inflation(fred_api_key="k") assert out["interpretation"] == "high_inflation_expected" + +@pytest.mark.asyncio +async def test_fetch_cot_tff_happy_path(httpx_mock: pytest_httpx.HTTPXMock): + from mcp_macro.fetchers import fetch_cot_tff + httpx_mock.add_response( + url=httpx.URL( + "https://publicreporting.cftc.gov/resource/gpe5-46if.json", + params={ + "cftc_contract_market_code": "13874A", + "$order": "report_date_as_yyyy_mm_dd DESC", + "$limit": "52", + }, + ), + json=[ + { + "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", + }, + { + "report_date_as_yyyy_mm_dd": "2026-04-15T00:00:00.000", + "dealer_positions_long_all": "11000", + "dealer_positions_short_all": "22000", + "asset_mgr_positions_long": "640000", + "asset_mgr_positions_short": "210000", + "lev_money_positions_long": "110000", + "lev_money_positions_short": "320000", + "other_rept_positions_long": "48000", + "other_rept_positions_short": "52000", + "open_interest_all": "2480000", + }, + ], + ) + out = await fetch_cot_tff("ES", lookback_weeks=52) + assert out["symbol"] == "ES" + assert out["report_type"] == "tff" + assert len(out["rows"]) == 2 + # Ordering ASC by date (oldest first) + assert out["rows"][0]["report_date"] == "2026-04-15" + assert out["rows"][1]["report_date"] == "2026-04-22" + assert out["rows"][1]["lev_funds_net"] == -250000 + assert "data_timestamp" in out + + +@pytest.mark.asyncio +async def test_fetch_cot_tff_unknown_symbol(): + from mcp_macro.fetchers import fetch_cot_tff + out = await fetch_cot_tff("INVALID", lookback_weeks=52) + assert out.get("error") == "unknown_symbol" + assert "ES" in out.get("available", []) +