Upload 10 files
Browse files- .dockerignore +5 -20
- Dockerfile +7 -6
- README.md +15 -18
- main.py +98 -178
.dockerignore
CHANGED
|
@@ -2,30 +2,15 @@ __pycache__/
|
|
| 2 |
*.py[cod]
|
| 3 |
*$py.class
|
| 4 |
*.so
|
| 5 |
-
.Python
|
| 6 |
-
env/
|
| 7 |
-
build/
|
| 8 |
-
develop-eggs/
|
| 9 |
-
dist/
|
| 10 |
-
downloads/
|
| 11 |
-
eggs/
|
| 12 |
-
.eggs/
|
| 13 |
-
lib/
|
| 14 |
-
lib64/
|
| 15 |
-
parts/
|
| 16 |
-
sdist/
|
| 17 |
-
var/
|
| 18 |
-
*.egg-info/
|
| 19 |
-
.installed.cfg
|
| 20 |
-
*.egg
|
| 21 |
.env
|
| 22 |
.venv
|
|
|
|
| 23 |
venv/
|
| 24 |
ENV/
|
| 25 |
-
.idea/
|
| 26 |
-
.vscode/
|
| 27 |
-
*.swp
|
| 28 |
-
*.bak
|
| 29 |
.DS_Store
|
| 30 |
.git
|
| 31 |
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.py[cod]
|
| 3 |
*$py.class
|
| 4 |
*.so
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
.env
|
| 6 |
.venv
|
| 7 |
+
env/
|
| 8 |
venv/
|
| 9 |
ENV/
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
.DS_Store
|
| 11 |
.git
|
| 12 |
.gitignore
|
| 13 |
+
*.log
|
| 14 |
+
*.sqlite
|
| 15 |
+
.idea/
|
| 16 |
+
.vscode/
|
Dockerfile
CHANGED
|
@@ -11,6 +11,9 @@ RUN apt-get update && \
|
|
| 11 |
&& apt-get clean \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
# Copy requirements first to leverage Docker cache
|
| 15 |
COPY requirements.txt .
|
| 16 |
|
|
@@ -18,13 +21,11 @@ COPY requirements.txt .
|
|
| 18 |
RUN pip install --no-cache-dir --upgrade pip && \
|
| 19 |
pip install --no-cache-dir -r requirements.txt
|
| 20 |
|
| 21 |
-
# Create necessary directories with correct permissions
|
| 22 |
-
RUN mkdir -p /code/data /code/logs && \
|
| 23 |
-
chmod -R 755 /code/data /code/logs
|
| 24 |
-
|
| 25 |
# Copy application code
|
| 26 |
-
COPY
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# Set environment variables
|
| 30 |
ENV PYTHONPATH=/code
|
|
|
|
| 11 |
&& apt-get clean \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
+
# Create necessary directories
|
| 15 |
+
RUN mkdir -p /code/data /code/logs
|
| 16 |
+
|
| 17 |
# Copy requirements first to leverage Docker cache
|
| 18 |
COPY requirements.txt .
|
| 19 |
|
|
|
|
| 21 |
RUN pip install --no-cache-dir --upgrade pip && \
|
| 22 |
pip install --no-cache-dir -r requirements.txt
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Copy application code
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# Ensure directories have correct permissions
|
| 28 |
+
RUN chmod -R 755 /code/data /code/logs
|
| 29 |
|
| 30 |
# Set environment variables
|
| 31 |
ENV PYTHONPATH=/code
|
README.md
CHANGED
|
@@ -10,36 +10,33 @@ pinned: false
|
|
| 10 |
|
| 11 |
# Cryptocurrency Data Source API
|
| 12 |
|
| 13 |
-
This API provides cryptocurrency market data
|
| 14 |
|
| 15 |
## Features
|
| 16 |
|
| 17 |
-
- Access to multiple cryptocurrency exchanges
|
| 18 |
-
- OHLCV
|
| 19 |
-
-
|
| 20 |
-
- Designed for easy integration with trading systems and analysis tools
|
| 21 |
|
| 22 |
## API Endpoints
|
| 23 |
|
| 24 |
-
- `GET
|
| 25 |
-
- `GET /exchanges
|
| 26 |
-
- `GET /
|
| 27 |
-
- `GET /
|
| 28 |
-
- `GET /data/available` - Get list of available cached data files
|
| 29 |
- `GET /health` - Health check endpoint
|
| 30 |
|
| 31 |
## Usage
|
| 32 |
|
| 33 |
-
|
| 34 |
|
| 35 |
## Development
|
| 36 |
|
| 37 |
-
|
| 38 |
-
- FastAPI - Modern web framework
|
| 39 |
-
- CCXT -
|
| 40 |
-
-
|
| 41 |
-
- Pandas - Data manipulation and analysis
|
| 42 |
|
| 43 |
-
## Deployment
|
| 44 |
|
| 45 |
-
This
|
|
|
|
| 10 |
|
| 11 |
# Cryptocurrency Data Source API
|
| 12 |
|
| 13 |
+
This API provides cryptocurrency market data from various exchanges using the CCXT library.
|
| 14 |
|
| 15 |
## Features
|
| 16 |
|
| 17 |
+
- Access to multiple cryptocurrency exchanges
|
| 18 |
+
- OHLCV (Open, High, Low, Close, Volume) data
|
| 19 |
+
- Easy integration with trading systems
|
|
|
|
| 20 |
|
| 21 |
## API Endpoints
|
| 22 |
|
| 23 |
+
- `GET /` - API status and version
|
| 24 |
+
- `GET /exchanges` - List all available exchanges
|
| 25 |
+
- `GET /markets/{exchange_id}` - Get markets for a specific exchange
|
| 26 |
+
- `GET /ohlcv/{exchange_id}/{symbol}` - Get OHLCV data for a specific market
|
|
|
|
| 27 |
- `GET /health` - Health check endpoint
|
| 28 |
|
| 29 |
## Usage
|
| 30 |
|
| 31 |
+
Once deployed, visit the `/docs` endpoint to see the interactive API documentation.
|
| 32 |
|
| 33 |
## Development
|
| 34 |
|
| 35 |
+
Built with:
|
| 36 |
+
- FastAPI - Modern, fast web framework
|
| 37 |
+
- CCXT - Cryptocurrency exchange trading library
|
| 38 |
+
- Docker - Containerization
|
|
|
|
| 39 |
|
| 40 |
+
## Deployment on Hugging Face Spaces
|
| 41 |
|
| 42 |
+
This project is designed to be deployed as a Docker space on Hugging Face.
|
main.py
CHANGED
|
@@ -1,23 +1,19 @@
|
|
| 1 |
-
from fastapi import FastAPI, HTTPException,
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
|
| 4 |
import os
|
| 5 |
import logging
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
from typing import List, Dict, Any, Optional
|
| 8 |
-
import json
|
| 9 |
-
import ccxt
|
| 10 |
import pandas as pd
|
| 11 |
import numpy as np
|
| 12 |
|
| 13 |
-
from app.models import CryptoPair, TimeFrame, ExchangeInfo
|
| 14 |
-
from app.utils import setup_logger, get_available_exchanges
|
| 15 |
-
from app.indicators import calculate_indicators
|
| 16 |
-
from app.storage import save_market_data, load_market_data, get_available_data
|
| 17 |
-
|
| 18 |
# Setup logging
|
| 19 |
-
|
| 20 |
-
logger.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Initialize FastAPI app
|
| 23 |
app = FastAPI(
|
|
@@ -35,128 +31,104 @@ app.add_middleware(
|
|
| 35 |
allow_headers=["*"],
|
| 36 |
)
|
| 37 |
|
| 38 |
-
|
| 39 |
-
available_exchanges = get_available_exchanges()
|
| 40 |
-
exchange_instances = {}
|
| 41 |
-
|
| 42 |
-
for exchange_id in available_exchanges:
|
| 43 |
-
try:
|
| 44 |
-
exchange_instances[exchange_id] = getattr(ccxt, exchange_id)({
|
| 45 |
-
'enableRateLimit': True,
|
| 46 |
-
})
|
| 47 |
-
logger.info(f"Initialized exchange: {exchange_id}")
|
| 48 |
-
except Exception as e:
|
| 49 |
-
logger.error(f"Error initializing {exchange_id}: {str(e)}")
|
| 50 |
-
|
| 51 |
-
@app.get("/", response_class=JSONResponse)
|
| 52 |
async def root():
|
| 53 |
-
"""Root endpoint
|
| 54 |
return {
|
| 55 |
-
"
|
| 56 |
"version": "1.0.0",
|
| 57 |
-
"
|
| 58 |
-
|
| 59 |
-
"GET /exchanges/{exchange_id}/markets": "List markets for a specific exchange",
|
| 60 |
-
"GET /data/{exchange_id}/{symbol}/{timeframe}": "Get OHLCV data for a market",
|
| 61 |
-
"GET /indicators/{exchange_id}/{symbol}/{timeframe}": "Get technical indicators for a market",
|
| 62 |
-
}
|
| 63 |
}
|
| 64 |
|
| 65 |
-
@app.get("/exchanges"
|
| 66 |
async def get_exchanges():
|
| 67 |
-
"""
|
| 68 |
-
|
| 69 |
-
|
|
|
|
| 70 |
try:
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
except
|
| 79 |
-
|
|
|
|
| 80 |
|
| 81 |
-
return
|
| 82 |
|
| 83 |
-
@app.get("/
|
| 84 |
-
async def get_markets(exchange_id: str
|
| 85 |
-
"""Get
|
| 86 |
-
if exchange_id not in exchange_instances:
|
| 87 |
-
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 88 |
-
|
| 89 |
try:
|
| 90 |
-
exchange
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
markets = exchange.load_markets()
|
| 92 |
|
|
|
|
| 93 |
result = []
|
| 94 |
for symbol, market in markets.items():
|
| 95 |
-
if market
|
| 96 |
-
result.append(
|
| 97 |
-
symbol
|
| 98 |
-
base
|
| 99 |
-
quote
|
| 100 |
-
type
|
| 101 |
-
)
|
| 102 |
|
| 103 |
-
return result
|
|
|
|
|
|
|
|
|
|
| 104 |
except Exception as e:
|
| 105 |
logger.error(f"Error fetching markets for {exchange_id}: {str(e)}")
|
| 106 |
raise HTTPException(status_code=500, detail=str(e))
|
| 107 |
|
| 108 |
-
@app.get("/
|
| 109 |
-
async def
|
| 110 |
-
|
| 111 |
-
symbol: str = Path(..., description="Market symbol (e.g. BTC/USDT)"),
|
| 112 |
-
timeframe: str = Path(..., description="Timeframe (e.g. 1h, 1d)"),
|
| 113 |
-
limit: int = Query(100, description="Number of candles to return"),
|
| 114 |
-
from_timestamp: Optional[int] = Query(None, description="Start timestamp in milliseconds"),
|
| 115 |
-
use_cache: bool = Query(True, description="Whether to use cached data")
|
| 116 |
-
):
|
| 117 |
-
"""Get OHLCV market data for a specific market"""
|
| 118 |
-
if exchange_id not in exchange_instances:
|
| 119 |
-
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 120 |
-
|
| 121 |
-
exchange = exchange_instances[exchange_id]
|
| 122 |
-
|
| 123 |
-
# Check if exchange supports OHLCV
|
| 124 |
-
if not exchange.has.get('fetchOHLCV', False):
|
| 125 |
-
raise HTTPException(status_code=400, detail=f"Exchange {exchange_id} does not support OHLCV data")
|
| 126 |
-
|
| 127 |
-
# Check if timeframe is supported
|
| 128 |
-
if hasattr(exchange, 'timeframes') and timeframe not in exchange.timeframes:
|
| 129 |
-
raise HTTPException(status_code=400, detail=f"Timeframe {timeframe} not supported by {exchange_id}")
|
| 130 |
-
|
| 131 |
try:
|
| 132 |
-
# Check if
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
if
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
data = [candle for candle in data if candle[0] >= from_timestamp]
|
| 142 |
-
|
| 143 |
-
# Limit the number of candles
|
| 144 |
-
data = data[-limit:]
|
| 145 |
-
else:
|
| 146 |
-
# Fetch data from exchange
|
| 147 |
-
data = exchange.fetch_ohlcv(
|
| 148 |
-
symbol=symbol,
|
| 149 |
-
timeframe=timeframe,
|
| 150 |
-
limit=limit,
|
| 151 |
-
since=from_timestamp
|
| 152 |
)
|
| 153 |
-
|
| 154 |
-
# Save to cache
|
| 155 |
-
save_market_data(data, cache_file)
|
| 156 |
|
| 157 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
result = []
|
| 159 |
-
for candle in
|
| 160 |
timestamp, open_price, high, low, close, volume = candle
|
| 161 |
result.append({
|
| 162 |
"timestamp": timestamp,
|
|
@@ -168,81 +140,29 @@ async def get_market_data(
|
|
| 168 |
"volume": volume
|
| 169 |
})
|
| 170 |
|
| 171 |
-
return
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
logger.error(f"Exchange error: {str(e)}")
|
| 178 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 179 |
-
except Exception as e:
|
| 180 |
-
logger.error(f"Error fetching data: {str(e)}")
|
| 181 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 182 |
-
|
| 183 |
-
@app.get("/indicators/{exchange_id}/{symbol}/{timeframe}")
|
| 184 |
-
async def get_indicators(
|
| 185 |
-
exchange_id: str = Path(..., description="Exchange ID"),
|
| 186 |
-
symbol: str = Path(..., description="Market symbol (e.g. BTC/USDT)"),
|
| 187 |
-
timeframe: str = Path(..., description="Timeframe (e.g. 1h, 1d)"),
|
| 188 |
-
limit: int = Query(100, description="Number of candles to include"),
|
| 189 |
-
include_price_data: bool = Query(False, description="Whether to include price data"),
|
| 190 |
-
indicators: List[str] = Query(
|
| 191 |
-
["rsi", "macd", "bollinger", "ema"],
|
| 192 |
-
description="Indicators to calculate"
|
| 193 |
-
)
|
| 194 |
-
):
|
| 195 |
-
"""Get technical indicators for a specific market"""
|
| 196 |
-
try:
|
| 197 |
-
# Get market data first
|
| 198 |
-
market_data = await get_market_data(
|
| 199 |
-
exchange_id=exchange_id,
|
| 200 |
-
symbol=symbol,
|
| 201 |
-
timeframe=timeframe,
|
| 202 |
-
limit=limit + 100, # Extra data for accurate indicator calculation
|
| 203 |
-
use_cache=True
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
-
# Convert to pandas DataFrame
|
| 207 |
-
df = pd.DataFrame(market_data)
|
| 208 |
-
|
| 209 |
-
# Calculate indicators
|
| 210 |
-
result = calculate_indicators(df, indicators)
|
| 211 |
-
|
| 212 |
-
# Remove extra data used for calculation
|
| 213 |
-
result = result.iloc[-limit:].reset_index(drop=True)
|
| 214 |
-
|
| 215 |
-
# Include or exclude price data
|
| 216 |
-
if not include_price_data:
|
| 217 |
-
for col in ['open', 'high', 'low', 'close', 'volume']:
|
| 218 |
-
if col in result.columns:
|
| 219 |
-
result = result.drop(col, axis=1)
|
| 220 |
-
|
| 221 |
-
# Convert DataFrame to list of records
|
| 222 |
-
result_list = result.to_dict(orient='records')
|
| 223 |
-
|
| 224 |
-
return result_list
|
| 225 |
|
| 226 |
-
except
|
| 227 |
-
logger.error(f"Error calculating indicators: {str(e)}")
|
| 228 |
raise HTTPException(status_code=500, detail=str(e))
|
| 229 |
-
|
| 230 |
-
@app.get("/data/available")
|
| 231 |
-
async def get_available_data_files():
|
| 232 |
-
"""Get list of available cached data files"""
|
| 233 |
-
try:
|
| 234 |
-
files = get_available_data("data")
|
| 235 |
-
return {"available_data": files}
|
| 236 |
except Exception as e:
|
| 237 |
-
logger.error(f"Error
|
| 238 |
raise HTTPException(status_code=500, detail=str(e))
|
| 239 |
|
| 240 |
-
# Add a health check endpoint
|
| 241 |
@app.get("/health")
|
| 242 |
async def health_check():
|
| 243 |
"""Health check endpoint"""
|
| 244 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
if __name__ == "__main__":
|
| 247 |
import uvicorn
|
| 248 |
-
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Depends
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
import ccxt
|
| 4 |
import os
|
| 5 |
import logging
|
| 6 |
+
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
| 7 |
import pandas as pd
|
| 8 |
import numpy as np
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
# Setup logging
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Create data and logs directories
|
| 15 |
+
os.makedirs("data", exist_ok=True)
|
| 16 |
+
os.makedirs("logs", exist_ok=True)
|
| 17 |
|
| 18 |
# Initialize FastAPI app
|
| 19 |
app = FastAPI(
|
|
|
|
| 31 |
allow_headers=["*"],
|
| 32 |
)
|
| 33 |
|
| 34 |
+
@app.get("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
async def root():
|
| 36 |
+
"""Root endpoint showing API status"""
|
| 37 |
return {
|
| 38 |
+
"status": "online",
|
| 39 |
"version": "1.0.0",
|
| 40 |
+
"timestamp": datetime.now().isoformat(),
|
| 41 |
+
"ccxt_version": ccxt.__version__
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
+
@app.get("/exchanges")
|
| 45 |
async def get_exchanges():
|
| 46 |
+
"""List available exchanges"""
|
| 47 |
+
# Get list of exchanges that support OHLCV data
|
| 48 |
+
exchanges = []
|
| 49 |
+
for exchange_id in ccxt.exchanges:
|
| 50 |
try:
|
| 51 |
+
exchange = getattr(ccxt, exchange_id)()
|
| 52 |
+
if exchange.has.get('fetchOHLCV'):
|
| 53 |
+
exchanges.append({
|
| 54 |
+
"id": exchange_id,
|
| 55 |
+
"name": exchange.name if hasattr(exchange, 'name') else exchange_id,
|
| 56 |
+
"url": exchange.urls.get('www') if hasattr(exchange, 'urls') and exchange.urls else None
|
| 57 |
+
})
|
| 58 |
+
except:
|
| 59 |
+
# Skip exchanges that fail to initialize
|
| 60 |
+
continue
|
| 61 |
|
| 62 |
+
return {"total": len(exchanges), "exchanges": exchanges}
|
| 63 |
|
| 64 |
+
@app.get("/markets/{exchange_id}")
|
| 65 |
+
async def get_markets(exchange_id: str):
|
| 66 |
+
"""Get markets for a specific exchange"""
|
|
|
|
|
|
|
|
|
|
| 67 |
try:
|
| 68 |
+
# Check if exchange exists
|
| 69 |
+
if exchange_id not in ccxt.exchanges:
|
| 70 |
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 71 |
+
|
| 72 |
+
# Initialize exchange
|
| 73 |
+
exchange = getattr(ccxt, exchange_id)({
|
| 74 |
+
'enableRateLimit': True
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
# Fetch markets
|
| 78 |
markets = exchange.load_markets()
|
| 79 |
|
| 80 |
+
# Format response
|
| 81 |
result = []
|
| 82 |
for symbol, market in markets.items():
|
| 83 |
+
if market.get('active', False):
|
| 84 |
+
result.append({
|
| 85 |
+
"symbol": symbol,
|
| 86 |
+
"base": market.get('base', ''),
|
| 87 |
+
"quote": market.get('quote', ''),
|
| 88 |
+
"type": market.get('type', 'spot')
|
| 89 |
+
})
|
| 90 |
|
| 91 |
+
return {"exchange": exchange_id, "total": len(result), "markets": result[:100]}
|
| 92 |
+
|
| 93 |
+
except ccxt.BaseError as e:
|
| 94 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 95 |
except Exception as e:
|
| 96 |
logger.error(f"Error fetching markets for {exchange_id}: {str(e)}")
|
| 97 |
raise HTTPException(status_code=500, detail=str(e))
|
| 98 |
|
| 99 |
+
@app.get("/ohlcv/{exchange_id}/{symbol}")
|
| 100 |
+
async def get_ohlcv(exchange_id: str, symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 101 |
+
"""Get OHLCV data for a specific market"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
try:
|
| 103 |
+
# Check if exchange exists
|
| 104 |
+
if exchange_id not in ccxt.exchanges:
|
| 105 |
+
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 106 |
+
|
| 107 |
+
# Initialize exchange
|
| 108 |
+
exchange = getattr(ccxt, exchange_id)({
|
| 109 |
+
'enableRateLimit': True
|
| 110 |
+
})
|
| 111 |
|
| 112 |
+
# Check if exchange supports OHLCV
|
| 113 |
+
if not exchange.has.get('fetchOHLCV'):
|
| 114 |
+
raise HTTPException(
|
| 115 |
+
status_code=400,
|
| 116 |
+
detail=f"Exchange {exchange_id} does not support OHLCV data"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
)
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
# Check timeframe
|
| 120 |
+
if timeframe not in exchange.timeframes:
|
| 121 |
+
raise HTTPException(
|
| 122 |
+
status_code=400,
|
| 123 |
+
detail=f"Timeframe {timeframe} not supported by {exchange_id}"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Fetch OHLCV data
|
| 127 |
+
ohlcv = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
|
| 128 |
+
|
| 129 |
+
# Convert to readable format
|
| 130 |
result = []
|
| 131 |
+
for candle in ohlcv:
|
| 132 |
timestamp, open_price, high, low, close, volume = candle
|
| 133 |
result.append({
|
| 134 |
"timestamp": timestamp,
|
|
|
|
| 140 |
"volume": volume
|
| 141 |
})
|
| 142 |
|
| 143 |
+
return {
|
| 144 |
+
"exchange": exchange_id,
|
| 145 |
+
"symbol": symbol,
|
| 146 |
+
"timeframe": timeframe,
|
| 147 |
+
"data": result
|
| 148 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
except ccxt.BaseError as e:
|
|
|
|
| 151 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
except Exception as e:
|
| 153 |
+
logger.error(f"Error fetching OHLCV data: {str(e)}")
|
| 154 |
raise HTTPException(status_code=500, detail=str(e))
|
| 155 |
|
|
|
|
| 156 |
@app.get("/health")
|
| 157 |
async def health_check():
|
| 158 |
"""Health check endpoint"""
|
| 159 |
+
return {
|
| 160 |
+
"status": "healthy",
|
| 161 |
+
"timestamp": datetime.now().isoformat(),
|
| 162 |
+
"version": "1.0.0"
|
| 163 |
+
}
|
| 164 |
|
| 165 |
if __name__ == "__main__":
|
| 166 |
import uvicorn
|
| 167 |
+
port = int(os.getenv("PORT", 7860))
|
| 168 |
+
uvicorn.run("main:app", host="0.0.0.0", port=port, log_level="info")
|