Skip to content
Merged

AI #39

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ applyTo: '/**'
# Quantflow Instructions


## Development

* Always run `make lint` after code changes — runs taplo, isort, black, ruff, and mypy
* Never edit `readme.md` directly — it is generated from `docs/index.md` via `make docs`

## Docker

* The Dockerfile is at `dev/quantflow.dockerfile`
Expand Down
2 changes: 1 addition & 1 deletion dev/quantflow.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ WORKDIR /build
COPY pyproject.toml uv.lock readme.md ./

# Install dependencies (no root package, with needed extras)
RUN uv sync --frozen --no-install-project --extra book --extra docs --extra data
RUN uv sync --frozen --no-install-project --extra ai --extra book --extra docs --extra data

# Copy source and build docs
COPY mkdocs.yml ./
Expand Down
12 changes: 12 additions & 0 deletions docs/api/options/vol_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,16 @@

::: quantflow.options.surface.VolSurfaceLoader

::: quantflow.options.surface.OptionPrice

::: quantflow.options.surface.OptionSelection

::: quantflow.options.inputs.VolSurfaceInputs

::: quantflow.options.inputs.VolSurfaceInput

::: quantflow.options.inputs.SpotInput

::: quantflow.options.inputs.ForwardInput

::: quantflow.options.inputs.OptionInput
74 changes: 60 additions & 14 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pip install quantflow

## Modules

