a13e3fe045
Common (mcp_common): - indicators.py: vol_cone, hurst_exponent, half_life_mean_reversion, garch11_forecast, autocorrelation, rolling_sharpe, var_cvar - options.py (nuovo): oi_weighted_skew, smile_asymmetry, atm_vs_wings_vol, dealer_gamma_profile, vanna_charm_aggregate - microstructure.py (nuovo): orderbook_imbalance (ratio + microprice + slope) - stats.py (nuovo): cointegration_test Engle-Granger + ADF helper Deribit (+6 tool MCP): - get_dealer_gamma_profile (net dealer gamma + flip level) - get_vanna_charm (vanna/charm aggregati pesati OI) - get_oi_weighted_skew, get_smile_asymmetry, get_atm_vs_wings_vol - get_orderbook_imbalance Bybit (+2 tool MCP): - get_orderbook_imbalance, get_basis_term_structure (futures dated curve) Macro (+2 tool MCP): - get_yield_curve_slope (2y10y/5y30y + butterfly + regime) - get_breakeven_inflation (FRED T5YIE/T10YIE/T5YIFR) Sentiment (+3 tool MCP): - get_funding_arb_spread (opportunità arb compatte annualizzate) - get_liquidation_heatmap (heuristic da OI delta + funding extreme, no feed paid Coinglass) - get_cointegration_pairs (Engle-Granger su coppie crypto Binance hourly) Tutto in TDD pure-Python (no numpy/scipy in mcp_common). README aggiornato con elenco completo. 442 test totali verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
263 lines
8.8 KiB
Python
263 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC
|
|
|
|
import httpx
|
|
import pytest
|
|
import pytest_httpx
|
|
from mcp_macro.fetchers import (
|
|
fetch_breakeven_inflation,
|
|
fetch_economic_indicators,
|
|
fetch_macro_calendar,
|
|
fetch_market_overview,
|
|
yield_curve_metrics,
|
|
)
|
|
|
|
# --- fetch_economic_indicators ---
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_economic_indicators_no_key():
|
|
result = await fetch_economic_indicators(fred_api_key="")
|
|
assert "error" in result
|
|
assert result["error"] == "No FRED API key configured"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_economic_indicators_happy_path(httpx_mock: pytest_httpx.HTTPXMock):
|
|
for series_id in ("FEDFUNDS", "CPIAUCSL", "UNRATE", "DGS10"):
|
|
httpx_mock.add_response(
|
|
url=httpx.URL(
|
|
"https://api.stlouisfed.org/fred/series/observations",
|
|
params={
|
|
"series_id": series_id,
|
|
"api_key": "testkey",
|
|
"file_type": "json",
|
|
"sort_order": "desc",
|
|
"limit": "1",
|
|
},
|
|
),
|
|
json={"observations": [{"value": "5.25"}]},
|
|
)
|
|
result = await fetch_economic_indicators(fred_api_key="testkey")
|
|
assert result["fed_rate"] == 5.25
|
|
assert result["cpi"] == 5.25
|
|
assert result["unemployment"] == 5.25
|
|
assert result["us10y_yield"] == 5.25
|
|
assert "updated_at" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_economic_indicators_filter(httpx_mock: pytest_httpx.HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url=httpx.URL(
|
|
"https://api.stlouisfed.org/fred/series/observations",
|
|
params={
|
|
"series_id": "FEDFUNDS",
|
|
"api_key": "k",
|
|
"file_type": "json",
|
|
"sort_order": "desc",
|
|
"limit": "1",
|
|
},
|
|
),
|
|
json={"observations": [{"value": "5.33"}]},
|
|
)
|
|
result = await fetch_economic_indicators(fred_api_key="k", indicators=["fed_rate"])
|
|
assert "fed_rate" in result
|
|
assert "cpi" not in result
|
|
|
|
|
|
# --- fetch_macro_calendar ---
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_macro_calendar_forex_factory_happy(httpx_mock: pytest_httpx.HTTPXMock):
|
|
from datetime import datetime, timedelta
|
|
|
|
future = (datetime.now(UTC) + timedelta(days=1)).isoformat()
|
|
httpx_mock.add_response(
|
|
url="https://nfs.faireconomy.media/ff_calendar_thisweek.json",
|
|
json=[
|
|
{
|
|
"date": future,
|
|
"title": "CPI",
|
|
"country": "US",
|
|
"impact": "High",
|
|
"forecast": "3.0%",
|
|
"previous": "3.2%",
|
|
}
|
|
],
|
|
)
|
|
result = await fetch_macro_calendar()
|
|
assert "events" in result
|
|
assert len(result["events"]) >= 1
|
|
assert result["events"][0]["name"] == "CPI"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_macro_calendar_no_source(httpx_mock: pytest_httpx.HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url="https://nfs.faireconomy.media/ff_calendar_thisweek.json",
|
|
status_code=500,
|
|
)
|
|
result = await fetch_macro_calendar(finnhub_api_key="")
|
|
assert result == {"events": [], "note": "No calendar source available"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.httpx_mock(assert_all_responses_were_requested=False, assert_all_requests_were_expected=False)
|
|
async def test_macro_calendar_finnhub_fallback(httpx_mock: pytest_httpx.HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url="https://nfs.faireconomy.media/ff_calendar_thisweek.json",
|
|
status_code=500,
|
|
)
|
|
|
|
def dispatch(request: httpx.Request) -> httpx.Response:
|
|
if "finnhub.io" in str(request.url):
|
|
return httpx.Response(
|
|
200,
|
|
json=[{"date": "2024-01-15", "event": "FOMC", "importance": "high", "forecast": "", "prev": ""}],
|
|
)
|
|
return httpx.Response(500)
|
|
|
|
httpx_mock.add_callback(dispatch)
|
|
result = await fetch_macro_calendar(finnhub_api_key="fkey")
|
|
assert "events" in result
|
|
assert result["events"][0]["name"] == "FOMC"
|
|
|
|
|
|
# --- fetch_market_overview ---
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_market_overview_happy(httpx_mock: pytest_httpx.HTTPXMock):
|
|
httpx_mock.add_response(
|
|
url="https://api.coingecko.com/api/v3/global",
|
|
json={
|
|
"data": {
|
|
"market_cap_percentage": {"btc": 52.3},
|
|
"total_market_cap": {"usd": 2_000_000_000_000},
|
|
}
|
|
},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=httpx.URL(
|
|
"https://api.coingecko.com/api/v3/simple/price",
|
|
params={"ids": "bitcoin,ethereum", "vs_currencies": "usd"},
|
|
),
|
|
json={"bitcoin": {"usd": 65000}, "ethereum": {"usd": 3500}},
|
|
)
|
|
import re as _re
|
|
httpx_mock.add_response(
|
|
url=_re.compile(
|
|
r"https://www\.deribit\.com/api/v2/public/get_volatility_index_data\?currency=BTC.*"
|
|
),
|
|
json={"result": {"data": [[1, 50, 52, 49, 51.5]], "continuation": None}},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_re.compile(
|
|
r"https://www\.deribit\.com/api/v2/public/get_volatility_index_data\?currency=ETH.*"
|
|
),
|
|
json={"result": {"data": [[1, 60, 62, 59, 61.2]], "continuation": None}},
|
|
)
|
|
import re as _re
|
|
httpx_mock.add_response(
|
|
url=_re.compile(r"https://query1\.finance\.yahoo\.com/v8/finance/chart/\^GSPC.*"),
|
|
json={"chart": {"result": [{"meta": {"regularMarketPrice": 5830.12}}]}},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_re.compile(r"https://query1\.finance\.yahoo\.com/v8/finance/chart/GC[%=].*"),
|
|
json={"chart": {"result": [{"meta": {"regularMarketPrice": 2412.5}}]}},
|
|
)
|
|
httpx_mock.add_response(
|
|
url=_re.compile(r"https://query1\.finance\.yahoo\.com/v8/finance/chart/\^VIX.*"),
|
|
json={"chart": {"result": [{"meta": {"regularMarketPrice": 18.3}}]}},
|
|
)
|
|
# Clear module cache to force fresh fetch
|
|
from mcp_macro import fetchers as _f
|
|
_f._MARKET_CACHE["data"] = None
|
|
_f._MARKET_CACHE["ts"] = 0.0
|
|
result = await fetch_market_overview()
|
|
assert result["btc_dominance"] == 52.3
|
|
assert result["btc_price"] == 65000
|
|
assert result["eth_price"] == 3500
|
|
assert result["total_market_cap"] == 2_000_000_000_000
|
|
assert result["dvol_btc"] == 51.5
|
|
assert result["dvol_eth"] == 61.2
|
|
assert result["sp500"] == 5830.12
|
|
assert result["gold"] == 2412.5
|
|
assert result["vix"] == 18.3
|
|
assert "data_timestamp" in result
|
|
|
|
|
|
# --- yield_curve_metrics ---
|
|
|
|
def test_yield_curve_metrics_normal_curve():
|
|
out = yield_curve_metrics({"us2y": 4.0, "us5y": 4.2, "us10y": 4.5, "us30y": 4.8})
|
|
assert out["slope_2y10y"] == 0.5
|
|
assert out["slope_5y30y"] == 0.6
|
|
assert out["regime"] == "steep"
|
|
# butterfly: 2*4.5 - 4.0 - 4.8 = 0.2
|
|
assert out["butterfly_2_10_30"] == 0.2
|
|
|
|
|
|
def test_yield_curve_metrics_inverted():
|
|
out = yield_curve_metrics({"us2y": 5.5, "us5y": 5.0, "us10y": 4.5, "us30y": 4.3})
|
|
assert out["slope_2y10y"] == -1.0
|
|
assert out["regime"] == "inverted"
|
|
|
|
|
|
def test_yield_curve_metrics_partial_data():
|
|
out = yield_curve_metrics({"us10y": 4.5})
|
|
assert out["slope_2y10y"] is None
|
|
assert out["regime"] == "unknown"
|
|
|
|
|
|
# --- fetch_breakeven_inflation ---
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_breakeven_no_key():
|
|
out = await fetch_breakeven_inflation(fred_api_key="")
|
|
assert "error" in out
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_breakeven_happy_path(httpx_mock: pytest_httpx.HTTPXMock):
|
|
for series_id, val in [("T5YIE", "2.3"), ("T10YIE", "2.5"), ("T5YIFR", "2.7")]:
|
|
httpx_mock.add_response(
|
|
url=httpx.URL(
|
|
"https://api.stlouisfed.org/fred/series/observations",
|
|
params={
|
|
"series_id": series_id,
|
|
"api_key": "k",
|
|
"file_type": "json",
|
|
"sort_order": "desc",
|
|
"limit": "1",
|
|
},
|
|
),
|
|
json={"observations": [{"value": val}]},
|
|
)
|
|
out = await fetch_breakeven_inflation(fred_api_key="k")
|
|
assert out["breakevens"]["be_5y"] == 2.3
|
|
assert out["breakevens"]["be_10y"] == 2.5
|
|
assert out["breakevens"]["be_5y5y_forward"] == 2.7
|
|
assert out["interpretation"] == "anchored"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_breakeven_high_inflation(httpx_mock: pytest_httpx.HTTPXMock):
|
|
for series_id in ("T5YIE", "T10YIE", "T5YIFR"):
|
|
httpx_mock.add_response(
|
|
url=httpx.URL(
|
|
"https://api.stlouisfed.org/fred/series/observations",
|
|
params={
|
|
"series_id": series_id,
|
|
"api_key": "k",
|
|
"file_type": "json",
|
|
"sort_order": "desc",
|
|
"limit": "1",
|
|
},
|
|
),
|
|
json={"observations": [{"value": "3.5"}]},
|
|
)
|
|
out = await fetch_breakeven_inflation(fred_api_key="k")
|
|
assert out["interpretation"] == "high_inflation_expected"
|
|
|