Really-amin commited on
Commit
3e2e46d
·
verified ·
1 Parent(s): df1f5ae

Upload 10 files

Browse files
Files changed (4) hide show
  1. .dockerignore +5 -20
  2. Dockerfile +7 -6
  3. README.md +15 -18
  4. 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 app /code/app
27
- COPY main.py /code/
 
 
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 and technical indicators from various exchanges. It's built with FastAPI and designed to be deployed on Hugging Face Spaces.
14
 
15
  ## Features
16
 
17
- - Access to multiple cryptocurrency exchanges through a unified API
18
- - OHLCV data retrieval with caching for better performance
19
- - Technical indicators calculation (RSI, MACD, Bollinger Bands, EMA, etc.)
20
- - Designed for easy integration with trading systems and analysis tools
21
 
22
  ## API Endpoints
23
 
24
- - `GET /exchanges` - List available exchanges
25
- - `GET /exchanges/{exchange_id}/markets` - List markets for a specific exchange
26
- - `GET /data/{exchange_id}/{symbol}/{timeframe}` - Get OHLCV data for a market
27
- - `GET /indicators/{exchange_id}/{symbol}/{timeframe}` - Get technical indicators for a market
28
- - `GET /data/available` - Get list of available cached data files
29
  - `GET /health` - Health check endpoint
30
 
31
  ## Usage
32
 
33
- The API documentation is available at `/docs` once the service is running.
34
 
35
  ## Development
36
 
37
- The API is built with:
38
- - FastAPI - Modern web framework for building APIs
39
- - CCXT - Library for cryptocurrency exchange trading
40
- - TA-Lib - Technical analysis library
41
- - Pandas - Data manipulation and analysis
42
 
43
- ## Deployment
44
 
45
- This application is designed for deployment on Hugging Face Spaces using Docker.
 
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, Query, Depends, Path
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi.responses import JSONResponse
4
  import os
5
  import logging
6
- from datetime import datetime, timedelta
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
- logger = setup_logger()
20
- logger.info("Starting Cryptocurrency Data Source API")
 
 
 
 
21
 
22
  # Initialize FastAPI app
23
  app = FastAPI(
@@ -35,128 +31,104 @@ app.add_middleware(
35
  allow_headers=["*"],
36
  )
37
 
38
- # Initialize exchanges
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 returning API information"""
54
  return {
55
- "message": "Cryptocurrency Data Source API is running",
56
  "version": "1.0.0",
57
- "endpoints": {
58
- "GET /exchanges": "List available exchanges",
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", response_model=List[ExchangeInfo])
66
  async def get_exchanges():
67
- """Get list of available exchanges"""
68
- result = []
69
- for exchange_id, exchange in exchange_instances.items():
 
70
  try:
71
- # Add basic exchange info
72
- result.append(ExchangeInfo(
73
- id=exchange_id,
74
- name=exchange.name,
75
- has_fetchOHLCV=exchange.has.get('fetchOHLCV', False),
76
- timeframes=list(exchange.timeframes.keys()) if hasattr(exchange, 'timeframes') else []
77
- ))
78
- except Exception as e:
79
- logger.error(f"Error getting info for {exchange_id}: {str(e)}")
 
80
 
81
- return result
82
 
83
- @app.get("/exchanges/{exchange_id}/markets", response_model=List[CryptoPair])
84
- async def get_markets(exchange_id: str = Path(..., description="Exchange ID")):
85
- """Get available markets for a specific exchange"""
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 = exchange_instances[exchange_id]
 
 
 
 
 
 
 
 
 
91
  markets = exchange.load_markets()
92
 
 
93
  result = []
94
  for symbol, market in markets.items():
95
- if market['active']:
96
- result.append(CryptoPair(
97
- symbol=symbol,
98
- base=market['base'],
99
- quote=market['quote'],
100
- type=market['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("/data/{exchange_id}/{symbol}/{timeframe}")
109
- async def get_market_data(
110
- exchange_id: str = Path(..., description="Exchange ID"),
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 we have cached data
133
- cache_file = f"data/{exchange_id}_{symbol.replace('/', '_')}_{timeframe}.json"
134
- cache_file = cache_file.replace(':', '_') # Replace colons for Windows compatibility
 
 
 
 
 
135
 
136
- if use_cache and os.path.exists(cache_file):
137
- data = load_market_data(cache_file)
138
-
139
- # Filter by from_timestamp if provided
140
- if from_timestamp:
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
- # Convert to a more readable format
 
 
 
 
 
 
 
 
 
 
158
  result = []
159
- for candle in data:
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 result
172
-
173
- except ccxt.NetworkError as e:
174
- logger.error(f"Network error: {str(e)}")
175
- raise HTTPException(status_code=503, detail="Network error when connecting to exchange")
176
- except ccxt.ExchangeError as e:
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 Exception as e:
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 listing available data: {str(e)}")
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 {"status": "healthy", "timestamp": datetime.now().isoformat()}
 
 
 
 
245
 
246
  if __name__ == "__main__":
247
  import uvicorn
248
- uvicorn.run("main:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)), log_level="info")
 
 
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")