Add historical data

This commit is contained in:
Nayshins 2024-12-24 12:50:21 -08:00
parent 2b90e9b11e
commit b16f836f0e
2 changed files with 376 additions and 8 deletions

195
README.md
View File

@ -0,0 +1,195 @@
# Cryptocurrency Market Data MCP Server
A Model Context Protocol (MCP) server that provides real-time and historical cryptocurrency market data through integration with major exchanges. This server enables LLMs like Claude to fetch current prices, analyze market trends, and access detailed trading information.
[![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io)
[![Python](https://img.shields.io/badge/Python-3.9%2B-blue)](https://www.python.org)
[![CCXT](https://img.shields.io/badge/CCXT-Powered-green)](https://github.com/ccxt/ccxt)
## Features
- **Real-time Market Data**
- Current cryptocurrency prices
- Market summaries with bid/ask spreads
- Top trading pairs by volume
- Multiple exchange support
- **Historical Analysis**
- OHLCV (candlestick) data
- Price change statistics
- Volume history tracking
- Customizable timeframes
- **Exchange Support**
- Binance
- Coinbase
- Kraken
- KuCoin
- HyperLiquid
- Huobi
- Bitfinex
- Bybit
- OKX
- MEXC
## Installation
```bash
# Using uv (recommended)
uv pip install mcp ccxt
# Using pip
pip install mcp ccxt
```
## Usage
### Running the Server
```bash
python crypto_server.py
```
### Connecting with Claude Desktop
1. Open your Claude Desktop configuration at:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
2. Add the server configuration:
```json
{
"mcpServers": {
"crypto": {
"command": "python",
"args": ["/path/to/crypto_server.py"]
}
}
}
```
3. Restart Claude Desktop
### Available Tools
1. **get-price**
- Get current price for any trading pair
- Example: "What's the current price of BTC/USDT on Binance?"
2. **get-market-summary**
- Fetch detailed market information
- Example: "Show me a market summary for ETH/USDT"
3. **get-top-volumes**
- List top trading pairs by volume
- Example: "What are the top 5 trading pairs on Kraken?"
4. **list-exchanges**
- Show all supported exchanges
- Example: "Which exchanges are supported?"
5. **get-historical-ohlcv**
- Get historical candlestick data
- Example: "Show me the last 7 days of BTC/USDT price data in 1-hour intervals"
6. **get-price-change**
- Calculate price changes over different timeframes
- Example: "What's the 24-hour price change for SOL/USDT?"
7. **get-volume-history**
- Track trading volume over time
- Example: "Show me the trading volume history for ETH/USDT over the last week"
### Example Queries
Here are some example questions you can ask Claude once the server is connected:
```
- What's the current Bitcoin price on Binance?
- Show me the top 5 trading pairs by volume on Coinbase
- How has ETH/USDT performed over the last 24 hours?
- Give me a detailed market summary for SOL/USDT on Kraken
- What's the trading volume history for BNB/USDT over the last week?
```
## Technical Details
### Dependencies
- `mcp`: Model Context Protocol SDK
- `ccxt`: Cryptocurrency Exchange Trading Library
- Python 3.9 or higher
### Architecture
The server uses:
- CCXT's async support for efficient exchange communication
- MCP's tool system for LLM integration
- Standardized data formatting for consistent outputs
- Connection pooling for optimal performance
### Error Handling
The server implements robust error handling for:
- Invalid trading pairs
- Exchange connectivity issues
- Rate limiting
- Malformed requests
- Network timeouts
## Development
### Running Tests
```bash
# To be implemented
pytest tests/
```
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
### Local Development
```bash
# Clone the repository
git clone [repository-url]
cd crypto-mcp-server
# Install dependencies
uv pip install -e .
```
## Troubleshooting
### Common Issues
1. **Exchange Connection Errors**
- Check your internet connection
- Verify the exchange is operational
- Ensure the trading pair exists on the selected exchange
2. **Rate Limiting**
- Implement delays between requests
- Use different exchanges for high-frequency queries
- Check exchange-specific rate limits
3. **Data Formatting Issues**
- Verify trading pair format (e.g., BTC/USDT, not BTCUSDT)
- Check timeframe specifications
- Ensure numerical parameters are within valid ranges
## License
MIT License - See LICENSE file for details
## Acknowledgments
- [CCXT](https://github.com/ccxt/ccxt) for exchange integrations
- [Model Context Protocol](https://modelcontextprotocol.io) for the MCP specification
- The cryptocurrency exchanges for providing market data APIs

View File

@ -5,6 +5,7 @@ import mcp.types as types
from mcp.server import Server, NotificationOptions from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions from mcp.server.models import InitializationOptions
import mcp.server.stdio import mcp.server.stdio
from datetime import datetime, timedelta
# Initialize server # Initialize server
server = Server("crypto-server") server = Server("crypto-server")
@ -15,7 +16,7 @@ SUPPORTED_EXCHANGES = {
'coinbase': ccxt.coinbase, 'coinbase': ccxt.coinbase,
'kraken': ccxt.kraken, 'kraken': ccxt.kraken,
'kucoin': ccxt.kucoin, 'kucoin': ccxt.kucoin,
'ftx': ccxt.hyperliquid, 'hyperliquid': ccxt.hyperliquid,
'huobi': ccxt.huobi, 'huobi': ccxt.huobi,
'bitfinex': ccxt.bitfinex, 'bitfinex': ccxt.bitfinex,
'bybit': ccxt.bybit, 'bybit': ccxt.bybit,
@ -26,6 +27,7 @@ SUPPORTED_EXCHANGES = {
# Exchange instances cache # Exchange instances cache
exchange_instances = {} exchange_instances = {}
async def get_exchange(exchange_id: str) -> ccxt.Exchange: async def get_exchange(exchange_id: str) -> ccxt.Exchange:
"""Get or create an exchange instance.""" """Get or create an exchange instance."""
exchange_id = exchange_id.lower() exchange_id = exchange_id.lower()
@ -38,6 +40,7 @@ async def get_exchange(exchange_id: str) -> ccxt.Exchange:
return exchange_instances[exchange_id] return exchange_instances[exchange_id]
async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str: async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
"""Format ticker data into a readable string.""" """Format ticker data into a readable string."""
return ( return (
@ -52,6 +55,7 @@ async def format_ticker(ticker: Dict[str, Any], exchange_id: str) -> str:
"---" "---"
) )
def get_exchange_schema() -> Dict[str, Any]: def get_exchange_schema() -> Dict[str, Any]:
"""Get the JSON schema for exchange selection.""" """Get the JSON schema for exchange selection."""
return { return {
@ -61,10 +65,43 @@ def get_exchange_schema() -> Dict[str, Any]:
"default": "binance" "default": "binance"
} }
def format_ohlcv_data(ohlcv_data: List[List], timeframe: str) -> str:
"""Format OHLCV data into a readable string with price changes."""
formatted_data = []
for i, candle in enumerate(ohlcv_data):
timestamp, open_price, high, low, close, volume = candle
# Calculate price change from previous close if available
price_change = ""
if i > 0:
prev_close = ohlcv_data[i-1][4]
change_pct = ((close - prev_close) / prev_close) * 100
price_change = f"Change: {change_pct:+.2f}%"
# Format the candle data
dt = datetime.fromtimestamp(timestamp/1000).strftime('%Y-%m-%d %H:%M:%S')
candle_str = (
f"Time: {dt}\n"
f"Open: {open_price:.8f}\n"
f"High: {high:.8f}\n"
f"Low: {low:.8f}\n"
f"Close: {close:.8f}\n"
f"Volume: {volume:.2f}\n"
f"{price_change}\n"
"---"
)
formatted_data.append(candle_str)
return "\n".join(formatted_data)
@server.list_tools() @server.list_tools()
async def handle_list_tools() -> List[types.Tool]: async def handle_list_tools() -> List[types.Tool]:
"""List available cryptocurrency tools.""" """List available cryptocurrency tools."""
return [ return [
# Market Data Tools
types.Tool( types.Tool(
name="get-price", name="get-price",
description="Get current price of a cryptocurrency pair from a specific exchange", description="Get current price of a cryptocurrency pair from a specific exchange",
@ -116,7 +153,71 @@ async def handle_list_tools() -> List[types.Tool]:
"type": "object", "type": "object",
"properties": {} "properties": {}
}, },
) ),
# Historical Data Tools
types.Tool(
name="get-historical-ohlcv",
description="Get historical OHLCV (candlestick) data for a trading pair",
inputSchema={
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
},
"timeframe": {
"type": "string",
"description": "Timeframe for candlesticks (e.g., 1m, 5m, 15m, 1h, 4h, 1d)",
"enum": ["1m", "5m", "15m", "1h", "4h", "1d"],
"default": "1h"
},
"days_back": {
"type": "number",
"description": "Number of days of historical data to fetch (default: 7, max: 30)",
"default": 7,
"maximum": 30
},
"exchange": get_exchange_schema()
},
"required": ["symbol"],
},
),
types.Tool(
name="get-price-change",
description="Get price change statistics over different time periods",
inputSchema={
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
},
"exchange": get_exchange_schema()
},
"required": ["symbol"],
},
),
types.Tool(
name="get-volume-history",
description="Get trading volume history over time",
inputSchema={
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Trading pair symbol (e.g., BTC/USDT, ETH/USDT)",
},
"days": {
"type": "number",
"description": "Number of days of volume history (default: 7, max: 30)",
"default": 7,
"maximum": 30
},
"exchange": get_exchange_schema()
},
"required": ["symbol"],
},
),
] ]
@server.call_tool() @server.call_tool()
@ -184,6 +285,78 @@ async def handle_call_tool(
) )
] ]
elif name == "get-historical-ohlcv":
symbol = arguments.get("symbol", "").upper()
timeframe = arguments.get("timeframe", "1h")
days_back = min(int(arguments.get("days_back", 7)), 30)
# Calculate timestamps
since = int((datetime.now() - timedelta(days=days_back)).timestamp() * 1000)
# Fetch historical data
ohlcv = await exchange.fetch_ohlcv(symbol, timeframe, since=since)
formatted_data = format_ohlcv_data(ohlcv, timeframe)
return [
types.TextContent(
type="text",
text=f"Historical OHLCV data for {symbol} ({timeframe}) on {exchange_id.upper()}:\n\n{formatted_data}"
)
]
elif name == "get-price-change":
symbol = arguments.get("symbol", "").upper()
# Get current price
ticker = await exchange.fetch_ticker(symbol)
current_price = ticker['last']
# Get historical prices
timeframes = {
"1h": (1, "1h"),
"24h": (1, "1d"),
"7d": (7, "1d"),
"30d": (30, "1d")
}
changes = []
for label, (days, timeframe) in timeframes.items():
since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
ohlcv = await exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=1)
if ohlcv:
start_price = ohlcv[0][1] # Open price
change_pct = ((current_price - start_price) / start_price) * 100
changes.append(f"{label} change: {change_pct:+.2f}%")
return [
types.TextContent(
type="text",
text=f"Price changes for {symbol} on {exchange_id.upper()}:\n\n" + "\n".join(changes)
)
]
elif name == "get-volume-history":
symbol = arguments.get("symbol", "").upper()
days = min(int(arguments.get("days", 7)), 30)
# Get daily volume data
since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
ohlcv = await exchange.fetch_ohlcv(symbol, "1d", since=since)
volume_data = []
for candle in ohlcv:
timestamp, _, _, _, _, volume = candle
dt = datetime.fromtimestamp(timestamp/1000).strftime('%Y-%m-%d')
volume_data.append(f"{dt}: {volume:,.2f}")
return [
types.TextContent(
type="text",
text=f"Daily trading volume history for {symbol} on {exchange_id.upper()}:\n\n" +
"\n".join(volume_data)
)
]
else: else:
raise ValueError(f"Unknown tool: {name}") raise ValueError(f"Unknown tool: {name}")
@ -201,12 +374,6 @@ async def handle_call_tool(
exchange_instances.clear() exchange_instances.clear()
def run_server():
"""Wrapper to run the async main function"""
asyncio.run(main())
async def main(): async def main():
"""Run the server using stdin/stdout streams.""" """Run the server using stdin/stdout streams."""
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
@ -223,5 +390,11 @@ async def main():
), ),
) )
def run_server():
"""Wrapper to run the async main function"""
asyncio.run(main())
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())