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.
@@ -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,3 @@
1
+ """ProFunding MCP Server — funding rate arbitrage data for AI assistants."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()