profunding-mcp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- profunding_mcp-0.2.0/.gitignore +53 -0
- profunding_mcp-0.2.0/PKG-INFO +113 -0
- profunding_mcp-0.2.0/README.md +95 -0
- profunding_mcp-0.2.0/pyproject.toml +30 -0
- profunding_mcp-0.2.0/src/profunding_mcp/__init__.py +3 -0
- profunding_mcp-0.2.0/src/profunding_mcp/client.py +58 -0
- profunding_mcp-0.2.0/src/profunding_mcp/config.py +9 -0
- profunding_mcp-0.2.0/src/profunding_mcp/server.py +906 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
venv/
|
|
8
|
+
env/
|
|
9
|
+
.venv/
|
|
10
|
+
|
|
11
|
+
# Node
|
|
12
|
+
node_modules/
|
|
13
|
+
.next/
|
|
14
|
+
out/
|
|
15
|
+
npm-debug.log*
|
|
16
|
+
|
|
17
|
+
# Environment
|
|
18
|
+
.env
|
|
19
|
+
.env.local
|
|
20
|
+
.env.prod
|
|
21
|
+
secrets.txt
|
|
22
|
+
|
|
23
|
+
# Skills / local agent files
|
|
24
|
+
.agents/
|
|
25
|
+
.claude/skills/
|
|
26
|
+
.claude/settings.local.json
|
|
27
|
+
skills-lock.json
|
|
28
|
+
backend/skills-lock.json
|
|
29
|
+
|
|
30
|
+
# Windows artifacts
|
|
31
|
+
nul
|
|
32
|
+
backend/nul
|
|
33
|
+
|
|
34
|
+
# Local files
|
|
35
|
+
SALES_PITCH.md
|
|
36
|
+
video/
|
|
37
|
+
|
|
38
|
+
# IDE
|
|
39
|
+
.vscode/
|
|
40
|
+
.idea/
|
|
41
|
+
*.swp
|
|
42
|
+
*.swo
|
|
43
|
+
|
|
44
|
+
# Database
|
|
45
|
+
*.db
|
|
46
|
+
*.sqlite
|
|
47
|
+
|
|
48
|
+
# OS
|
|
49
|
+
.DS_Store
|
|
50
|
+
Thumbs.db
|
|
51
|
+
|
|
52
|
+
# Docker
|
|
53
|
+
*.log
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: profunding-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for ProFunding — AI-powered funding rate arbitrage across 20+ perp DEXes
|
|
5
|
+
Project-URL: Homepage, https://profunding.pro
|
|
6
|
+
Project-URL: Repository, https://github.com/Desperate10/dex_funding_dashboard
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: arbitrage,defi,delta-neutral,funding-rate,mcp,trading
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Requires-Dist: mcp>=1.0.0
|
|
16
|
+
Requires-Dist: pydantic>=2.0.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# ProFunding MCP Server
|
|
20
|
+
|
|
21
|
+
MCP server for [ProFunding](https://profunding.pro) — AI-powered funding rate arbitrage across 20+ perpetual DEXes.
|
|
22
|
+
|
|
23
|
+
Analyze delta-neutral opportunities, check real-time liquidity, run backtests, and get deep analytics directly from your AI assistant.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install profunding-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
### Claude Code
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
claude mcp add profunding profunding-mcp -e PROFUNDING_API_KEY=pfk_your_key_here
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Desktop
|
|
40
|
+
|
|
41
|
+
Add to your `claude_desktop_config.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"profunding": {
|
|
47
|
+
"command": "profunding-mcp",
|
|
48
|
+
"env": {
|
|
49
|
+
"PROFUNDING_API_KEY": "pfk_your_key_here"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Environment Variables
|
|
57
|
+
|
|
58
|
+
| Variable | Required | Default | Description |
|
|
59
|
+
|----------|----------|---------|-------------|
|
|
60
|
+
| `PROFUNDING_API_KEY` | No | (none) | API key. Free tier works without key. |
|
|
61
|
+
| `PROFUNDING_API_URL` | No | `https://profunding.pro/api/api` | API base URL |
|
|
62
|
+
|
|
63
|
+
## Tools (20)
|
|
64
|
+
|
|
65
|
+
### Free Tier
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `get_opportunities` | Live funding rate arbitrage opportunities across all DEXes |
|
|
70
|
+
| `get_exchanges` | List connected DEXes with status and funding interval |
|
|
71
|
+
| `get_historical_rates` | Historical funding rates for a symbol on one exchange |
|
|
72
|
+
| `run_backtest` | Backtest a delta-neutral trade with real historical data |
|
|
73
|
+
| `get_rate_chart_data` | Funding rate spread over time between two exchanges |
|
|
74
|
+
|
|
75
|
+
### Paid Tier — Analysis
|
|
76
|
+
|
|
77
|
+
| Tool | Description |
|
|
78
|
+
|------|-------------|
|
|
79
|
+
| `analyze_pair` | Full end-to-end analysis: funding + backtest + risk + depth on both legs + price spread — one call |
|
|
80
|
+
| `compare_exchanges` | Side-by-side comparison of same symbol across exchanges: rates, spread, depth, volume |
|
|
81
|
+
| `find_best_trade` | Best trade you can open RIGHT NOW at your position size — ranked by composite score |
|
|
82
|
+
| `get_smart_opportunities` | Opportunities ranked by tradability (APR x stability x depth x data confidence) |
|
|
83
|
+
| `check_liquidity` | Real-time order book depth + slippage estimates at $1k/$5k/$10k |
|
|
84
|
+
| `get_pair_intelligence` | Risk scores, backtested APR, smart ranking, depth tiers for all pairs |
|
|
85
|
+
| `get_price_spread_data` | Mark price divergence between two exchanges (price risk analysis) |
|
|
86
|
+
|
|
87
|
+
### Paid Tier — Analytics
|
|
88
|
+
|
|
89
|
+
| Tool | Description |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `get_live_alpha` | Top 5 deduplicated opportunities |
|
|
92
|
+
| `get_still_paying` | Pairs above 50% APR for 24h+ still active now |
|
|
93
|
+
| `get_top_holders` | Pairs holding high APR the longest |
|
|
94
|
+
| `get_momentum_movers` | Biggest funding spread jumps in last 6 hours |
|
|
95
|
+
| `get_weekly_recap` | Best ROI pair, best DEX combo, most stable pair |
|
|
96
|
+
| `get_unbroken_streaks` | Consecutive hours above APR threshold |
|
|
97
|
+
| `get_record_roi` | Best single trade by ROI in a period |
|
|
98
|
+
|
|
99
|
+
## Example Queries
|
|
100
|
+
|
|
101
|
+
Once connected, ask your AI assistant:
|
|
102
|
+
|
|
103
|
+
- "What's the best trade I can open right now with $5k?"
|
|
104
|
+
- "Analyze MEGA/USDC on Extended/Aster — is it worth entering?"
|
|
105
|
+
- "Compare ETH funding rates across Hyperliquid, dYdX, and Aster"
|
|
106
|
+
- "Check liquidity for SOL on Paradex — can I fill $10k?"
|
|
107
|
+
- "Run a 30-day backtest on XMR long Nado short dYdX"
|
|
108
|
+
- "Which pairs have been paying above 50% for 3+ days straight?"
|
|
109
|
+
- "What moved the most in the last 6 hours?"
|
|
110
|
+
|
|
111
|
+
## Get an API Key
|
|
112
|
+
|
|
113
|
+
Visit [profunding.pro](https://profunding.pro) to get your API key.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ProFunding MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server for [ProFunding](https://profunding.pro) — AI-powered funding rate arbitrage across 20+ perpetual DEXes.
|
|
4
|
+
|
|
5
|
+
Analyze delta-neutral opportunities, check real-time liquidity, run backtests, and get deep analytics directly from your AI assistant.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install profunding-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### Claude Code
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add profunding profunding-mcp -e PROFUNDING_API_KEY=pfk_your_key_here
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
Add to your `claude_desktop_config.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"profunding": {
|
|
29
|
+
"command": "profunding-mcp",
|
|
30
|
+
"env": {
|
|
31
|
+
"PROFUNDING_API_KEY": "pfk_your_key_here"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Environment Variables
|
|
39
|
+
|
|
40
|
+
| Variable | Required | Default | Description |
|
|
41
|
+
|----------|----------|---------|-------------|
|
|
42
|
+
| `PROFUNDING_API_KEY` | No | (none) | API key. Free tier works without key. |
|
|
43
|
+
| `PROFUNDING_API_URL` | No | `https://profunding.pro/api/api` | API base URL |
|
|
44
|
+
|
|
45
|
+
## Tools (20)
|
|
46
|
+
|
|
47
|
+
### Free Tier
|
|
48
|
+
|
|
49
|
+
| Tool | Description |
|
|
50
|
+
|------|-------------|
|
|
51
|
+
| `get_opportunities` | Live funding rate arbitrage opportunities across all DEXes |
|
|
52
|
+
| `get_exchanges` | List connected DEXes with status and funding interval |
|
|
53
|
+
| `get_historical_rates` | Historical funding rates for a symbol on one exchange |
|
|
54
|
+
| `run_backtest` | Backtest a delta-neutral trade with real historical data |
|
|
55
|
+
| `get_rate_chart_data` | Funding rate spread over time between two exchanges |
|
|
56
|
+
|
|
57
|
+
### Paid Tier — Analysis
|
|
58
|
+
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `analyze_pair` | Full end-to-end analysis: funding + backtest + risk + depth on both legs + price spread — one call |
|
|
62
|
+
| `compare_exchanges` | Side-by-side comparison of same symbol across exchanges: rates, spread, depth, volume |
|
|
63
|
+
| `find_best_trade` | Best trade you can open RIGHT NOW at your position size — ranked by composite score |
|
|
64
|
+
| `get_smart_opportunities` | Opportunities ranked by tradability (APR x stability x depth x data confidence) |
|
|
65
|
+
| `check_liquidity` | Real-time order book depth + slippage estimates at $1k/$5k/$10k |
|
|
66
|
+
| `get_pair_intelligence` | Risk scores, backtested APR, smart ranking, depth tiers for all pairs |
|
|
67
|
+
| `get_price_spread_data` | Mark price divergence between two exchanges (price risk analysis) |
|
|
68
|
+
|
|
69
|
+
### Paid Tier — Analytics
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| `get_live_alpha` | Top 5 deduplicated opportunities |
|
|
74
|
+
| `get_still_paying` | Pairs above 50% APR for 24h+ still active now |
|
|
75
|
+
| `get_top_holders` | Pairs holding high APR the longest |
|
|
76
|
+
| `get_momentum_movers` | Biggest funding spread jumps in last 6 hours |
|
|
77
|
+
| `get_weekly_recap` | Best ROI pair, best DEX combo, most stable pair |
|
|
78
|
+
| `get_unbroken_streaks` | Consecutive hours above APR threshold |
|
|
79
|
+
| `get_record_roi` | Best single trade by ROI in a period |
|
|
80
|
+
|
|
81
|
+
## Example Queries
|
|
82
|
+
|
|
83
|
+
Once connected, ask your AI assistant:
|
|
84
|
+
|
|
85
|
+
- "What's the best trade I can open right now with $5k?"
|
|
86
|
+
- "Analyze MEGA/USDC on Extended/Aster — is it worth entering?"
|
|
87
|
+
- "Compare ETH funding rates across Hyperliquid, dYdX, and Aster"
|
|
88
|
+
- "Check liquidity for SOL on Paradex — can I fill $10k?"
|
|
89
|
+
- "Run a 30-day backtest on XMR long Nado short dYdX"
|
|
90
|
+
- "Which pairs have been paying above 50% for 3+ days straight?"
|
|
91
|
+
- "What moved the most in the last 6 hours?"
|
|
92
|
+
|
|
93
|
+
## Get an API Key
|
|
94
|
+
|
|
95
|
+
Visit [profunding.pro](https://profunding.pro) to get your API key.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "profunding-mcp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "MCP server for ProFunding — AI-powered funding rate arbitrage across 20+ perp DEXes"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
keywords = ["mcp", "funding-rate", "arbitrage", "defi", "trading", "delta-neutral"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
16
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"mcp>=1.0.0",
|
|
21
|
+
"httpx>=0.27.0",
|
|
22
|
+
"pydantic>=2.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://profunding.pro"
|
|
27
|
+
Repository = "https://github.com/Desperate10/dex_funding_dashboard"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
profunding-mcp = "profunding_mcp.server:main"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""HTTP client for the ProFunding REST API."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .config import API_URL, API_KEY
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProFundingClient:
|
|
10
|
+
"""Thin wrapper around the ProFunding REST API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
headers = {"Content-Type": "application/json"}
|
|
14
|
+
if API_KEY:
|
|
15
|
+
headers["X-API-Key"] = API_KEY
|
|
16
|
+
self._client = httpx.AsyncClient(
|
|
17
|
+
base_url=API_URL,
|
|
18
|
+
headers=headers,
|
|
19
|
+
timeout=30.0,
|
|
20
|
+
)
|
|
21
|
+
self._tier: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
async def validate_key(self) -> dict:
|
|
24
|
+
"""Validate the API key at startup and cache the tier."""
|
|
25
|
+
if not API_KEY:
|
|
26
|
+
self._tier = "free"
|
|
27
|
+
return {"valid": True, "tier": "free"}
|
|
28
|
+
resp = await self._client.get("/mcp/validate")
|
|
29
|
+
resp.raise_for_status()
|
|
30
|
+
data = resp.json()
|
|
31
|
+
self._tier = data.get("tier", "free")
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def tier(self) -> str:
|
|
36
|
+
return self._tier or "free"
|
|
37
|
+
|
|
38
|
+
def is_paid(self) -> bool:
|
|
39
|
+
return self._tier == "paid"
|
|
40
|
+
|
|
41
|
+
async def get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
42
|
+
"""GET request to the API."""
|
|
43
|
+
resp = await self._client.get(path, params=params)
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
return resp.json()
|
|
46
|
+
|
|
47
|
+
async def post(self, path: str, json: Optional[dict] = None) -> Any:
|
|
48
|
+
"""POST request to the API."""
|
|
49
|
+
resp = await self._client.post(path, json=json)
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
return resp.json()
|
|
52
|
+
|
|
53
|
+
async def close(self):
|
|
54
|
+
await self._client.aclose()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Singleton
|
|
58
|
+
client = ProFundingClient()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Configuration from environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Double /api/api: nginx rewrites /api/* → /* but backend routes are mounted at /api,
|
|
7
|
+
# so external callers need /api/api to reach /api/* on the backend.
|
|
8
|
+
API_URL = os.getenv("PROFUNDING_API_URL", "https://profunding.pro/api/api")
|
|
9
|
+
API_KEY = os.getenv("PROFUNDING_API_KEY", "")
|
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
"""ProFunding MCP Server — funding rate arbitrage tools for AI assistants."""
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from .client import client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@asynccontextmanager
|
|
9
|
+
async def _lifespan(server):
|
|
10
|
+
"""Validate API key on startup."""
|
|
11
|
+
try:
|
|
12
|
+
info = await client.validate_key()
|
|
13
|
+
tier = info.get("tier", "free")
|
|
14
|
+
except Exception:
|
|
15
|
+
tier = "free"
|
|
16
|
+
yield
|
|
17
|
+
await client.close()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP(
|
|
21
|
+
"ProFunding",
|
|
22
|
+
instructions="Live funding rate arbitrage data across 20+ perpetual DEXes. "
|
|
23
|
+
"Find delta-neutral trading opportunities, run backtests, and get deep analytics.",
|
|
24
|
+
lifespan=_lifespan,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ─── Helpers ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
async def _ensure_validated():
|
|
31
|
+
"""Lazy-validate the API key if not done yet."""
|
|
32
|
+
if client._tier is None:
|
|
33
|
+
try:
|
|
34
|
+
await client.validate_key()
|
|
35
|
+
except Exception:
|
|
36
|
+
client._tier = "free"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _require_paid():
|
|
40
|
+
"""Raise if the API key doesn't have paid tier."""
|
|
41
|
+
await _ensure_validated()
|
|
42
|
+
if not client.is_paid():
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"This tool requires a paid ProFunding API key. "
|
|
45
|
+
"Visit profunding.pro to upgrade."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fmt_opp(o: dict) -> str:
|
|
50
|
+
"""Format a single opportunity for display."""
|
|
51
|
+
line = (
|
|
52
|
+
f" {o['symbol']} — Long {o['long_exchange']} ({o['long_apr']:+.1f}%) / "
|
|
53
|
+
f"Short {o['short_exchange']} ({o['short_apr']:+.1f}%) — "
|
|
54
|
+
f"Net {o['net_apr']:.1f}% APR, breakeven {o.get('breakeven_days', '?')} days"
|
|
55
|
+
)
|
|
56
|
+
# Add liquidity info if available
|
|
57
|
+
tags = []
|
|
58
|
+
long_spread = o.get('long_spread_pct')
|
|
59
|
+
short_spread = o.get('short_spread_pct')
|
|
60
|
+
if long_spread is not None or short_spread is not None:
|
|
61
|
+
worst_spread = max(long_spread or 0, short_spread or 0)
|
|
62
|
+
tags.append(f"spread {worst_spread:.2f}%")
|
|
63
|
+
long_vol = o.get('long_volume_24h')
|
|
64
|
+
short_vol = o.get('short_volume_24h')
|
|
65
|
+
if long_vol is not None or short_vol is not None:
|
|
66
|
+
min_volume = min(long_vol or float('inf'), short_vol or float('inf'))
|
|
67
|
+
if min_volume < float('inf'):
|
|
68
|
+
tags.append(f"vol ${min_volume:,.0f}")
|
|
69
|
+
if tags:
|
|
70
|
+
line += f" [{', '.join(tags)}]"
|
|
71
|
+
return line
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ─── FREE TIER TOOLS ─────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
async def get_opportunities(
|
|
78
|
+
min_apr: float = 0,
|
|
79
|
+
symbol: str = "",
|
|
80
|
+
exchange: str = "",
|
|
81
|
+
limit: int = 20,
|
|
82
|
+
) -> str:
|
|
83
|
+
"""Get live funding rate arbitrage opportunities across all DEXes.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
min_apr: Minimum net APR to filter (default 0)
|
|
87
|
+
symbol: Filter by trading pair (e.g. "ETH/USDC")
|
|
88
|
+
exchange: Filter by exchange name (e.g. "Hyperliquid")
|
|
89
|
+
limit: Max number of results (default 20)
|
|
90
|
+
"""
|
|
91
|
+
params = {}
|
|
92
|
+
if min_apr > 0:
|
|
93
|
+
params["min_apr"] = min_apr
|
|
94
|
+
if symbol:
|
|
95
|
+
params["symbol"] = symbol
|
|
96
|
+
|
|
97
|
+
data = await client.get("/opportunities", params=params)
|
|
98
|
+
|
|
99
|
+
if exchange:
|
|
100
|
+
ex = exchange.lower()
|
|
101
|
+
data = [o for o in data if ex in o["long_exchange"].lower() or ex in o["short_exchange"].lower()]
|
|
102
|
+
|
|
103
|
+
data = sorted(data, key=lambda o: o["net_apr"], reverse=True)[:limit]
|
|
104
|
+
|
|
105
|
+
if not data:
|
|
106
|
+
return "No opportunities found matching your criteria."
|
|
107
|
+
|
|
108
|
+
lines = [f"Found {len(data)} opportunities:\n"]
|
|
109
|
+
for o in data:
|
|
110
|
+
lines.append(_fmt_opp(o))
|
|
111
|
+
|
|
112
|
+
return "\n".join(lines)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
async def get_exchanges() -> str:
|
|
117
|
+
"""List all connected DEXes with their status and funding interval."""
|
|
118
|
+
data = await client.get("/exchanges")
|
|
119
|
+
exchanges = data.get("exchanges", [])
|
|
120
|
+
|
|
121
|
+
if not exchanges:
|
|
122
|
+
return "No exchanges connected."
|
|
123
|
+
|
|
124
|
+
lines = [f"{len(exchanges)} exchanges connected:\n"]
|
|
125
|
+
for ex in exchanges:
|
|
126
|
+
status = "online" if ex["connected"] else "offline"
|
|
127
|
+
lines.append(f" {ex['name']} — {status}, {ex['funding_interval_hours']}h funding interval")
|
|
128
|
+
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
async def get_historical_rates(
|
|
134
|
+
symbol: str,
|
|
135
|
+
exchange: str,
|
|
136
|
+
days: int = 7,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Get historical funding rates for a symbol on a specific exchange.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
142
|
+
exchange: Exchange name (e.g. "Hyperliquid")
|
|
143
|
+
days: Lookback period in days (1-90, default 7)
|
|
144
|
+
"""
|
|
145
|
+
data = await client.get("/rates/historical", params={
|
|
146
|
+
"symbol": symbol,
|
|
147
|
+
"exchange": exchange,
|
|
148
|
+
"days": days,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
rates = data if isinstance(data, list) else data.get("rates", [])
|
|
152
|
+
|
|
153
|
+
if not rates:
|
|
154
|
+
return f"No historical data for {symbol} on {exchange} (last {days} days)."
|
|
155
|
+
|
|
156
|
+
# Summarize
|
|
157
|
+
aprs = [float(r.get("funding_rate_apr", 0)) for r in rates]
|
|
158
|
+
avg = sum(aprs) / len(aprs) if aprs else 0
|
|
159
|
+
mn, mx = min(aprs), max(aprs)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
f"{symbol} on {exchange} — last {days} days ({len(rates)} data points):\n"
|
|
163
|
+
f" Avg APR: {avg:.1f}%\n"
|
|
164
|
+
f" Min APR: {mn:.1f}%\n"
|
|
165
|
+
f" Max APR: {mx:.1f}%"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@mcp.tool()
|
|
170
|
+
async def run_backtest(
|
|
171
|
+
symbol: str,
|
|
172
|
+
long_exchange: str,
|
|
173
|
+
short_exchange: str,
|
|
174
|
+
position_size: float = 1000,
|
|
175
|
+
days: int = 7,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Backtest a delta-neutral funding arbitrage trade with historical data.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
181
|
+
long_exchange: Exchange to go long on
|
|
182
|
+
short_exchange: Exchange to go short on
|
|
183
|
+
position_size: Position size in USD (default 1000)
|
|
184
|
+
days: Backtest period in days (1-90, default 7)
|
|
185
|
+
"""
|
|
186
|
+
data = await client.post("/backtest", json={
|
|
187
|
+
"symbol": symbol,
|
|
188
|
+
"long_exchange": long_exchange,
|
|
189
|
+
"short_exchange": short_exchange,
|
|
190
|
+
"position_size": position_size,
|
|
191
|
+
"days": days,
|
|
192
|
+
"execution_fee_bps": 10,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
pnl = data.get("total_pnl", 0)
|
|
196
|
+
pnl_pct = data.get("pnl_percentage", 0)
|
|
197
|
+
funding = data.get("total_funding_received", 0)
|
|
198
|
+
fees = data.get("total_fees_paid", 0)
|
|
199
|
+
sharpe = data.get("sharpe_ratio", 0)
|
|
200
|
+
|
|
201
|
+
result = (
|
|
202
|
+
f"Backtest: {symbol} — Long {long_exchange} / Short {short_exchange}\n"
|
|
203
|
+
f" Period: {days} days, Position: ${position_size:,.0f}\n"
|
|
204
|
+
f" Total PnL: ${pnl:,.2f} ({pnl_pct:+.2f}%)\n"
|
|
205
|
+
f" Funding received: ${funding:,.2f}\n"
|
|
206
|
+
f" Fees paid: ${fees:,.2f}\n"
|
|
207
|
+
f" Sharpe ratio: {sharpe:.2f}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if data.get("has_price_data") and data.get("total_pnl_with_spread") is not None:
|
|
211
|
+
spread_pnl = data.get("spread_pnl", 0)
|
|
212
|
+
total_with_spread = data["total_pnl_with_spread"]
|
|
213
|
+
result += (
|
|
214
|
+
f"\n Price spread PnL: ${spread_pnl:,.2f}"
|
|
215
|
+
f"\n Total PnL (with spread): ${total_with_spread:,.2f}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
hist_slip = data.get("historical_slippage_bps")
|
|
219
|
+
if hist_slip is not None:
|
|
220
|
+
result += f"\n Historical bid-ask slippage: {hist_slip:.1f} bps"
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def get_rate_chart_data(
|
|
227
|
+
symbol: str,
|
|
228
|
+
long_exchange: str,
|
|
229
|
+
short_exchange: str,
|
|
230
|
+
days: int = 30,
|
|
231
|
+
) -> str:
|
|
232
|
+
"""Get funding rate spread chart data for a pair across two exchanges.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
236
|
+
long_exchange: First exchange
|
|
237
|
+
short_exchange: Second exchange
|
|
238
|
+
days: Lookback period (1-90, default 30)
|
|
239
|
+
"""
|
|
240
|
+
data = await client.get("/rates/chart-data", params={
|
|
241
|
+
"symbol": symbol,
|
|
242
|
+
"long_exchange": long_exchange,
|
|
243
|
+
"short_exchange": short_exchange,
|
|
244
|
+
"days": days,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
points = data if isinstance(data, list) else data.get("data", [])
|
|
248
|
+
|
|
249
|
+
if not points:
|
|
250
|
+
return f"No chart data for {symbol} ({long_exchange} vs {short_exchange})."
|
|
251
|
+
|
|
252
|
+
# Compute spread statistics
|
|
253
|
+
spreads = []
|
|
254
|
+
for p in points:
|
|
255
|
+
long_apr = p.get("long_apr", 0) or 0
|
|
256
|
+
short_apr = p.get("short_apr", 0) or 0
|
|
257
|
+
spreads.append(short_apr - long_apr)
|
|
258
|
+
|
|
259
|
+
avg_spread = sum(spreads) / len(spreads) if spreads else 0
|
|
260
|
+
max_spread = max(spreads) if spreads else 0
|
|
261
|
+
min_spread = min(spreads) if spreads else 0
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
f"{symbol} spread ({long_exchange} vs {short_exchange}) — last {days} days:\n"
|
|
265
|
+
f" Data points: {len(points)}\n"
|
|
266
|
+
f" Avg spread: {avg_spread:.1f}% APR\n"
|
|
267
|
+
f" Max spread: {max_spread:.1f}% APR\n"
|
|
268
|
+
f" Min spread: {min_spread:.1f}% APR"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ─── PAID TIER TOOLS ─────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
@mcp.tool()
|
|
275
|
+
async def get_pair_intelligence() -> str:
|
|
276
|
+
"""Get risk scores and backtested APR for all opportunity pairs.
|
|
277
|
+
Pre-computed every 5 minutes. Includes spread risk, 30d average, smart ranking.
|
|
278
|
+
[Requires paid API key]
|
|
279
|
+
"""
|
|
280
|
+
await _require_paid()
|
|
281
|
+
data = await client.get("/analytics/pair-intelligence")
|
|
282
|
+
|
|
283
|
+
pairs = data.get("pairs", {})
|
|
284
|
+
if not pairs:
|
|
285
|
+
return "No pair intelligence data available."
|
|
286
|
+
|
|
287
|
+
lines = [f"Pair intelligence ({len(pairs)} pairs, {data.get('days', 30)}d window):\n"]
|
|
288
|
+
|
|
289
|
+
# Sort by smart score (combines APR, stability, liquidity, depth)
|
|
290
|
+
sorted_pairs = sorted(pairs.items(), key=lambda x: x[1].get("smart_score", 0), reverse=True)
|
|
291
|
+
|
|
292
|
+
for pair_key, info in sorted_pairs[:15]:
|
|
293
|
+
bt_apr = info.get("backtested_net_apr", info.get("backtested_apr", 0))
|
|
294
|
+
score = info.get("smart_score", 0)
|
|
295
|
+
stability = info.get("stability_pct", 0)
|
|
296
|
+
risk = info.get("risk_level", "?")
|
|
297
|
+
depth = info.get("depth_score")
|
|
298
|
+
days = info.get("data_days", 0)
|
|
299
|
+
|
|
300
|
+
depth_label = {1.0: "$10k+", 0.8: "$5-10k", 0.5: "$1-5k", 0.2: "<$1k"}.get(depth, "?")
|
|
301
|
+
lines.append(
|
|
302
|
+
f" {pair_key} — score {score:.1f}, bt {bt_apr:.1f}% APR, "
|
|
303
|
+
f"stability {stability:.0f}%, depth {depth_label}, risk {risk}, {days:.0f}d data"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return "\n".join(lines)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@mcp.tool()
|
|
310
|
+
async def get_price_spread_data(
|
|
311
|
+
symbol: str,
|
|
312
|
+
long_exchange: str,
|
|
313
|
+
short_exchange: str,
|
|
314
|
+
days: int = 7,
|
|
315
|
+
) -> str:
|
|
316
|
+
"""Get hourly mark prices and spread between two exchanges for price risk analysis.
|
|
317
|
+
[Requires paid API key]
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
321
|
+
long_exchange: First exchange
|
|
322
|
+
short_exchange: Second exchange
|
|
323
|
+
days: Lookback period (1-30, default 7)
|
|
324
|
+
"""
|
|
325
|
+
await _require_paid()
|
|
326
|
+
data = await client.get("/prices/chart-data", params={
|
|
327
|
+
"symbol": symbol,
|
|
328
|
+
"long_exchange": long_exchange,
|
|
329
|
+
"short_exchange": short_exchange,
|
|
330
|
+
"days": days,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
points = data if isinstance(data, list) else data.get("data", [])
|
|
334
|
+
if not points:
|
|
335
|
+
return f"No price data for {symbol} ({long_exchange} vs {short_exchange})."
|
|
336
|
+
|
|
337
|
+
spreads = [p.get("spread_pct", 0) for p in points if p.get("spread_pct") is not None]
|
|
338
|
+
if not spreads:
|
|
339
|
+
return "Price data available but no spread calculations."
|
|
340
|
+
|
|
341
|
+
avg = sum(spreads) / len(spreads)
|
|
342
|
+
mx = max(spreads)
|
|
343
|
+
high_hours = sum(1 for s in spreads if s > 0.5)
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
f"{symbol} price spread ({long_exchange} vs {short_exchange}) — last {days} days:\n"
|
|
347
|
+
f" Data points: {len(spreads)}\n"
|
|
348
|
+
f" Avg spread: {avg:.4f}%\n"
|
|
349
|
+
f" Max spread: {mx:.4f}%\n"
|
|
350
|
+
f" Hours > 0.5% spread: {high_hours}"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@mcp.tool()
|
|
355
|
+
async def get_record_roi(period: str = "day") -> str:
|
|
356
|
+
"""Best single trade by ROI in a given period.
|
|
357
|
+
[Requires paid API key]
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
period: "day" for last 24h or "week" for last 7 days
|
|
361
|
+
"""
|
|
362
|
+
await _require_paid()
|
|
363
|
+
query = f"record_roi_{period}"
|
|
364
|
+
data = await client.get(f"/social/run/{query}")
|
|
365
|
+
result = data.get("data", {})
|
|
366
|
+
|
|
367
|
+
if not result:
|
|
368
|
+
return f"No ROI data for period: {period}"
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
f"Record ROI ({period}):\n"
|
|
372
|
+
f" {result.get('symbol', '?')} — "
|
|
373
|
+
f"Long {result.get('long_exchange', '?')} / Short {result.get('short_exchange', '?')}\n"
|
|
374
|
+
f" APR: {result.get('apr', 0):.1f}%\n"
|
|
375
|
+
f" ROI on $1000: ${result.get('roi_1k', 0):.2f}"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@mcp.tool()
|
|
380
|
+
async def get_still_paying() -> str:
|
|
381
|
+
"""Pairs above 50% APR for 24h+ that are still active right now.
|
|
382
|
+
[Requires paid API key]
|
|
383
|
+
"""
|
|
384
|
+
await _require_paid()
|
|
385
|
+
data = await client.get("/social/run/still_paying")
|
|
386
|
+
results = data.get("data", [])
|
|
387
|
+
|
|
388
|
+
if not results:
|
|
389
|
+
return "No pairs currently on a 24h+ streak above 50% APR."
|
|
390
|
+
|
|
391
|
+
lines = ["Active streaks (50%+ APR for 24h+):\n"]
|
|
392
|
+
for r in results[:10]:
|
|
393
|
+
lines.append(
|
|
394
|
+
f" {r.get('symbol', '?')} — {r.get('long_exchange', '?')}/{r.get('short_exchange', '?')} — "
|
|
395
|
+
f"{r.get('hours', 0)}h streak, {r.get('apr', 0):.1f}% APR"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return "\n".join(lines)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@mcp.tool()
|
|
402
|
+
async def get_top_holders(days: int = 7) -> str:
|
|
403
|
+
"""Top pairs holding 50%+ APR longest.
|
|
404
|
+
[Requires paid API key]
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
days: Lookback period — 7 or 14 days
|
|
408
|
+
"""
|
|
409
|
+
await _require_paid()
|
|
410
|
+
query = "top_holders" if days <= 7 else "top_holders_14d"
|
|
411
|
+
data = await client.get(f"/social/run/{query}")
|
|
412
|
+
results = data.get("data", [])
|
|
413
|
+
|
|
414
|
+
if not results:
|
|
415
|
+
return f"No pairs held 50%+ APR in the last {days} days."
|
|
416
|
+
|
|
417
|
+
lines = [f"Top holders (50%+ APR, last {days} days):\n"]
|
|
418
|
+
for r in results[:5]:
|
|
419
|
+
lines.append(
|
|
420
|
+
f" {r.get('symbol', '?')} — {r.get('long_exchange', '?')}/{r.get('short_exchange', '?')} — "
|
|
421
|
+
f"{r.get('hours', 0)}h above threshold"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return "\n".join(lines)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@mcp.tool()
|
|
428
|
+
async def get_momentum_movers() -> str:
|
|
429
|
+
"""Biggest funding spread jumps in the last 6 hours vs prior 6 hours.
|
|
430
|
+
[Requires paid API key]
|
|
431
|
+
"""
|
|
432
|
+
await _require_paid()
|
|
433
|
+
data = await client.get("/social/run/momentum_movers")
|
|
434
|
+
results = data.get("data", [])
|
|
435
|
+
|
|
436
|
+
if not results:
|
|
437
|
+
return "No significant momentum changes in the last 6 hours."
|
|
438
|
+
|
|
439
|
+
lines = ["Momentum movers (last 6h):\n"]
|
|
440
|
+
for r in results[:10]:
|
|
441
|
+
lines.append(
|
|
442
|
+
f" {r.get('symbol', '?')} — {r.get('long_exchange', '?')}/{r.get('short_exchange', '?')} — "
|
|
443
|
+
f"{r.get('change', 0):+.1f}% APR change"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return "\n".join(lines)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@mcp.tool()
|
|
450
|
+
async def get_weekly_recap(days: int = 7) -> str:
|
|
451
|
+
"""Weekly recap: best ROI pairs, best DEX combos, most stable pair.
|
|
452
|
+
[Requires paid API key]
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
days: Period — 7 or 14 days
|
|
456
|
+
"""
|
|
457
|
+
await _require_paid()
|
|
458
|
+
query = "weekly_recap" if days <= 7 else "weekly_recap_14d"
|
|
459
|
+
data = await client.get(f"/social/run/{query}")
|
|
460
|
+
result = data.get("data", {})
|
|
461
|
+
|
|
462
|
+
if not result:
|
|
463
|
+
return f"No recap data for the last {days} days."
|
|
464
|
+
|
|
465
|
+
lines = [f"Weekly recap (last {days} days):\n"]
|
|
466
|
+
|
|
467
|
+
if result.get("best_roi"):
|
|
468
|
+
roi = result["best_roi"]
|
|
469
|
+
lines.append(f" Best ROI: {roi.get('symbol', '?')} — {roi.get('apr', 0):.1f}% APR")
|
|
470
|
+
|
|
471
|
+
if result.get("best_dex_combo"):
|
|
472
|
+
combo = result["best_dex_combo"]
|
|
473
|
+
lines.append(f" Best DEX combo: {combo.get('long', '?')}/{combo.get('short', '?')}")
|
|
474
|
+
|
|
475
|
+
if result.get("most_stable"):
|
|
476
|
+
stable = result["most_stable"]
|
|
477
|
+
lines.append(f" Most stable: {stable.get('symbol', '?')} — {stable.get('std', 0):.1f}% std dev")
|
|
478
|
+
|
|
479
|
+
return "\n".join(lines)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@mcp.tool()
|
|
483
|
+
async def get_unbroken_streaks(mode: str = "top5") -> str:
|
|
484
|
+
"""Consecutive hours above APR threshold — multiple viewing modes.
|
|
485
|
+
[Requires paid API key]
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
mode: "spotlight" (random long streak), "top5" (longest 5), "fresh" (started <24h ago), "elite" (100%+ APR)
|
|
489
|
+
"""
|
|
490
|
+
await _require_paid()
|
|
491
|
+
query_map = {
|
|
492
|
+
"spotlight": "unbroken_streak",
|
|
493
|
+
"top5": "unbroken_streak_top5",
|
|
494
|
+
"fresh": "unbroken_streak_fresh",
|
|
495
|
+
"elite": "unbroken_streak_elite",
|
|
496
|
+
}
|
|
497
|
+
query = query_map.get(mode, "unbroken_streak_top5")
|
|
498
|
+
data = await client.get(f"/social/run/{query}")
|
|
499
|
+
results = data.get("data", [])
|
|
500
|
+
|
|
501
|
+
if isinstance(results, dict):
|
|
502
|
+
results = [results]
|
|
503
|
+
|
|
504
|
+
if not results:
|
|
505
|
+
return f"No unbroken streaks found (mode: {mode})."
|
|
506
|
+
|
|
507
|
+
lines = [f"Unbroken streaks ({mode}):\n"]
|
|
508
|
+
for r in results[:10]:
|
|
509
|
+
lines.append(
|
|
510
|
+
f" {r.get('symbol', '?')} — {r.get('long_exchange', '?')}/{r.get('short_exchange', '?')} — "
|
|
511
|
+
f"{r.get('hours', 0)}h streak at {r.get('apr', 0):.1f}% APR"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return "\n".join(lines)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@mcp.tool()
|
|
518
|
+
async def get_live_alpha() -> str:
|
|
519
|
+
"""Top 5 live funding rate arbitrage opportunities, deduplicated by symbol.
|
|
520
|
+
[Requires paid API key]
|
|
521
|
+
"""
|
|
522
|
+
await _require_paid()
|
|
523
|
+
data = await client.get("/social/run/live_alpha")
|
|
524
|
+
results = data.get("data", [])
|
|
525
|
+
|
|
526
|
+
if not results:
|
|
527
|
+
return "No live alpha opportunities right now."
|
|
528
|
+
|
|
529
|
+
lines = ["Live alpha (top 5):\n"]
|
|
530
|
+
for r in results[:5]:
|
|
531
|
+
lines.append(
|
|
532
|
+
f" {r.get('symbol', '?')} — Long {r.get('long_exchange', '?')} / "
|
|
533
|
+
f"Short {r.get('short_exchange', '?')} — {r.get('apr', 0):.1f}% APR"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return "\n".join(lines)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@mcp.tool()
|
|
540
|
+
async def find_best_trade(
|
|
541
|
+
position_size: float = 1000,
|
|
542
|
+
min_apr: float = 30,
|
|
543
|
+
limit: int = 5,
|
|
544
|
+
) -> str:
|
|
545
|
+
"""Find the best delta-neutral trade you can open RIGHT NOW.
|
|
546
|
+
Combines live funding rates + 30d backtest + spread risk + real-time order book depth.
|
|
547
|
+
Returns fully-analyzed, tradeable opportunities ranked by composite risk/reward score.
|
|
548
|
+
[Requires paid API key]
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
position_size: How much USD you want to deploy (default 1000)
|
|
552
|
+
min_apr: Minimum net APR filter (default 30)
|
|
553
|
+
limit: Max results (default 5)
|
|
554
|
+
"""
|
|
555
|
+
await _require_paid()
|
|
556
|
+
data = await client.get("/smart-opportunities", params={
|
|
557
|
+
"position_size": position_size,
|
|
558
|
+
"min_apr": min_apr,
|
|
559
|
+
"limit": limit,
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
opps = data.get("opportunities", [])
|
|
563
|
+
if not opps:
|
|
564
|
+
return f"No tradeable opportunities found at ${position_size:,.0f} with ≥{min_apr}% APR."
|
|
565
|
+
|
|
566
|
+
lines = [f"Best trades for ${position_size:,.0f} ({len(opps)} found):\n"]
|
|
567
|
+
|
|
568
|
+
for i, o in enumerate(opps, 1):
|
|
569
|
+
depth = o.get("depth", {})
|
|
570
|
+
entry_cost = o.get("entry_cost_pct", 0)
|
|
571
|
+
stability = o.get("stability_pct", 0)
|
|
572
|
+
risk = o.get("risk_level", "unknown")
|
|
573
|
+
bt_apr = o.get("backtested_apr", 0)
|
|
574
|
+
days = o.get("data_days", 0)
|
|
575
|
+
|
|
576
|
+
lines.append(f" #{i} {o['symbol']} — Long {o['long_exchange']} / Short {o['short_exchange']}")
|
|
577
|
+
lines.append(f" Live APR: {o['net_apr']:.1f}% | 30d backtested: {bt_apr:.1f}% | Stability: {stability:.0f}%")
|
|
578
|
+
lines.append(f" Entry cost: {entry_cost:.4f}% slippage | Breakeven: {o['breakeven_days']:.1f} days")
|
|
579
|
+
lines.append(f" Risk: {risk} | Price spread: {o.get('spread_risk_pct', 0):.3f}% | Data: {days:.0f} days")
|
|
580
|
+
lines.append(f" Score: {o['score']:.1f}")
|
|
581
|
+
lines.append("")
|
|
582
|
+
|
|
583
|
+
return "\n".join(lines)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@mcp.tool()
|
|
587
|
+
async def get_smart_opportunities(
|
|
588
|
+
position_size: float = 1000,
|
|
589
|
+
min_apr: float = 50,
|
|
590
|
+
limit: int = 10,
|
|
591
|
+
) -> str:
|
|
592
|
+
"""Get opportunities ranked by composite score (APR × stability × liquidity).
|
|
593
|
+
Unlike get_opportunities, this checks real order book depth and filters out
|
|
594
|
+
pairs you can't actually trade at your position size.
|
|
595
|
+
[Requires paid API key]
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
position_size: Position size in USD (default 1000)
|
|
599
|
+
min_apr: Minimum net APR (default 50)
|
|
600
|
+
limit: Max results (default 10)
|
|
601
|
+
"""
|
|
602
|
+
await _require_paid()
|
|
603
|
+
data = await client.get("/smart-opportunities", params={
|
|
604
|
+
"position_size": position_size,
|
|
605
|
+
"min_apr": min_apr,
|
|
606
|
+
"limit": limit,
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
opps = data.get("opportunities", [])
|
|
610
|
+
if not opps:
|
|
611
|
+
return f"No tradeable opportunities at ${position_size:,.0f} with ≥{min_apr}% APR."
|
|
612
|
+
|
|
613
|
+
lines = [f"Smart opportunities for ${position_size:,.0f} ({len(opps)} results):\n"]
|
|
614
|
+
for o in opps:
|
|
615
|
+
risk_emoji = {"low": "safe", "medium": "moderate", "high": "risky", "unknown": "no data"}.get(o.get("risk_level", "unknown"), "?")
|
|
616
|
+
lines.append(
|
|
617
|
+
f" {o['symbol']} — {o['long_exchange']}/{o['short_exchange']} — "
|
|
618
|
+
f"{o['net_apr']:.1f}% APR, {o.get('entry_cost_pct', 0):.3f}% entry cost, "
|
|
619
|
+
f"risk: {risk_emoji}, score: {o['score']:.1f}"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
return "\n".join(lines)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@mcp.tool()
|
|
626
|
+
async def check_liquidity(
|
|
627
|
+
exchange: str,
|
|
628
|
+
symbol: str,
|
|
629
|
+
position_size: float = 1000,
|
|
630
|
+
) -> str:
|
|
631
|
+
"""Check real-time order book liquidity for a symbol on a specific exchange.
|
|
632
|
+
Returns bid-ask spread, depth analysis at multiple position sizes, and slippage estimates.
|
|
633
|
+
[Requires paid API key]
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
exchange: Exchange name (e.g. "Hyperliquid")
|
|
637
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
638
|
+
position_size: Position size in USD for slippage estimate (default 1000)
|
|
639
|
+
"""
|
|
640
|
+
await _require_paid()
|
|
641
|
+
data = await client.get(f"/liquidity/{exchange}", params={
|
|
642
|
+
"symbol": symbol,
|
|
643
|
+
"position_size": position_size,
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
if not data.get("available"):
|
|
647
|
+
return f"No order book data for {symbol} on {exchange}: {data.get('message', 'unavailable')}"
|
|
648
|
+
|
|
649
|
+
lines = [f"Liquidity: {symbol} on {data['exchange']}\n"]
|
|
650
|
+
lines.append(f" Spread: {data['spread_pct']:.4f}% ({data['bid_levels']} bid / {data['ask_levels']} ask levels)")
|
|
651
|
+
lines.append(f" Best bid: {data['best_bid']} Best ask: {data['best_ask']}")
|
|
652
|
+
|
|
653
|
+
if data.get("volume_24h"):
|
|
654
|
+
lines.append(f" 24h volume: ${data['volume_24h']:,.0f}")
|
|
655
|
+
if data.get("open_interest"):
|
|
656
|
+
lines.append(f" Open interest: ${data['open_interest']:,.0f}")
|
|
657
|
+
|
|
658
|
+
depth = data.get("depth", {})
|
|
659
|
+
if depth:
|
|
660
|
+
lines.append("\n Depth analysis:")
|
|
661
|
+
for size_label, sides in depth.items():
|
|
662
|
+
buy = sides.get("buy", {})
|
|
663
|
+
sell = sides.get("sell", {})
|
|
664
|
+
buy_slip = f"{buy['slippage_pct']:.4f}%" if buy.get("slippage_pct") is not None else "insufficient depth"
|
|
665
|
+
sell_slip = f"{sell['slippage_pct']:.4f}%" if sell.get("slippage_pct") is not None else "insufficient depth"
|
|
666
|
+
buy_fill = buy.get("filled_pct", 0)
|
|
667
|
+
sell_fill = sell.get("filled_pct", 0)
|
|
668
|
+
lines.append(f" {size_label}: Buy slippage {buy_slip} ({buy_fill}% filled) | Sell slippage {sell_slip} ({sell_fill}% filled)")
|
|
669
|
+
|
|
670
|
+
return "\n".join(lines)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@mcp.tool()
|
|
674
|
+
async def analyze_pair(
|
|
675
|
+
symbol: str,
|
|
676
|
+
long_exchange: str,
|
|
677
|
+
short_exchange: str,
|
|
678
|
+
position_size: float = 1000,
|
|
679
|
+
) -> str:
|
|
680
|
+
"""Full end-to-end analysis of a specific delta-neutral pair.
|
|
681
|
+
Combines: current funding rates, 7d backtest, pair intelligence (30d risk/stability),
|
|
682
|
+
live order book depth on both legs, and price spread risk — all in one call.
|
|
683
|
+
[Requires paid API key]
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
687
|
+
long_exchange: Exchange to go long on
|
|
688
|
+
short_exchange: Exchange to go short on
|
|
689
|
+
position_size: Your intended position size in USD (default 1000)
|
|
690
|
+
"""
|
|
691
|
+
await _require_paid()
|
|
692
|
+
import asyncio
|
|
693
|
+
|
|
694
|
+
# Fetch everything in parallel
|
|
695
|
+
opps_task = client.get("/opportunities", params={"symbol": symbol})
|
|
696
|
+
backtest_task = client.post("/backtest", json={
|
|
697
|
+
"symbol": symbol,
|
|
698
|
+
"long_exchange": long_exchange,
|
|
699
|
+
"short_exchange": short_exchange,
|
|
700
|
+
"position_size": position_size,
|
|
701
|
+
"days": 7,
|
|
702
|
+
"execution_fee_bps": 10,
|
|
703
|
+
})
|
|
704
|
+
intel_task = client.get("/analytics/pair-intelligence")
|
|
705
|
+
long_liq_task = client.get(f"/liquidity/{long_exchange}", params={
|
|
706
|
+
"symbol": symbol, "position_size": position_size,
|
|
707
|
+
})
|
|
708
|
+
short_liq_task = client.get(f"/liquidity/{short_exchange}", params={
|
|
709
|
+
"symbol": symbol, "position_size": position_size,
|
|
710
|
+
})
|
|
711
|
+
spread_task = client.get("/prices/chart-data", params={
|
|
712
|
+
"symbol": symbol,
|
|
713
|
+
"long_exchange": long_exchange,
|
|
714
|
+
"short_exchange": short_exchange,
|
|
715
|
+
"days": 7,
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
results = await asyncio.gather(
|
|
719
|
+
opps_task, backtest_task, intel_task,
|
|
720
|
+
long_liq_task, short_liq_task, spread_task,
|
|
721
|
+
return_exceptions=True,
|
|
722
|
+
)
|
|
723
|
+
opps, backtest, intel_data, long_liq, short_liq, price_spread = results
|
|
724
|
+
|
|
725
|
+
lines = [f"═══ Analysis: {symbol} — Long {long_exchange} / Short {short_exchange} ═══\n"]
|
|
726
|
+
|
|
727
|
+
# 1. Current funding rates
|
|
728
|
+
lines.append("── Current Funding ──")
|
|
729
|
+
if not isinstance(opps, Exception):
|
|
730
|
+
found = None
|
|
731
|
+
for o in (opps if isinstance(opps, list) else []):
|
|
732
|
+
if (o.get("long_exchange") == long_exchange and
|
|
733
|
+
o.get("short_exchange") == short_exchange and
|
|
734
|
+
o.get("symbol") == symbol):
|
|
735
|
+
found = o
|
|
736
|
+
break
|
|
737
|
+
if found:
|
|
738
|
+
lines.append(f" Long APR: {found['long_apr']:+.1f}% | Short APR: {found['short_apr']:+.1f}%")
|
|
739
|
+
lines.append(f" Net APR: {found['net_apr']:.1f}% | Breakeven: {found.get('breakeven_days', '?')} days")
|
|
740
|
+
else:
|
|
741
|
+
lines.append(f" Pair not found in current opportunities")
|
|
742
|
+
else:
|
|
743
|
+
lines.append(f" Failed to fetch rates")
|
|
744
|
+
|
|
745
|
+
# 2. Backtest
|
|
746
|
+
lines.append("\n── 7-Day Backtest ──")
|
|
747
|
+
if not isinstance(backtest, Exception):
|
|
748
|
+
lines.append(f" PnL: ${backtest.get('total_pnl', 0):,.2f} ({backtest.get('pnl_percentage', 0):+.2f}%)")
|
|
749
|
+
lines.append(f" Funding: ${backtest.get('total_funding_received', 0):,.2f} | Fees: ${backtest.get('total_fees_paid', 0):,.2f}")
|
|
750
|
+
lines.append(f" Sharpe: {backtest.get('sharpe_ratio', 0):.2f}")
|
|
751
|
+
if backtest.get("has_price_data") and backtest.get("spread_pnl") is not None:
|
|
752
|
+
lines.append(f" Price spread PnL: ${backtest['spread_pnl']:,.2f}")
|
|
753
|
+
hist_slip = backtest.get("historical_slippage_bps")
|
|
754
|
+
if hist_slip is not None:
|
|
755
|
+
lines.append(f" Historical slippage: {hist_slip:.1f} bps")
|
|
756
|
+
else:
|
|
757
|
+
lines.append(f" Backtest failed")
|
|
758
|
+
|
|
759
|
+
# 3. Pair intelligence
|
|
760
|
+
lines.append("\n── 30-Day Intelligence ──")
|
|
761
|
+
if not isinstance(intel_data, Exception):
|
|
762
|
+
pair_key = f"{symbol}|{long_exchange}|{short_exchange}"
|
|
763
|
+
intel = intel_data.get("pairs", {}).get(pair_key, {})
|
|
764
|
+
if intel:
|
|
765
|
+
depth_label = {1.0: "$10k+", 0.8: "$5-10k", 0.5: "$1-5k", 0.2: "<$1k"}.get(
|
|
766
|
+
intel.get("depth_score"), "unknown")
|
|
767
|
+
lines.append(f" Smart score: {intel.get('smart_score', 0):.1f}")
|
|
768
|
+
lines.append(f" Backtested APR: {intel.get('backtested_net_apr', 0):.1f}%")
|
|
769
|
+
lines.append(f" Stability: {intel.get('stability_pct', 0):.0f}% of hours profitable")
|
|
770
|
+
lines.append(f" Risk level: {intel.get('risk_level', 'unknown')}")
|
|
771
|
+
lines.append(f" Depth tier: {depth_label}")
|
|
772
|
+
lines.append(f" Data: {intel.get('data_days', 0):.1f} days of history")
|
|
773
|
+
else:
|
|
774
|
+
lines.append(f" No intelligence data for this pair")
|
|
775
|
+
else:
|
|
776
|
+
lines.append(f" Intelligence fetch failed")
|
|
777
|
+
|
|
778
|
+
# 4. Live liquidity — both legs
|
|
779
|
+
for side, liq, ex_name in [("Long", long_liq, long_exchange), ("Short", short_liq, short_exchange)]:
|
|
780
|
+
lines.append(f"\n── {side} Leg: {ex_name} ──")
|
|
781
|
+
if isinstance(liq, Exception):
|
|
782
|
+
lines.append(f" Liquidity check failed")
|
|
783
|
+
continue
|
|
784
|
+
if not liq.get("available"):
|
|
785
|
+
lines.append(f" {liq.get('message', 'No order book data available')}")
|
|
786
|
+
continue
|
|
787
|
+
lines.append(f" Spread: {liq['spread_pct']:.4f}% | Levels: {liq['bid_levels']} bid / {liq['ask_levels']} ask")
|
|
788
|
+
if liq.get("volume_24h"):
|
|
789
|
+
lines.append(f" 24h volume: ${liq['volume_24h']:,.0f}")
|
|
790
|
+
depth = liq.get("depth", {})
|
|
791
|
+
size_key = f"${int(position_size)}"
|
|
792
|
+
if size_key not in depth:
|
|
793
|
+
size_key = "$1000" # fallback
|
|
794
|
+
if size_key in depth:
|
|
795
|
+
buy = depth[size_key].get("buy", {})
|
|
796
|
+
sell = depth[size_key].get("sell", {})
|
|
797
|
+
buy_slip = f"{buy['slippage_pct']:.4f}%" if buy.get("slippage_pct") is not None else f"{buy.get('filled_pct', 0):.0f}% filled"
|
|
798
|
+
sell_slip = f"{sell['slippage_pct']:.4f}%" if sell.get("slippage_pct") is not None else f"{sell.get('filled_pct', 0):.0f}% filled"
|
|
799
|
+
lines.append(f" At {size_key}: buy slippage {buy_slip} | sell slippage {sell_slip}")
|
|
800
|
+
|
|
801
|
+
# 5. Price spread risk
|
|
802
|
+
lines.append("\n── Price Spread Risk (7d) ──")
|
|
803
|
+
if not isinstance(price_spread, Exception):
|
|
804
|
+
points = price_spread if isinstance(price_spread, list) else price_spread.get("data", [])
|
|
805
|
+
if points:
|
|
806
|
+
spreads = [p.get("spread_pct", 0) for p in points if p.get("spread_pct") is not None]
|
|
807
|
+
if spreads:
|
|
808
|
+
lines.append(f" Avg divergence: {sum(spreads)/len(spreads):.4f}%")
|
|
809
|
+
lines.append(f" Max divergence: {max(spreads):.4f}%")
|
|
810
|
+
lines.append(f" Data points: {len(spreads)}")
|
|
811
|
+
else:
|
|
812
|
+
lines.append(f" No spread data in response")
|
|
813
|
+
else:
|
|
814
|
+
lines.append(f" No price history between these exchanges")
|
|
815
|
+
else:
|
|
816
|
+
lines.append(f" Price spread fetch failed")
|
|
817
|
+
|
|
818
|
+
return "\n".join(lines)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
@mcp.tool()
|
|
822
|
+
async def compare_exchanges(
|
|
823
|
+
symbol: str,
|
|
824
|
+
exchanges: str = "",
|
|
825
|
+
) -> str:
|
|
826
|
+
"""Compare the same symbol across multiple exchanges — funding rates, spread, depth, volume.
|
|
827
|
+
Helps decide which exchange to use for each leg of a delta-neutral trade.
|
|
828
|
+
[Requires paid API key]
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
symbol: Trading pair (e.g. "ETH/USDC")
|
|
832
|
+
exchanges: Comma-separated exchange names to compare (e.g. "Hyperliquid,dYdX,Aster"). If empty, shows all exchanges that list this symbol.
|
|
833
|
+
"""
|
|
834
|
+
await _require_paid()
|
|
835
|
+
import asyncio
|
|
836
|
+
|
|
837
|
+
# Get opportunities to find which exchanges list this symbol
|
|
838
|
+
opps = await client.get("/opportunities", params={"symbol": symbol})
|
|
839
|
+
if isinstance(opps, dict) and "detail" in opps:
|
|
840
|
+
return f"Error: {opps['detail']}"
|
|
841
|
+
|
|
842
|
+
# Collect unique exchanges and their raw funding rate for this symbol
|
|
843
|
+
# Raw rate: positive = longs pay shorts. short_apr = raw rate, long_apr = -raw rate.
|
|
844
|
+
exchange_set = set()
|
|
845
|
+
exchange_rates = {}
|
|
846
|
+
for o in (opps if isinstance(opps, list) else []):
|
|
847
|
+
if o.get("symbol") != symbol:
|
|
848
|
+
continue
|
|
849
|
+
if o["long_exchange"] not in exchange_rates:
|
|
850
|
+
exchange_rates[o["long_exchange"]] = -o["long_apr"] # negate: long_apr is PnL when long
|
|
851
|
+
if o["short_exchange"] not in exchange_rates:
|
|
852
|
+
exchange_rates[o["short_exchange"]] = o["short_apr"] # short_apr = raw rate
|
|
853
|
+
exchange_set.add(o["long_exchange"])
|
|
854
|
+
exchange_set.add(o["short_exchange"])
|
|
855
|
+
|
|
856
|
+
if exchanges:
|
|
857
|
+
filter_set = {e.strip() for e in exchanges.split(",")}
|
|
858
|
+
exchange_set = exchange_set & filter_set
|
|
859
|
+
if not exchange_set:
|
|
860
|
+
return f"None of [{exchanges}] list {symbol}. Available: {', '.join(sorted(exchange_rates.keys()))}"
|
|
861
|
+
|
|
862
|
+
if not exchange_set:
|
|
863
|
+
return f"No exchanges found for {symbol}"
|
|
864
|
+
|
|
865
|
+
# Fetch liquidity for each exchange in parallel
|
|
866
|
+
liq_results = await asyncio.gather(
|
|
867
|
+
*[client.get(f"/liquidity/{ex}", params={"symbol": symbol, "position_size": 5000})
|
|
868
|
+
for ex in sorted(exchange_set)],
|
|
869
|
+
return_exceptions=True,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
lines = [f"═══ {symbol} — Exchange Comparison ═══\n"]
|
|
873
|
+
|
|
874
|
+
for ex, liq in zip(sorted(exchange_set), liq_results):
|
|
875
|
+
apr = exchange_rates.get(ex)
|
|
876
|
+
apr_str = f"{apr:+.1f}%" if apr is not None else "?"
|
|
877
|
+
|
|
878
|
+
lines.append(f" {ex}")
|
|
879
|
+
lines.append(f" Funding APR: {apr_str}")
|
|
880
|
+
|
|
881
|
+
if isinstance(liq, Exception) or not liq.get("available"):
|
|
882
|
+
lines.append(f" Liquidity: no data")
|
|
883
|
+
else:
|
|
884
|
+
lines.append(f" Spread: {liq['spread_pct']:.4f}%")
|
|
885
|
+
if liq.get("volume_24h"):
|
|
886
|
+
lines.append(f" 24h volume: ${liq['volume_24h']:,.0f}")
|
|
887
|
+
depth = liq.get("depth", {})
|
|
888
|
+
d5k = depth.get("$5000", {}).get("buy", {})
|
|
889
|
+
if d5k.get("slippage_pct") is not None:
|
|
890
|
+
lines.append(f" $5k buy slippage: {d5k['slippage_pct']:.4f}%")
|
|
891
|
+
elif d5k.get("filled_pct") is not None:
|
|
892
|
+
lines.append(f" $5k fill: {d5k['filled_pct']:.0f}%")
|
|
893
|
+
lines.append("")
|
|
894
|
+
|
|
895
|
+
return "\n".join(lines)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
# ─── Server entry point ──────────────────────────────────────
|
|
899
|
+
|
|
900
|
+
def main():
|
|
901
|
+
"""Run the MCP server (stdio transport)."""
|
|
902
|
+
mcp.run(transport="stdio")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
if __name__ == "__main__":
|
|
906
|
+
main()
|