* [quantflow.cli](https://github.com/quantmind/quantflow/tree/main/quantflow/cli) command line client (requires `quantflow[cli,data]`)
* [quantflow.ai](https://github.com/quantmind/quantflow/tree/main/quantflow/ai) MCP server for AI clients (requires `quantflow[ai,data]`)
* [quantflow.data](https://github.com/quantmind/quantflow/tree/main/quantflow/data) data APIs (requires `quantflow[data]`)
* [quantflow.options](https://github.com/quantmind/quantflow/tree/main/quantflow/options) option pricing and calibration
* [quantflow.sp](https://github.com/quantmind/quantflow/tree/main/quantflow/sp) stochastic process primitives
Expand All @@ -27,23 +27,69 @@ pip install quantflow

## Optional dependencies

Quantflow comes with two optional dependencies:
* `data` — data retrieval: `pip install quantflow[data]`
* `ai` — MCP server for AI clients: `pip install quantflow[ai,data]`

* `data` for data retrieval, to install it use
```
pip install quantflow[data]
```
* `cli` for command line interface, to install it use
```
pip install quantflow[data,cli]
```
## MCP Server

## Command line tools
Quantflow exposes its data tools as an [MCP](https://modelcontextprotocol.io) server, allowing AI clients such as Claude to query market data, crypto volatility surfaces, and economic indicators directly.

The command line tools are available when installing with the extra `cli` and `data` dependencies.
Install with the `ai` and `data` extras:

```bash
pip install quantflow[cli,data]
pip install quantflow[ai,data]
```

It is possible to use the command line tool `qf` to download data and run pricing and calibration scripts.
### API keys

Store your API keys in `~/.quantflow/.vault`:

```
fmp=your-fmp-key
fred=your-fred-key
```

Or let the AI manage them for you via the `vault_add` tool once connected.

### Claude Code

```bash
claude mcp add quantflow -- uv run qf-mcp
```

### Claude Desktop

Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
"mcpServers": {
"quantflow": {
"command": "uv",
"args": ["run", "qf-mcp"]
}
}
}
```

### Available tools

| Tool | Description |
|---|---|
| `vault_keys` | List stored API keys |
| `vault_add` | Add or update an API key |
| `vault_delete` | Delete an API key |
| `stock_indices` | List stock market indices |
| `stock_search` | Search companies by name or symbol |
| `stock_profile` | Get company profile |
| `stock_prices` | Get OHLC price history |
| `sector_performance` | Sector performance and PE ratios |
| `crypto_instruments` | List Deribit instruments |
| `crypto_historical_volatility` | Historical volatility from Deribit |
| `crypto_term_structure` | Volatility term structure |
| `crypto_implied_volatility` | Implied volatility surface |
| `crypto_prices` | Crypto OHLC price history |
| `ascii_chart` | ASCII chart for any stock or crypto symbol |
| `fred_subcategories` | Browse FRED categories |
| `fred_series` | List series in a FRED category |
| `fred_data` | Fetch FRED observations |
24 changes: 11 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "quantflow"
version = "0.4.4"
version = "0.5.0"
description = "quantitative analysis"
authors = [ { name = "Luca Sbardella", email = "luca@quantmind.com" } ]
license = "BSD-3-Clause"
Expand All @@ -21,27 +21,24 @@ Repository = "https://github.com/quantmind/quantflow"
Documentation = "https://quantmind.github.io/quantflow/"

[project.optional-dependencies]
ai = [
"asciichartpy>=1.5.25",
"ccy[holidays]>=1.7.1",
"google-genai>=1.61.0",
"mcp>=1.26.0",
"openai>=2.16.0",
"pydantic-ai-slim>=1.51.0",
"rich>=13.9.4",
]
book = [
"altair>=6.0.0",
"autodocsumm>=0.2.14",
"duckdb>=1.4.4",
"fastapi>=0.129.0",
"google-genai>=1.61.0",
"marimo>=0.19.7",
"mcp>=1.26.0",
"openai>=2.16.0",
"plotly>=6.2.0",
"pydantic-ai-slim>=1.51.0",
"sympy>=1.12",
]
cli = [
"asciichartpy>=1.5.25",
"async-cache>=1.1.1",
"click>=8.1.7",
"holidays>=0.63",
"prompt-toolkit>=3.0.43",
"rich>=13.9.4",
]
data = [ "aio-fluid[http]>=1.2.1" ]
dev = [
"black>=26.3.1",
Expand All @@ -67,6 +64,7 @@ ml = [

[project.scripts]
qf = "quantflow.cli.script:main"
qf-mcp = "quantflow.ai.server:main"

[build-system]
requires = [ "hatchling" ]
Expand Down
2 changes: 1 addition & 1 deletion quantflow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Quantitative analysis and pricing"""

__version__ = "0.4.4"
__version__ = "0.5.0"
1 change: 1 addition & 0 deletions quantflow/ai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""AI module for quantflow - MCP server exposing quantflow data tools."""
27 changes: 27 additions & 0 deletions quantflow/ai/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Quantflow MCP server."""

from mcp.server.fastmcp import FastMCP

from quantflow.ai.tools import charts, crypto, fred, stocks, vault

from .tools.base import McpTool


def create_server() -> FastMCP:
mcp = FastMCP("quantflow")
tool = McpTool()
vault.register(mcp, tool)
crypto.register(mcp, tool)
stocks.register(mcp, tool)
fred.register(mcp, tool)
charts.register(mcp, tool)
return mcp


def main() -> None:
server = create_server()
server.run()


if __name__ == "__main__":
main()
File renamed without changes.
33 changes: 33 additions & 0 deletions quantflow/ai/tools/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from dataclasses import dataclass, field
from pathlib import Path

from mcp.server.fastmcp.exceptions import ToolError

from quantflow.data.fmp import FMP
from quantflow.data.fred import Fred
from quantflow.data.vault import Vault

VAULT_PATH = Path.home() / ".quantflow" / ".vault"


@dataclass
class McpTool:
vault: Vault = field(default_factory=lambda: Vault(VAULT_PATH))

def fmp(self) -> FMP:
key = self.vault.get("fmp")
if not key:
raise ToolError(
"FMP API key not found in vault. "
" Please add it using the vault_add tool."
)
return FMP(key=key)

def fred(self) -> Fred:
key = self.vault.get("fred")
if not key:
raise ToolError(
"FRED API key not found in vault. "
" Please add it using the vault_add tool."
)
return Fred(key=key)
40 changes: 40 additions & 0 deletions quantflow/ai/tools/charts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Chart tools for the quantflow MCP server."""

from mcp.server.fastmcp import FastMCP

from .base import McpTool


def register(mcp: FastMCP, tool: McpTool) -> None:

@mcp.tool()
async def ascii_chart(symbol: str, frequency: str = "", height: int = 20) -> str:
"""Plot an ASCII candlestick chart for a stock or cryptocurrency.

Args:
symbol: Ticker symbol e.g. AAPL, BTCUSD, ETHUSD
frequency: Data frequency - 1min, 5min, 15min, 30min, 1hour, 4hour,
or empty for daily
height: Chart height in terminal rows (default: 20)
"""
import asciichartpy as ac

async with tool.fmp() as client:
df = await client.prices(symbol, frequency=frequency)
if df.empty:
return f"No price data for {symbol}"

df = df.sort_values("date").tail(50)
prices = df["close"].tolist()
first_date = df["date"].iloc[0]
last_date = df["date"].iloc[-1]
low = min(prices)
high = max(prices)
last = prices[-1]

chart = ac.plot(prices, {"height": height, "format": "{:8,.0f}"})
return (
f"{symbol} Close Price ({first_date} → {last_date})\n"
f"High: {high:,.2f} Low: {low:,.2f} Last: {last:,.2f}\n\n"
f"{chart}"
)
90 changes: 90 additions & 0 deletions quantflow/ai/tools/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Crypto tools for the quantflow MCP server."""

from mcp.server.fastmcp import FastMCP

from quantflow.data.deribit import Deribit, InstrumentKind

from .base import McpTool


def register(mcp: FastMCP, tool: McpTool) -> None:

@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
"""
from quantflow.options.surface import VolSurface

async with Deribit() as client:
loader = await client.volatility_surface_loader(currency)
vs: VolSurface = 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)
"""
from quantflow.options.surface import VolSurface

async with Deribit() as client:
loader = await client.volatility_surface_loader(currency)
vs: VolSurface = loader.surface()
index = None if maturity_index < 0 else maturity_index
vs.bs(index=index)
df = vs.options_df(index=index)
df["implied_vol"] = df["implied_vol"].map("{:.2%}".format)
return df.to_csv(index=False)

@mcp.tool()
async def crypto_prices(symbol: str, frequency: str = "") -> str:
"""Get OHLC price history for a cryptocurrency via FMP.

Args:
symbol: Cryptocurrency symbol e.g. BTCUSD
frequency: Data frequency - 1min, 5min, 15min, 30min, 1hour, 4hour,
or empty for daily
"""
async with tool.fmp() as client:
df = await client.prices(symbol, frequency=frequency)
if df.empty:
return f"No price data for {symbol}"
df = df[["date", "open", "high", "low", "close", "volume"]].sort_values("date")
return df.tail(50).to_csv(index=False)
Loading
Loading