|
|
|
|
|
""" |
|
|
Trading & Backtesting API Router |
|
|
Smart exchange integration for trading and backtesting |
|
|
Binance & KuCoin with advanced features |
|
|
""" |
|
|
|
|
|
from fastapi import APIRouter, Query, HTTPException |
|
|
from typing import Optional |
|
|
import logging |
|
|
|
|
|
from backend.services.trading_backtesting_service import ( |
|
|
get_trading_service, |
|
|
get_backtesting_service |
|
|
) |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(prefix="/api/trading", tags=["Trading & Backtesting"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/price/{symbol}") |
|
|
async def get_trading_price( |
|
|
symbol: str, |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy for geo-restricted access"), |
|
|
use_fallback: bool = Query(True, description="Use multi-source fallback if primary fails") |
|
|
): |
|
|
""" |
|
|
Get current trading price from smart exchange client |
|
|
|
|
|
**Features:** |
|
|
- Smart routing with geo-block bypass |
|
|
- DNS over HTTPS (DoH) |
|
|
- Multi-layer proxies (optional) |
|
|
- Auto-fallback to multi-source system |
|
|
|
|
|
**Exchanges:** |
|
|
- `binance`: Symbol format: BTCUSDT, ETHUSDT, etc. |
|
|
- `kucoin`: Symbol format: BTC-USDT, ETH-USDT, etc. |
|
|
|
|
|
**Example:** |
|
|
``` |
|
|
GET /api/trading/price/BTCUSDT?exchange=binance |
|
|
GET /api/trading/price/BTC-USDT?exchange=kucoin&enable_proxy=true |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
|
|
|
result = await service.get_trading_price( |
|
|
symbol=symbol, |
|
|
exchange=exchange, |
|
|
use_fallback=use_fallback |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get price for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/ohlcv/{symbol}") |
|
|
async def get_trading_ohlcv( |
|
|
symbol: str, |
|
|
timeframe: str = Query("1h", description="Timeframe (1m, 5m, 15m, 1h, 4h, 1d, etc.)"), |
|
|
limit: int = Query(100, ge=1, le=1000, description="Number of candles"), |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
start_time: Optional[int] = Query(None, description="Start timestamp (milliseconds)"), |
|
|
end_time: Optional[int] = Query(None, description="End timestamp (milliseconds)"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Get OHLCV candlestick data for trading/backtesting |
|
|
|
|
|
**Features:** |
|
|
- Up to 1000 candles per request |
|
|
- Smart client with geo-block bypass |
|
|
- Historical data with timestamps |
|
|
|
|
|
**Timeframes:** |
|
|
- Binance: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M |
|
|
- KuCoin: 1min, 3min, 5min, 15min, 30min, 1hour, 2hour, 4hour, 6hour, 8hour, 12hour, 1day, 1week |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"exchange": "binance", |
|
|
"symbol": "BTCUSDT", |
|
|
"timeframe": "1h", |
|
|
"candles": [ |
|
|
{ |
|
|
"timestamp": 1733491200000, |
|
|
"open": 43200.00, |
|
|
"high": 43300.00, |
|
|
"low": 43150.00, |
|
|
"close": 43250.50, |
|
|
"volume": 1234.56 |
|
|
} |
|
|
], |
|
|
"count": 100 |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
|
|
|
result = await service.get_trading_ohlcv( |
|
|
symbol=symbol, |
|
|
timeframe=timeframe, |
|
|
limit=limit, |
|
|
exchange=exchange, |
|
|
start_time=start_time, |
|
|
end_time=end_time |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get OHLCV for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/orderbook/{symbol}") |
|
|
async def get_orderbook( |
|
|
symbol: str, |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
limit: int = Query(100, ge=1, le=5000, description="Depth limit"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Get order book for trading |
|
|
|
|
|
**Features:** |
|
|
- Real-time bid/ask prices |
|
|
- Market depth analysis |
|
|
- Up to 5000 levels (Binance) |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"exchange": "binance", |
|
|
"symbol": "BTCUSDT", |
|
|
"bids": [ |
|
|
[43250.50, 1.234], |
|
|
[43249.00, 0.567] |
|
|
], |
|
|
"asks": [ |
|
|
[43251.00, 0.890], |
|
|
[43252.50, 1.456] |
|
|
] |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
|
|
|
result = await service.get_orderbook( |
|
|
symbol=symbol, |
|
|
exchange=exchange, |
|
|
limit=limit |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get orderbook for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/stats/24h/{symbol}") |
|
|
async def get_24h_stats( |
|
|
symbol: str, |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Get 24-hour trading statistics |
|
|
|
|
|
**Metrics:** |
|
|
- Current price |
|
|
- 24h change (amount and percentage) |
|
|
- 24h high/low |
|
|
- 24h volume |
|
|
- Number of trades (Binance only) |
|
|
|
|
|
**Example:** |
|
|
``` |
|
|
GET /api/trading/stats/24h/BTCUSDT?exchange=binance |
|
|
``` |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"exchange": "binance", |
|
|
"symbol": "BTCUSDT", |
|
|
"price": 43250.50, |
|
|
"change": 850.25, |
|
|
"change_percent": 2.01, |
|
|
"high": 43500.00, |
|
|
"low": 42800.00, |
|
|
"volume": 12345.67, |
|
|
"trades": 987654 |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
|
|
|
result = await service.get_24h_stats( |
|
|
symbol=symbol, |
|
|
exchange=exchange |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get 24h stats for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/backtest/historical/{symbol}") |
|
|
async def fetch_historical_data( |
|
|
symbol: str, |
|
|
timeframe: str = Query("1h", description="Timeframe"), |
|
|
days: int = Query(30, ge=1, le=365, description="Days of historical data"), |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Fetch historical data for backtesting |
|
|
|
|
|
**Features:** |
|
|
- Automatic chunking for large datasets |
|
|
- Up to 365 days of historical data |
|
|
- Returns DataFrame-ready format |
|
|
|
|
|
**Note:** This may take some time for large datasets due to API rate limits. |
|
|
|
|
|
**Example:** |
|
|
``` |
|
|
GET /api/trading/backtest/historical/BTCUSDT?timeframe=1h&days=30 |
|
|
``` |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"symbol": "BTCUSDT", |
|
|
"exchange": "binance", |
|
|
"timeframe": "1h", |
|
|
"days": 30, |
|
|
"candles": [...], |
|
|
"count": 720 |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
backtest_service = get_backtesting_service() |
|
|
|
|
|
df = await backtest_service.fetch_historical_data( |
|
|
symbol=symbol, |
|
|
timeframe=timeframe, |
|
|
days=days, |
|
|
exchange=exchange |
|
|
) |
|
|
|
|
|
if df.empty: |
|
|
return { |
|
|
"success": False, |
|
|
"error": "No historical data available", |
|
|
"symbol": symbol, |
|
|
"exchange": exchange |
|
|
} |
|
|
|
|
|
|
|
|
df_reset = df.reset_index() |
|
|
candles = df_reset.to_dict('records') |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"symbol": symbol, |
|
|
"exchange": exchange, |
|
|
"timeframe": timeframe, |
|
|
"days": days, |
|
|
"candles": candles, |
|
|
"count": len(candles) |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to fetch historical data for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/backtest/run/{symbol}") |
|
|
async def run_backtest( |
|
|
symbol: str, |
|
|
strategy: str = Query(..., description="Strategy name (sma_crossover, rsi, macd)"), |
|
|
timeframe: str = Query("1h", description="Timeframe"), |
|
|
days: int = Query(30, ge=1, le=365, description="Historical data period"), |
|
|
exchange: str = Query("binance", description="Exchange (binance/kucoin)"), |
|
|
initial_capital: float = Query(10000.0, ge=100, description="Initial capital"), |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Run backtesting with a trading strategy |
|
|
|
|
|
**Available Strategies:** |
|
|
|
|
|
1. **sma_crossover**: Simple Moving Average Crossover |
|
|
- Buy when fast SMA (10) crosses above slow SMA (30) |
|
|
- Sell when fast SMA crosses below slow SMA |
|
|
|
|
|
2. **rsi**: Relative Strength Index |
|
|
- Buy when RSI < 30 (oversold) |
|
|
- Sell when RSI > 70 (overbought) |
|
|
|
|
|
3. **macd**: Moving Average Convergence Divergence |
|
|
- Buy when MACD crosses above signal line |
|
|
- Sell when MACD crosses below signal line |
|
|
|
|
|
**Example:** |
|
|
``` |
|
|
GET /api/trading/backtest/run/BTCUSDT?strategy=sma_crossover&days=30&initial_capital=10000 |
|
|
``` |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"symbol": "BTCUSDT", |
|
|
"exchange": "binance", |
|
|
"strategy": "sma_crossover", |
|
|
"timeframe": "1h", |
|
|
"days": 30, |
|
|
"initial_capital": 10000.0, |
|
|
"final_capital": 10567.89, |
|
|
"profit": 567.89, |
|
|
"total_return": 5.68, |
|
|
"trades": 12, |
|
|
"candles_analyzed": 720 |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
backtest_service = get_backtesting_service() |
|
|
|
|
|
result = await backtest_service.run_backtest( |
|
|
symbol=symbol, |
|
|
strategy=strategy, |
|
|
timeframe=timeframe, |
|
|
days=days, |
|
|
exchange=exchange, |
|
|
initial_capital=initial_capital |
|
|
) |
|
|
|
|
|
return result |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to run backtest for {symbol}: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/exchanges/status") |
|
|
async def get_exchanges_status( |
|
|
enable_proxy: bool = Query(False, description="Enable proxy") |
|
|
): |
|
|
""" |
|
|
Get status of smart exchange clients |
|
|
|
|
|
**Features:** |
|
|
- Test connection to Binance and KuCoin |
|
|
- Show proxy status |
|
|
- Show DoH status |
|
|
|
|
|
**Response:** |
|
|
```json |
|
|
{ |
|
|
"success": true, |
|
|
"exchanges": { |
|
|
"binance": { |
|
|
"available": true, |
|
|
"endpoints": 5, |
|
|
"proxy_enabled": false, |
|
|
"doh_enabled": true |
|
|
}, |
|
|
"kucoin": { |
|
|
"available": true, |
|
|
"endpoints": 2, |
|
|
"proxy_enabled": false, |
|
|
"doh_enabled": true |
|
|
} |
|
|
} |
|
|
} |
|
|
``` |
|
|
""" |
|
|
try: |
|
|
service = get_trading_service(enable_proxy=enable_proxy) |
|
|
|
|
|
|
|
|
binance_available = False |
|
|
try: |
|
|
await service.binance.ping() |
|
|
binance_available = True |
|
|
except: |
|
|
pass |
|
|
|
|
|
|
|
|
kucoin_available = False |
|
|
try: |
|
|
await service.kucoin.get_ticker_price("BTC-USDT") |
|
|
kucoin_available = True |
|
|
except: |
|
|
pass |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"exchanges": { |
|
|
"binance": { |
|
|
"available": binance_available, |
|
|
"endpoints": len(service.binance.endpoints), |
|
|
"current_endpoint": service.binance.endpoints[service.binance.current_endpoint_index], |
|
|
"proxy_enabled": service.binance.enable_proxy, |
|
|
"doh_enabled": service.binance.enable_doh |
|
|
}, |
|
|
"kucoin": { |
|
|
"available": kucoin_available, |
|
|
"endpoints": len(service.kucoin.endpoints), |
|
|
"current_endpoint": service.kucoin.endpoints[service.kucoin.current_endpoint_index], |
|
|
"proxy_enabled": service.kucoin.enable_proxy, |
|
|
"doh_enabled": service.kucoin.enable_doh |
|
|
} |
|
|
}, |
|
|
"timestamp": "2025-12-06T00:00:00Z" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to get exchanges status: {e}") |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
__all__ = ["router"] |
|
|
|