diff --git a/app/__main__.py b/app/__main__.py index ca261e98..df60de3a 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -8,7 +8,6 @@ from fastapi.staticfiles import StaticFiles from fluid.utils import log -from app.api.mcp import create_mcp as _create_mcp from app.utils.paths import APP_PATH from quantflow import __version__ @@ -56,17 +55,15 @@ async def api_redoc() -> HTMLResponse: title="Quantflow API", ) - api.include_router(cointegration_router) - api.include_router(heston_router) - api.include_router(hurst_router) - api.include_router(rates_router) - api.include_router(sampling_router) - api.include_router(smoother_router) - api.include_router(volatility_router) + api.include_router(cointegration_router, tags=["timeseries"]) + api.include_router(heston_router, tags=["pricing"]) + api.include_router(hurst_router, tags=["statistics"]) + api.include_router(rates_router, tags=["pricing"]) + api.include_router(sampling_router, tags=["statistics"]) + api.include_router(smoother_router, tags=["timeseries"]) + api.include_router(volatility_router, tags=["pricing"]) app.include_router(api) app.include_router(status_router, include_in_schema=False) - if _create_mcp is not None: - api.mount("/mcp", _create_mcp().streamable_http_app()) examples_dir = APP_PATH / "examples" if examples_dir.is_dir(): app.mount( diff --git a/app/api/cointegration.py b/app/api/cointegration.py index 53f96c4d..f07cd469 100644 --- a/app/api/cointegration.py +++ b/app/api/cointegration.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field from statsmodels.tsa.vector_ar.vecm import coint_johansen +from app.api.docs import load_description from quantflow.data.fmp import FMP from .deps import FMPDep, RedisCache, RedisDep @@ -20,13 +21,17 @@ class CointegrationResponse(BaseModel): ) -@cointegration_router.get("/cointegration") +@cointegration_router.get( + "/cointegration", + summary="BTC/ETH/SOL cointegration", + description=load_description("cointegration.md"), +) async def cointegration( fmp: FMPDep, redis: RedisDep, frequency: FMP.freq = Query( FMP.freq.daily, - description="Price frequency", + description="Price sampling frequency.", ), ) -> CointegrationResponse: cache = RedisCache( diff --git a/app/api/docs/__init__.py b/app/api/docs/__init__.py new file mode 100644 index 00000000..49ae0c54 --- /dev/null +++ b/app/api/docs/__init__.py @@ -0,0 +1,7 @@ +from pathlib import Path + +_DOCS_PATH = Path(__file__).parent + + +def load_description(filename: str) -> str: + return (_DOCS_PATH / filename).read_text(encoding="utf-8") diff --git a/app/api/docs/cointegration.md b/app/api/docs/cointegration.md new file mode 100644 index 00000000..d6988735 --- /dev/null +++ b/app/api/docs/cointegration.md @@ -0,0 +1,9 @@ +Johansen cointegration analysis of BTC, ETH, and SOL log prices. + +Returns the mean-reverting spread (residuals) and the cointegrating vector +(eigenvector for the largest eigenvalue of the Johansen test). + +Log prices are standardised before the test; the returned cointegrating vector +is rescaled back to the original units and normalised to unit length. +A residual near zero means the spread is close to its long-run equilibrium. +Large positive or negative residuals signal potential mean-reversion trades. diff --git a/app/api/docs/double_exponential_sampling.md b/app/api/docs/double_exponential_sampling.md new file mode 100644 index 00000000..14681df4 --- /dev/null +++ b/app/api/docs/double_exponential_sampling.md @@ -0,0 +1,11 @@ +Sample from a double exponential (asymmetric Laplace) distribution and compare +the empirical PDF to both the analytical PDF and the PDF recovered from the +characteristic function. + +The double exponential distribution models asymmetric jump sizes in jump-diffusion +models. The asymmetry is controlled by `log_kappa`: positive values produce +heavier right tails (upward jumps more likely), negative values heavier left tails. +At `log_kappa = 0` the distribution is symmetric. + +The characteristic function inversion provides an independent check that the +analytical and numerical PDFs agree. diff --git a/app/api/docs/gaussian_sampling.md b/app/api/docs/gaussian_sampling.md new file mode 100644 index 00000000..b47fbfaa --- /dev/null +++ b/app/api/docs/gaussian_sampling.md @@ -0,0 +1,12 @@ +Sample from a Vasicek mean-reverting Gaussian process and compare the empirical +distribution to the analytical marginal PDF. + +The process is run for one unit of time with 1000 time steps. The terminal +distribution is binned into 50 bins and compared against the analytical Gaussian PDF. + +A Kolmogorov-Smirnov test is included to quantify goodness of fit. +A high p-value (e.g. above 0.05) means the simulation is statistically consistent +with the analytical distribution. + +Antithetic variates halve the variance of Monte Carlo estimates by pairing each +draw with its negative, without additional model evaluations. diff --git a/app/api/docs/heston_vol_surface.md b/app/api/docs/heston_vol_surface.md new file mode 100644 index 00000000..35500003 --- /dev/null +++ b/app/api/docs/heston_vol_surface.md @@ -0,0 +1,15 @@ +Generate a theoretical implied volatility surface using a stochastic volatility model. + +Two models are available: + +- **jd** (Jump Diffusion): a Variance-Gamma-style model with a double-exponential + jump distribution. Controlled by `vol`, `jump_fraction`, `jump_intensity`, and + `jump_asymmetry`. The `kappa` and `rho` parameters are ignored. + +- **hj** (Heston with Jumps): the Heston stochastic volatility model augmented with + double-exponential jumps. All parameters are used. + +The surface is returned as a grid: 10 maturities evenly spaced from 0.1 to 1.0 years, +and 51 moneyness values from -0.5 to 0.5 (log-moneyness scaled by sqrt(ttm)). +Implied volatilities that cannot be computed (e.g. deep in/out of the money) are +returned as 0. diff --git a/app/api/docs/hurst_vasicek.md b/app/api/docs/hurst_vasicek.md new file mode 100644 index 00000000..2aa1a4af --- /dev/null +++ b/app/api/docs/hurst_vasicek.md @@ -0,0 +1,9 @@ +Simulate a Vasicek mean-reverting process and estimate its Hurst exponent. + +The Vasicek process reverts to zero at speed `kappa`. Higher values of `kappa` +produce stronger mean reversion and a lower Hurst exponent (well below 0.5). +Low values of `kappa` approach Brownian motion (Hurst near 0.5). + +The Hurst exponent is estimated from both realized variance and three OHLC-based +estimators (Parkinson, Garman-Klass, Rogers-Satchell) sampled at periods of +10min, 20min, 30min, and 1h. diff --git a/app/api/docs/hurst_wiener.md b/app/api/docs/hurst_wiener.md new file mode 100644 index 00000000..b11aa894 --- /dev/null +++ b/app/api/docs/hurst_wiener.md @@ -0,0 +1,15 @@ +Simulate a Wiener (Brownian motion) process over one day at one-second resolution +and estimate its Hurst exponent using multiple methods. + +The Hurst exponent measures long-range dependence in a time series. +A value of 0.5 indicates pure Brownian motion (no memory). +Values above 0.5 indicate trending behaviour; values below 0.5 indicate mean reversion. + +Three OHLC-based range estimators are computed across multiple sampling periods: + +- **Parkinson**: uses high-low range. +- **Garman-Klass**: uses open, high, low, close. +- **Rogers-Satchell**: drift-adjusted version of Garman-Klass. + +The Hurst exponent for each estimator is derived from the slope of log(variance) +vs log(sampling period). diff --git a/app/api/docs/poisson_sampling.md b/app/api/docs/poisson_sampling.md new file mode 100644 index 00000000..e3ed62c6 --- /dev/null +++ b/app/api/docs/poisson_sampling.md @@ -0,0 +1,10 @@ +Sample from a Poisson jump process and compare the empirical jump count distribution +to the analytical Poisson PMF. + +The process is run for one unit of time with 1000 time steps. Jump counts are +binned and compared against the analytical distribution using a chi-squared +goodness-of-fit test. Tail bins with expected count below 5 are merged to satisfy +the chi-squared validity requirement. + +A high p-value (e.g. above 0.05) means the simulation is statistically consistent +with the analytical Poisson distribution. diff --git a/app/api/docs/supersmoother.md b/app/api/docs/supersmoother.md new file mode 100644 index 00000000..5fc90181 --- /dev/null +++ b/app/api/docs/supersmoother.md @@ -0,0 +1,12 @@ +Apply SuperSmoother and EWMA filters to two years of historical daily price data +for a given symbol. + +SuperSmoother is an adaptive low-pass filter designed by John Ehlers. It suppresses +high-frequency noise more aggressively than EWMA while introducing less lag at +trend inflections. + +Both filters use the same `period` parameter, making it easy to compare their +smoothing characteristics on the same data. Shorter periods follow price more +closely; longer periods produce smoother output with more lag. + +Supported symbols include crypto pairs (e.g. BTCUSD, ETHUSD) and US equities. diff --git a/app/api/docs/volatility_surface.md b/app/api/docs/volatility_surface.md new file mode 100644 index 00000000..08327113 --- /dev/null +++ b/app/api/docs/volatility_surface.md @@ -0,0 +1,21 @@ +Fetch the live implied volatility surface for a crypto or equity asset and return +the calibrated surface with discount curves and forward prices. + +Data sources: + +- **BTC, ETH**: live option chain from Deribit (inverse, crypto-quoted contracts). +- **SPY, AAPL, NVDA**: option chain from Yahoo Finance (standard equity contracts). + +The surface is calibrated using Black-Scholes implied volatilities. Outlier options +(bid-ask spread too wide, or IV outside plausible bounds) are disabled before +the surface is returned. + +Two discount curves are calibrated from the option data: + +- **quote_curve**: discount curve for the numeraire (USD for equity, crypto for inverse). +- **asset_curve**: discount curve for the underlying asset. + +The forward curve and per-maturity implied forwards from put-call parity are also +included, which are useful for detecting curve arbitrage or funding dislocations. + +Responses are cached; live data may be up to a few minutes old. diff --git a/app/api/docs/yield_curve.md b/app/api/docs/yield_curve.md new file mode 100644 index 00000000..f7959522 --- /dev/null +++ b/app/api/docs/yield_curve.md @@ -0,0 +1,17 @@ +Fit a yield curve model to a set of (time-to-maturity, rate) pairs and return +the calibrated curve plus interpolated rates. + +Four curve types are supported: + +- **nelson_siegel**: the Nelson-Siegel parametric model, well suited for smooth + yield curves with a single hump. Good default choice. +- **cir_curve**: Cox-Ingersoll-Ross model, a mean-reverting short-rate model. +- **vasicek_curve**: Vasicek model, a Gaussian mean-reverting short-rate model. +- **no_discount**: flat discount curve (no discounting), useful as a baseline. + +Rates should be continuously compounded and expressed as decimals (e.g. 0.045 for 4.5%). +Time to maturity is in years (e.g. 0.25 for 3 months, 2.0 for 2 years). + +If `max_ttm` is provided, the response includes `num_points` interpolated rates +evenly spaced up to that maturity, which is useful for plotting a smooth curve. +Otherwise, rates are returned at the input maturities only. diff --git a/app/api/heston.py b/app/api/heston.py index 7d70914d..ffdcedbe 100644 --- a/app/api/heston.py +++ b/app/api/heston.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from app.api.docs import load_description from quantflow.options.pricer import OptionPricer from quantflow.sp.heston import HestonJ from quantflow.sp.jump_diffusion import JumpDiffusion @@ -18,7 +19,11 @@ class VolSurfaceGridResponse(BaseModel): ) -@heston_router.get("/heston-vol-surface") +@heston_router.get( + "/heston-vol-surface", + summary="Theoretical implied volatility surface", + description=load_description("heston_vol_surface.md"), +) async def heston_vol_surface( model: str = Query( "jd", diff --git a/app/api/hurst.py b/app/api/hurst.py index be6bdd13..233e196d 100644 --- a/app/api/hurst.py +++ b/app/api/hurst.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from app.api.docs import load_description from quantflow.sp.ou import Vasicek from quantflow.sp.wiener import WienerProcess from quantflow.ta.ohlc import OHLC @@ -86,7 +87,11 @@ def _ohlc_hurst(df: pd.DataFrame, serie: str, periods: tuple) -> dict[str, float return result -@hurst_router.get("/hurst-wiener") +@hurst_router.get( + "/hurst-wiener", + summary="Hurst exponent of a Wiener process", + description=load_description("hurst_wiener.md"), +) async def hurst_wiener( sigma: float = Query(2.0, description="Wiener process volatility", ge=0.1, le=10), ) -> HurstWienerResponse: @@ -138,7 +143,11 @@ async def hurst_wiener( ) -@hurst_router.get("/hurst-vasicek") +@hurst_router.get( + "/hurst-vasicek", + summary="Hurst exponent of a Vasicek process", + description=load_description("hurst_vasicek.md"), +) async def hurst_vasicek( kappa: float = Query(10.0, description="Mean reversion speed", ge=1.0, le=500.0), ) -> HurstVasicekResponse: diff --git a/app/api/mcp.py b/app/api/mcp.py deleted file mode 100644 index 8854a222..00000000 --- a/app/api/mcp.py +++ /dev/null @@ -1,90 +0,0 @@ -"""MCP server for quantflow - crypto volatility tools served via HTTP.""" - -from pathlib import Path - -from mcp.server.fastmcp import FastMCP - -from quantflow.data.deribit import Deribit, InstrumentKind - - -def create_mcp() -> FastMCP: - mcp = FastMCP("quantflow") - - @mcp.tool() - async def crypto_instruments(currency: str, kind: str = "spot") -> str: - """List available instruments for a cryptocurrency on Deribit. - - Args: - currency: Cryptocurrency symbol e.g. BTC, ETH - kind: Instrument kind - spot, future, option (default: spot) - """ - async with Deribit() as client: - data = await client.get_instruments( - currency=currency, kind=InstrumentKind(kind) - ) - if not data: - return f"No instruments found for {currency} ({kind})" - rows = "\n".join(str(d) for d in data[:20]) - return f"Instruments for {currency} ({kind}):\n{rows}" - - @mcp.tool() - async def crypto_historical_volatility(currency: str) -> str: - """Get historical volatility for a cryptocurrency from Deribit. - - Args: - currency: Cryptocurrency symbol e.g. BTC, ETH - """ - async with Deribit() as client: - df = await client.get_volatility(currency) - if df.empty: - return f"No volatility data for {currency}" - return df.to_csv(index=False) - - @mcp.tool() - async def crypto_term_structure(currency: str) -> str: - """Get the volatility term structure for a cryptocurrency from Deribit. - - Args: - currency: Cryptocurrency symbol e.g. BTC, ETH - """ - async with Deribit() as client: - loader = await client.volatility_surface_loader(currency) - vs = loader.surface() - ts = vs.term_structure().round({"ttm": 4}) - return ts.to_csv(index=False) - - @mcp.tool() - async def crypto_implied_volatility(currency: str, maturity_index: int = -1) -> str: - """Get the implied volatility surface for a cryptocurrency from Deribit. - - Args: - currency: Cryptocurrency symbol e.g. BTC, ETH - maturity_index: Maturity index (-1 for all maturities) - """ - async with Deribit() as client: - loader = await client.volatility_surface_loader(currency) - vs = loader.surface() - index = None if maturity_index < 0 else maturity_index - vs.bs(index=index) - df = vs.options_df(index=index) - df["iv"] = df["iv"].map("{:.2%}".format) - return df.to_csv(index=False) - - @mcp.tool() - async def vol_surface_snapshot(currency: str, path: str) -> str: - """Fetch a live volatility surface from Deribit and save it as a JSON snapshot. - - Args: - currency: Cryptocurrency symbol e.g. BTC, ETH - path: File path to write the snapshot to - """ - async with Deribit() as client: - loader = await client.volatility_surface_loader(currency) - vs = loader.surface() - vs.bs() - vs.disable_outliers() - inputs = vs.inputs(converged=True) - Path(path).write_text(inputs.model_dump_json(indent=2)) - return f"Saved {len(vs.maturities)} maturities to {path}" - - return mcp diff --git a/app/api/rates.py b/app/api/rates.py index 427c402e..134c2c05 100644 --- a/app/api/rates.py +++ b/app/api/rates.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from app.api.docs import load_description from quantflow.rates import AnyYieldCurve, YieldCurve rates_router = APIRouter() @@ -17,7 +18,11 @@ class YieldCurveResponse(BaseModel): rates: list[float] = Field(description="List of rates for the fitted curve") -@rates_router.get("/yield-curve") +@rates_router.get( + "/yield-curve", + summary="Fit and interpolate a yield curve", + description=load_description("yield_curve.md"), +) async def yield_curve( ttm: list[float] = Query( ..., description="List of time to maturities corresponding to the rates" diff --git a/app/api/sampling.py b/app/api/sampling.py index 18add3e9..db9a21bc 100644 --- a/app/api/sampling.py +++ b/app/api/sampling.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field from scipy.stats import chisquare, ks_1samp +from app.api.docs import load_description from quantflow.sp.ou import Vasicek from quantflow.sp.poisson import PoissonProcess from quantflow.ta.paths import Paths @@ -35,7 +36,11 @@ class DoubleExponentialResponse(SamplingResponse): char_y: list[float] = Field(description="Y values from characteristic function") -@sampling_router.get("/gaussian-sampling") +@sampling_router.get( + "/gaussian-sampling", + summary="Gaussian process sampling vs analytical PDF", + description=load_description("gaussian_sampling.md"), +) async def gaussian_sampling( kappa: float = Query(1.0, description="Mean reversion speed", ge=0.1, le=5.0), samples: int = Query(1000, description="Number of sample paths", ge=100, le=10000), @@ -61,7 +66,11 @@ async def gaussian_sampling( ) -@sampling_router.get("/poisson-sampling") +@sampling_router.get( + "/poisson-sampling", + summary="Poisson process sampling vs analytical PMF", + description=load_description("poisson_sampling.md"), +) async def poisson_sampling( intensity: float = Query(2.0, description="Poisson intensity", ge=2.0, le=20.0), samples: int = Query(1000, description="Number of sample paths", ge=100, le=10000), @@ -94,7 +103,11 @@ async def poisson_sampling( ) -@sampling_router.get("/double-exponential-sampling") +@sampling_router.get( + "/double-exponential-sampling", + summary="Double exponential sampling vs analytical PDF", + description=load_description("double_exponential_sampling.md"), +) async def double_exponential_sampling( log_kappa: float = Query( 0.1, description="Log of asymmetry parameter", ge=-2.0, le=2.0 diff --git a/app/api/smoother.py b/app/api/smoother.py index ab27818e..40ebeccc 100644 --- a/app/api/smoother.py +++ b/app/api/smoother.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from app.api.docs import load_description from quantflow.ta.ewma import EWMA from quantflow.ta.supersmoother import SuperSmoother @@ -23,7 +24,12 @@ class SmootherResponse(BaseModel): data: list[SmootherPoint] = Field(description="Time series with smoothed values") -@smoother_router.get("/supersmoother") +@smoother_router.get( + "/supersmoother", + summary="SuperSmoother and EWMA price filter", + description=load_description("supersmoother.md"), + response_model=SmootherResponse, +) async def supersmoother( redis: RedisDep, fmp: FMPDep, diff --git a/app/api/volatility.py b/app/api/volatility.py index 68fab547..009bfdbe 100644 --- a/app/api/volatility.py +++ b/app/api/volatility.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Query from pydantic import BaseModel, Field +from app.api.docs import load_description from quantflow.data.deribit import Deribit from quantflow.data.yahoo import Yahoo from quantflow.options.inputs import VolSurfaceInputs @@ -54,7 +55,11 @@ class VolSurfaceResponse(BaseModel): ) -@volatility_router.get("/volatility-surface") +@volatility_router.get( + "/volatility-surface", + summary="Live implied volatility surface", + description=load_description("volatility_surface.md"), +) async def volatility_surface( redis: RedisDep, asset: str = Query( diff --git a/docs/api/index.md b/docs/api/index.md index c64cbe60..7dffd0e8 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -53,7 +53,6 @@ Time series filters and indicators for financial data. | Module | Description | |---|---| | [EWMA](ta/ewma.md) | Exponentially weighted moving average | -| [Kalman Filter](ta/kalman.md) | Kalman filter for state estimation | | [Supersmoother](ta/supersmoother.md) | Ehlers two-pole supersmoother filter | | [OHLC](ta/ohlc.md) | OHLC bar utilities and resampling | | [Paths](ta/paths.md) | Simulated path containers and statistics | diff --git a/docs/api/ta/kalman.md b/docs/api/ta/kalman.md deleted file mode 100644 index 4f1d395d..00000000 --- a/docs/api/ta/kalman.md +++ /dev/null @@ -1,3 +0,0 @@ -# Kalman Filter - -::: quantflow.ta.kalman.KalmanFilter diff --git a/docs/mcp.md b/docs/mcp.md deleted file mode 100644 index 1065e3c9..00000000 --- a/docs/mcp.md +++ /dev/null @@ -1,42 +0,0 @@ -# MCP Server - -Quantflow exposes its crypto volatility tools as an [MCP](https://modelcontextprotocol.io) server, allowing AI clients such as Claude to query live Deribit data directly. - -The server is hosted at: - -``` -https://quantflow.quantmind.com/.api/mcp -``` - -It uses the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport. No API key is required — all tools use the public Deribit API. - -## Claude Code - -```bash -claude mcp add --transport http quantflow https://quantflow.quantmind.com/.api/mcp -``` - -## Claude Desktop - -Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): - -```json -{ - "mcpServers": { - "quantflow": { - "type": "streamable-http", - "url": "https://quantflow.quantmind.com/.api/mcp" - } - } -} -``` - -## Available tools - -| Tool | Description | -|---|---| -| `crypto_instruments` | List instruments for a currency on Deribit (spot, future, option) | -| `crypto_historical_volatility` | Historical volatility index from Deribit | -| `crypto_term_structure` | Volatility term structure across maturities | -| `crypto_implied_volatility` | Implied volatility surface (all maturities or a single one) | -| `vol_surface_snapshot` | Fetch a live vol surface and write it as a JSON file | diff --git a/docs/release-notes.md b/docs/release-notes.md index 07129dd2..13f7d679 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -154,7 +154,6 @@ and signature changes were made along the way: see **Breaking changes** below. - New logo set (favicon, lockup, marks, social banners) under `docs/assets/logos/`. ([#53](https://github.com/quantmind/quantflow/pull/53)) -- New `docs/mcp.md` page covering the MCP server. - Bibliography rebuilt from BibTeX via `docs/bib2md.py`; glossary expanded; mathjax tweaks for inline rendering. ([#47](https://github.com/quantmind/quantflow/pull/47), diff --git a/frontend/observablehq.config.js b/frontend/observablehq.config.js index 95cdb0b0..7be60b04 100644 --- a/frontend/observablehq.config.js +++ b/frontend/observablehq.config.js @@ -13,7 +13,10 @@ export default { root: "src", output: "../app/examples", base: "/examples", - head: `\n${headSnippet}`, + head: ` + + +${headSnippet}`, style: "style.css", pages: [{name: "Volatility Surface", path: "/volatility-surface"}, {name: "Yield Curve", path: "/yield-curve"}, {name: "Sampling", path: "/sampling"}, {name: "SuperSmoother", path: "/supersmoother"}, {name: "Cointegration", path: "/cointegration"}, {name: "Hurst Exponent", path: "/hurst"}, {name: "Heston Vol Surface", path: "/heston-vol-surface"}], header: ` @@ -44,5 +47,5 @@ export default { })(); `, - footer: "Quantflow live examples" + footer: 'Quantflow live examples · API Reference' }; diff --git a/mkdocs.yml b/mkdocs.yml index 2bf80493..d9d3ad4a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,7 +94,6 @@ nav: - Timeseries Analysis: - api/ta/index.md - EWMA: api/ta/ewma.md - - Kalman Filter: api/ta/kalman.md - OHLC: api/ta/ohlc.md - Paths: api/ta/paths.md - Supersmoother: api/ta/supersmoother.md @@ -132,7 +131,6 @@ nav: - Lévy Process: theory/levy.md - Option Pricing: theory/option_pricing.md - Live Examples: https://quantflow.quantmind.com/examples - - MCP Server: mcp.md - Glossary: glossary.md - Contributing: contributing.md - Bibliography: bibliography.md diff --git a/pyproject.toml b/pyproject.toml index 834725a5..a4f609d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,6 @@ docs = [ "griffe-pydantic>=1.1.0", "griffe-typingdoc>=0.2.7", "kaleido>=1.2.0", - "mcp>=1.26.0", "mkdocs-macros-plugin>=1.3.7", "mkdocs-material>=9.7.0", "mkdocs-redirects>=1.2.1", diff --git a/quantflow/ta/kalman.py b/quantflow/ta/kalman.py deleted file mode 100644 index 4ec5606c..00000000 --- a/quantflow/ta/kalman.py +++ /dev/null @@ -1,72 +0,0 @@ -from pydantic import BaseModel, Field, PrivateAttr -from typing_extensions import Annotated, Doc - - -class KalmanFilter(BaseModel): - r"""One-dimensional Kalman filter for time series data. - - This implementation uses a simple 1D state-space model: - - $$ - \begin{align} - x_t &= x_{t-1} + w_t, \quad w_t \sim \mathcal{N}(0, Q) \\ - z_t &= x_t + v_t, \quad v_t \sim \mathcal{N}(0, R) - \end{align} - $$ - - The Kalman filter estimates the hidden state $x_t$ given noisy measurements $z_t$. - The ratio $Q/R$ determines the smoothing behavior. - """ - - R: float = Field(default=1.0, gt=0.0, description="Measurement noise covariance") - Q: float = Field(default=0.01, gt=0.0, description="Process noise covariance") - - _x: float | None = PrivateAttr(default=None) # State estimate - _P: float = PrivateAttr(default=1.0) # Error covariance - _K: float = PrivateAttr(default=0.0) # Kalman Gain - - def value(self) -> float | None: - """Get the most recent smoothed value (state estimate), if available.""" - return self._x - - def update( - self, - value: Annotated[float, Doc("New noisy measurement to update the filter")], - ) -> float: - """Update the filter with a new value and return the smoothed result.""" - # Initialize on first update - if self._x is None: - self._x = value - self._P = self.R - return value - - # Prediction step - # x_pred = x_prev (Random walk model) - # P_pred = P_prev + Q - x_pred = self._x - P_pred = self._P + self.Q - - # Update step - # K = P_pred / (P_pred + R) - # x_new = x_pred + K * (measurement - x_pred) - # P_new = (1 - K) * P_pred - K = P_pred / (P_pred + self.R) - x_new = x_pred + K * (value - x_pred) - P_new = (1 - K) * P_pred - - # Update state - self._x = x_new - self._P = P_new - self._K = K - - return x_new - - @property - def error_covariance(self) -> float: - """Current estimated error covariance.""" - return self._P - - @property - def kalman_gain(self) -> float: - """Most recent Kalman gain.""" - return self._K diff --git a/quantflow_tests/test_ai.py b/quantflow_tests/test_ai.py deleted file mode 100644 index ab1d09c8..00000000 --- a/quantflow_tests/test_ai.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Tests for the quantflow MCP crypto tools.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch - -import pandas as pd -import pytest -from mcp.server.fastmcp import FastMCP - -from app.api.mcp import create_mcp - - -def text(result: Any) -> str: - """Extract text from a call_tool result.""" - blocks = result[0] if isinstance(result, tuple) else result - if blocks and hasattr(blocks[0], "text"): - return blocks[0].text - return str(result) - - -@pytest.fixture -def mcp_server() -> FastMCP: - return create_mcp() - - -async def test_crypto_instruments(mcp_server: FastMCP) -> None: - mock_client = AsyncMock() - mock_client.get_instruments.return_value = [ - MagicMock(__str__=lambda self: "BTC-SPOT") - ] - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool("crypto_instruments", {"currency": "BTC"}) - assert "BTC" in text(result) - - -async def test_crypto_instruments_empty(mcp_server: FastMCP) -> None: - mock_client = AsyncMock() - mock_client.get_instruments.return_value = [] - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool("crypto_instruments", {"currency": "BTC"}) - assert "No instruments" in text(result) - - -async def test_crypto_historical_volatility(mcp_server: FastMCP) -> None: - mock_client = AsyncMock() - mock_client.get_volatility.return_value = pd.DataFrame( - [{"date": "2025-01-01", "volatility": 0.8}] - ) - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool( - "crypto_historical_volatility", {"currency": "BTC"} - ) - assert "volatility" in text(result) - assert "2025-01-01" in text(result) - - -async def test_crypto_historical_volatility_empty(mcp_server: FastMCP) -> None: - mock_client = AsyncMock() - mock_client.get_volatility.return_value = pd.DataFrame() - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool( - "crypto_historical_volatility", {"currency": "BTC"} - ) - assert "No volatility data" in text(result) - - -async def test_crypto_term_structure(mcp_server: FastMCP, vol_surface: Any) -> None: - mock_loader = MagicMock() - mock_loader.surface.return_value = vol_surface - mock_client = AsyncMock() - mock_client.volatility_surface_loader.return_value = mock_loader - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool( - "crypto_term_structure", {"currency": "ETH"} - ) - assert "ttm" in text(result) - - -async def test_crypto_implied_volatility(mcp_server: FastMCP, vol_surface: Any) -> None: - mock_loader = MagicMock() - mock_loader.surface.return_value = vol_surface - mock_client = AsyncMock() - mock_client.volatility_surface_loader.return_value = mock_loader - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool( - "crypto_implied_volatility", {"currency": "ETH"} - ) - assert "iv" in text(result) - - -async def test_vol_surface_snapshot( - mcp_server: FastMCP, vol_surface: Any, tmp_path: Path -) -> None: - mock_loader = MagicMock() - mock_loader.surface.return_value = vol_surface - mock_client = AsyncMock() - mock_client.volatility_surface_loader.return_value = mock_loader - out = str(tmp_path / "snapshot.json") - with patch("app.api.mcp.Deribit") as MockDeribit: - MockDeribit.return_value.__aenter__ = AsyncMock(return_value=mock_client) - MockDeribit.return_value.__aexit__ = AsyncMock(return_value=False) - result = await mcp_server.call_tool( - "vol_surface_snapshot", {"currency": "ETH", "path": out} - ) - assert "Saved" in text(result) - assert Path(out).exists() diff --git a/quantflow_tests/test_ta_filters.py b/quantflow_tests/test_ta_filters.py index ffc83490..69850ac3 100644 --- a/quantflow_tests/test_ta_filters.py +++ b/quantflow_tests/test_ta_filters.py @@ -3,7 +3,6 @@ import pytest from quantflow.ta.ewma import EWMA -from quantflow.ta.kalman import KalmanFilter from quantflow.ta.supersmoother import SuperSmoother @@ -44,24 +43,6 @@ def test_ewma_from_alpha_validation() -> None: EWMA.from_alpha(alpha=0.0) -def test_kalman_initialization_and_first_update() -> None: - kf = KalmanFilter(R=1.0, Q=0.1) - assert kf.value() is None - first = kf.update(5.0) - assert first == 5.0 - assert kf.value() == 5.0 - assert kf.error_covariance == 1.0 - - -def test_kalman_steady_updates_and_properties() -> None: - kf = KalmanFilter(R=1.0, Q=0.1) - kf.update(10.0) - updated = kf.update(12.0) - assert 10.0 < updated < 12.0 - assert 0.0 < kf.kalman_gain < 1.0 - assert kf.error_covariance > 0.0 - - def test_supersmoother_initialization() -> None: smoother = SuperSmoother(period=10) assert smoother.raw_value() is None diff --git a/uv.lock b/uv.lock index 8e81e96e..80c49876 100644 --- a/uv.lock +++ b/uv.lock @@ -679,65 +679,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cryptography" -version = "48.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, -] - [[package]] name = "cuda-bindings" version = "12.9.6" @@ -1169,15 +1110,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - [[package]] name = "hypothesis" version = "6.152.9" @@ -1633,31 +1565,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mcp" -version = "1.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -2886,20 +2793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -2909,20 +2802,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] -[[package]] -name = "pyjwt" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - [[package]] name = "pymdown-extensions" version = "10.21.3" @@ -3064,25 +2943,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -3250,7 +3110,6 @@ docs = [ { name = "griffe-pydantic" }, { name = "griffe-typingdoc" }, { name = "kaleido" }, - { name = "mcp" }, { name = "mkdocs-macros-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, @@ -3280,7 +3139,6 @@ requires-dist = [ { name = "isort", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "kaleido", marker = "extra == 'docs'", specifier = ">=1.2.0" }, { name = "marimo", marker = "extra == 'book'", specifier = ">=0.23.7" }, - { name = "mcp", marker = "extra == 'docs'", specifier = ">=1.26.0" }, { name = "mkdocs-macros-plugin", marker = "extra == 'docs'", specifier = ">=1.3.7" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", marker = "extra == 'docs'", specifier = ">=1.2.1" }, @@ -3799,19 +3657,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] -[[package]] -name = "sse-starlette" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, -] - [[package]] name = "starlette" version = "1.1.0"