feat(mcp-macro): fetch_cot_tff async fetcher with cache
This commit is contained in:
@@ -5,6 +5,16 @@ from typing import Any
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from mcp_common.http import async_client
|
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"
|
FRED_BASE = "https://api.stlouisfed.org/fred/series/observations"
|
||||||
FINNHUB_CALENDAR = "https://finnhub.io/api/v1/calendar/economic"
|
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["data"] = out
|
||||||
_MARKET_CACHE["ts"] = now
|
_MARKET_CACHE["ts"] = now
|
||||||
return out
|
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
|
||||||
|
|||||||
@@ -260,3 +260,61 @@ async def test_breakeven_high_inflation(httpx_mock: pytest_httpx.HTTPXMock):
|
|||||||
out = await fetch_breakeven_inflation(fred_api_key="k")
|
out = await fetch_breakeven_inflation(fred_api_key="k")
|
||||||
assert out["interpretation"] == "high_inflation_expected"
|
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", [])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user