feat: 15 nuovi indicatori quant (common + deribit + bybit + macro + sentiment)
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>
This commit is contained in:
@@ -159,6 +159,100 @@ async def fetch_treasury_yields() -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def yield_curve_metrics(yields: dict[str, float | None]) -> dict[str, Any]:
|
||||
"""Slope + convexity da curva yields (us2y, us5y, us10y, us30y).
|
||||
Convexity (butterfly): 2*us10y - us2y - us30y. >0 = curva concava.
|
||||
"""
|
||||
y2 = yields.get("us2y")
|
||||
y5 = yields.get("us5y")
|
||||
y10 = yields.get("us10y")
|
||||
y30 = yields.get("us30y")
|
||||
|
||||
slope_2y10y = (y10 - y2) if (y2 is not None and y10 is not None) else None
|
||||
slope_5y30y = (y30 - y5) if (y5 is not None and y30 is not None) else None
|
||||
butterfly_2_10_30 = (2 * y10 - y2 - y30) if (y2 is not None and y10 is not None and y30 is not None) else None
|
||||
|
||||
regime = "unknown"
|
||||
if slope_2y10y is not None:
|
||||
if slope_2y10y >= 0.5:
|
||||
regime = "steep"
|
||||
elif slope_2y10y > 0.1:
|
||||
regime = "normal"
|
||||
elif slope_2y10y > -0.1:
|
||||
regime = "flat"
|
||||
else:
|
||||
regime = "inverted"
|
||||
|
||||
return {
|
||||
"slope_2y10y": round(slope_2y10y, 3) if slope_2y10y is not None else None,
|
||||
"slope_5y30y": round(slope_5y30y, 3) if slope_5y30y is not None else None,
|
||||
"butterfly_2_10_30": round(butterfly_2_10_30, 3) if butterfly_2_10_30 is not None else None,
|
||||
"regime": regime,
|
||||
}
|
||||
|
||||
|
||||
async def fetch_yield_curve_slope() -> dict[str, Any]:
|
||||
"""Curve slope/convexity metrics su treasury yields correnti."""
|
||||
base = await fetch_treasury_yields()
|
||||
metrics = yield_curve_metrics(base.get("yields") or {})
|
||||
return {
|
||||
"yields": base.get("yields"),
|
||||
**metrics,
|
||||
"data_timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_breakeven_inflation(fred_api_key: str = "") -> dict[str, Any]:
|
||||
"""Breakeven inflation rate via FRED:
|
||||
- T10YIE (10Y breakeven, market expectation 10Y inflation)
|
||||
- T5YIE (5Y breakeven)
|
||||
- T5YIFR (5Y forward 5Y forward inflation expectation)
|
||||
"""
|
||||
if not fred_api_key:
|
||||
return {"error": "No FRED API key configured", "breakevens": {}}
|
||||
|
||||
series_map = {
|
||||
"be_5y": "T5YIE",
|
||||
"be_10y": "T10YIE",
|
||||
"be_5y5y_forward": "T5YIFR",
|
||||
}
|
||||
out: dict[str, float | None] = {}
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
for name, series_id in series_map.items():
|
||||
resp = await client.get(
|
||||
FRED_BASE,
|
||||
params={
|
||||
"series_id": series_id,
|
||||
"api_key": fred_api_key,
|
||||
"file_type": "json",
|
||||
"sort_order": "desc",
|
||||
"limit": 1,
|
||||
},
|
||||
)
|
||||
data = resp.json()
|
||||
obs = data.get("observations", [])
|
||||
try:
|
||||
out[name] = float(obs[0]["value"]) if obs and obs[0]["value"] != "." else None
|
||||
except (ValueError, IndexError, KeyError):
|
||||
out[name] = None
|
||||
|
||||
interpretation = "unknown"
|
||||
be10 = out.get("be_10y")
|
||||
if be10 is not None:
|
||||
if be10 > 3.0:
|
||||
interpretation = "high_inflation_expected"
|
||||
elif be10 < 1.5:
|
||||
interpretation = "low_inflation_expected"
|
||||
else:
|
||||
interpretation = "anchored"
|
||||
|
||||
return {
|
||||
"breakevens": out,
|
||||
"interpretation": interpretation,
|
||||
"data_timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_equity_futures() -> dict[str, Any]:
|
||||
"""Fetch ES/NQ/YM/RTY futures con session detection."""
|
||||
tickers = [("es", "ES=F"), ("nq", "NQ=F"), ("ym", "YM=F"), ("rty", "RTY=F")]
|
||||
|
||||
@@ -10,11 +10,13 @@ from pydantic import BaseModel
|
||||
|
||||
from mcp_macro.fetchers import (
|
||||
fetch_asset_price,
|
||||
fetch_breakeven_inflation,
|
||||
fetch_economic_indicators,
|
||||
fetch_equity_futures,
|
||||
fetch_macro_calendar,
|
||||
fetch_market_overview,
|
||||
fetch_treasury_yields,
|
||||
fetch_yield_curve_slope,
|
||||
)
|
||||
|
||||
# --- Body models ---
|
||||
@@ -47,6 +49,14 @@ class GetEquityFuturesReq(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class GetYieldCurveSlopeReq(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class GetBreakevenInflationReq(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
# --- ACL helper ---
|
||||
|
||||
def _check(principal: Principal, *, core: bool = False, observer: bool = False) -> None:
|
||||
@@ -115,6 +125,20 @@ def create_app(*, fred_api_key: str = "", finnhub_api_key: str = "", token_store
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_equity_futures()
|
||||
|
||||
@app.post("/tools/get_yield_curve_slope", tags=["reads"])
|
||||
async def t_get_yield_curve_slope(
|
||||
body: GetYieldCurveSlopeReq, principal: Principal = Depends(require_principal)
|
||||
):
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_yield_curve_slope()
|
||||
|
||||
@app.post("/tools/get_breakeven_inflation", tags=["reads"])
|
||||
async def t_get_breakeven_inflation(
|
||||
body: GetBreakevenInflationReq, principal: Principal = Depends(require_principal)
|
||||
):
|
||||
_check(principal, core=True, observer=True)
|
||||
return await fetch_breakeven_inflation(fred_api_key=fred_api_key)
|
||||
|
||||
# ───── MCP endpoint (/mcp) — bridge verso /tools/* ─────
|
||||
port = int(os.environ.get("PORT", "9013"))
|
||||
mount_mcp_endpoint(
|
||||
@@ -130,6 +154,8 @@ def create_app(*, fred_api_key: str = "", finnhub_api_key: str = "", token_store
|
||||
{"name": "get_asset_price", "description": "Prezzo cross-asset: WTI, DXY, SPX, VIX, yields, FX, ecc."},
|
||||
{"name": "get_treasury_yields", "description": "Curva US Treasury 2y/5y/10y/30y + shape detection."},
|
||||
{"name": "get_equity_futures", "description": "Futures ES/NQ/YM/RTY con session status."},
|
||||
{"name": "get_yield_curve_slope", "description": "Slope 2y10y/5y30y + butterfly + regime (steep/normal/flat/inverted)."},
|
||||
{"name": "get_breakeven_inflation", "description": "Breakeven inflation 5Y/10Y + 5y5y forward (FRED T5YIE/T10YIE/T5YIFR)."},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ 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 ---
|
||||
@@ -183,3 +185,78 @@ async def test_market_overview_happy(httpx_mock: pytest_httpx.HTTPXMock):
|
||||
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user