578 lines
22 KiB
Python
578 lines
22 KiB
Python
import logging
|
|
import time
|
|
from typing import Dict, Any, List, Optional, Tuple
|
|
import asyncio
|
|
import websockets
|
|
import json
|
|
from datetime import datetime, timezone
|
|
import requests
|
|
|
|
try:
|
|
from deribit_api import RestClient
|
|
except ImportError:
|
|
RestClient = None
|
|
logging.warning("deribit-api not installed. Run: pip install deribit-api")
|
|
|
|
from .exchange_interface import ExchangeInterface
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DeribitInterface(ExchangeInterface):
|
|
"""Deribit Exchange API Interface for cryptocurrency derivatives trading.
|
|
|
|
Supports both testnet and live trading environments.
|
|
Focus on BTC and ETH perpetual and options contracts.
|
|
"""
|
|
|
|
def __init__(self, api_key: str = "", api_secret: str = "", test_mode: bool = True):
|
|
"""Initialize Deribit exchange interface.
|
|
|
|
Args:
|
|
api_key: Deribit API key
|
|
api_secret: Deribit API secret
|
|
test_mode: If True, use testnet environment
|
|
"""
|
|
super().__init__(api_key, api_secret, test_mode)
|
|
|
|
# Deribit API endpoints
|
|
if test_mode:
|
|
self.base_url = "https://test.deribit.com"
|
|
self.ws_url = "wss://test.deribit.com/ws/api/v2"
|
|
else:
|
|
self.base_url = "https://www.deribit.com"
|
|
self.ws_url = "wss://www.deribit.com/ws/api/v2"
|
|
|
|
self.rest_client = None
|
|
self.auth_token = None
|
|
self.token_expires = 0
|
|
|
|
# Deribit-specific settings
|
|
self.supported_currencies = ['BTC', 'ETH']
|
|
self.supported_instruments = {}
|
|
|
|
logger.info(f"DeribitInterface initialized in {'testnet' if test_mode else 'live'} mode")
|
|
|
|
def connect(self) -> bool:
|
|
"""Connect to Deribit API and authenticate."""
|
|
try:
|
|
if RestClient is None:
|
|
logger.error("deribit-api library not installed")
|
|
return False
|
|
|
|
# Initialize REST client
|
|
self.rest_client = RestClient(
|
|
client_id=self.api_key,
|
|
client_secret=self.api_secret,
|
|
env="test" if self.test_mode else "prod"
|
|
)
|
|
|
|
# Test authentication
|
|
if self.api_key and self.api_secret:
|
|
auth_result = self._authenticate()
|
|
if not auth_result:
|
|
logger.error("Failed to authenticate with Deribit API")
|
|
return False
|
|
|
|
# Test connection by fetching account summary
|
|
account_info = self.get_account_summary()
|
|
if account_info:
|
|
logger.info("Successfully connected to Deribit API")
|
|
self._load_instruments()
|
|
return True
|
|
else:
|
|
logger.warning("No API credentials provided - using public API only")
|
|
self._load_instruments()
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to Deribit API: {e}")
|
|
return False
|
|
|
|
return False
|
|
|
|
def _authenticate(self) -> bool:
|
|
"""Authenticate with Deribit API."""
|
|
try:
|
|
if not self.rest_client:
|
|
return False
|
|
|
|
# Get authentication token
|
|
auth_response = self.rest_client.auth()
|
|
|
|
if auth_response and 'result' in auth_response:
|
|
self.auth_token = auth_response['result']['access_token']
|
|
self.token_expires = auth_response['result']['expires_in'] + int(time.time())
|
|
logger.info("Successfully authenticated with Deribit")
|
|
return True
|
|
else:
|
|
logger.error("Failed to get authentication token from Deribit")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Authentication error: {e}")
|
|
return False
|
|
|
|
def _load_instruments(self) -> None:
|
|
"""Load available instruments for supported currencies."""
|
|
try:
|
|
for currency in self.supported_currencies:
|
|
instruments = self.get_instruments(currency)
|
|
self.supported_instruments[currency] = instruments
|
|
logger.info(f"Loaded {len(instruments)} instruments for {currency}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load instruments: {e}")
|
|
|
|
def get_instruments(self, currency: str) -> List[Dict[str, Any]]:
|
|
"""Get available instruments for a currency."""
|
|
try:
|
|
if not self.rest_client:
|
|
return []
|
|
|
|
response = self.rest_client.getinstruments(currency=currency.upper())
|
|
|
|
if response and 'result' in response:
|
|
return response['result']
|
|
else:
|
|
logger.error(f"Failed to get instruments for {currency}")
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting instruments for {currency}: {e}")
|
|
return []
|
|
|
|
def get_balance(self, asset: str) -> float:
|
|
"""Get balance of a specific asset.
|
|
|
|
Args:
|
|
asset: Currency symbol (BTC, ETH)
|
|
|
|
Returns:
|
|
float: Available balance
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
logger.warning("Not authenticated - cannot get balance")
|
|
return 0.0
|
|
|
|
currency = asset.upper()
|
|
if currency not in self.supported_currencies:
|
|
logger.warning(f"Currency {currency} not supported by Deribit")
|
|
return 0.0
|
|
|
|
response = self.rest_client.getaccountsummary(currency=currency)
|
|
|
|
if response and 'result' in response:
|
|
result = response['result']
|
|
# Deribit returns balance in the currency's base unit
|
|
return float(result.get('available_funds', 0.0))
|
|
else:
|
|
logger.error(f"Failed to get balance for {currency}")
|
|
return 0.0
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting balance for {asset}: {e}")
|
|
return 0.0
|
|
|
|
def get_account_summary(self, currency: str = 'BTC') -> Dict[str, Any]:
|
|
"""Get account summary for a currency."""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
return {}
|
|
|
|
response = self.rest_client.getaccountsummary(currency=currency.upper())
|
|
|
|
if response and 'result' in response:
|
|
return response['result']
|
|
else:
|
|
logger.error(f"Failed to get account summary for {currency}")
|
|
return {}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting account summary: {e}")
|
|
return {}
|
|
|
|
def get_ticker(self, symbol: str) -> Dict[str, Any]:
|
|
"""Get ticker information for a symbol.
|
|
|
|
Args:
|
|
symbol: Instrument name (e.g., 'BTC-PERPETUAL', 'ETH-PERPETUAL')
|
|
|
|
Returns:
|
|
Dict containing ticker data
|
|
"""
|
|
try:
|
|
if not self.rest_client:
|
|
return {}
|
|
|
|
# Format symbol for Deribit
|
|
deribit_symbol = self._format_symbol(symbol)
|
|
|
|
response = self.rest_client.getticker(instrument_name=deribit_symbol)
|
|
|
|
if response and 'result' in response:
|
|
ticker = response['result']
|
|
return {
|
|
'symbol': symbol,
|
|
'last_price': float(ticker.get('last_price', 0)),
|
|
'bid': float(ticker.get('best_bid_price', 0)),
|
|
'ask': float(ticker.get('best_ask_price', 0)),
|
|
'volume': float(ticker.get('stats', {}).get('volume', 0)),
|
|
'timestamp': ticker.get('timestamp', int(time.time() * 1000))
|
|
}
|
|
else:
|
|
logger.error(f"Failed to get ticker for {symbol}")
|
|
return {}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting ticker for {symbol}: {e}")
|
|
return {}
|
|
|
|
def place_order(self, symbol: str, side: str, order_type: str,
|
|
quantity: float, price: float = None) -> Dict[str, Any]:
|
|
"""Place an order on Deribit.
|
|
|
|
Args:
|
|
symbol: Instrument name
|
|
side: 'buy' or 'sell'
|
|
order_type: 'limit', 'market', 'stop_limit', 'stop_market'
|
|
quantity: Order quantity (in contracts)
|
|
price: Order price (required for limit orders)
|
|
|
|
Returns:
|
|
Dict containing order information
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
logger.error("Not authenticated - cannot place order")
|
|
return {'error': 'Not authenticated'}
|
|
|
|
# Format symbol for Deribit
|
|
deribit_symbol = self._format_symbol(symbol)
|
|
|
|
# Validate order parameters
|
|
if order_type.lower() in ['limit', 'stop_limit'] and price is None:
|
|
return {'error': 'Price required for limit orders'}
|
|
|
|
# Map order types to Deribit format
|
|
deribit_order_type = self._map_order_type(order_type)
|
|
|
|
# Place order based on side
|
|
if side.lower() == 'buy':
|
|
response = self.rest_client.buy(
|
|
instrument_name=deribit_symbol,
|
|
amount=int(quantity),
|
|
type=deribit_order_type,
|
|
price=price
|
|
)
|
|
elif side.lower() == 'sell':
|
|
response = self.rest_client.sell(
|
|
instrument_name=deribit_symbol,
|
|
amount=int(quantity),
|
|
type=deribit_order_type,
|
|
price=price
|
|
)
|
|
else:
|
|
return {'error': f'Invalid side: {side}'}
|
|
|
|
if response and 'result' in response:
|
|
order = response['result']['order']
|
|
return {
|
|
'orderId': order['order_id'],
|
|
'symbol': symbol,
|
|
'side': side,
|
|
'type': order_type,
|
|
'quantity': quantity,
|
|
'price': price,
|
|
'status': order['order_state'],
|
|
'timestamp': order['creation_timestamp']
|
|
}
|
|
else:
|
|
error_msg = response.get('error', {}).get('message', 'Unknown error') if response else 'No response'
|
|
logger.error(f"Failed to place order: {error_msg}")
|
|
return {'error': error_msg}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error placing order: {e}")
|
|
return {'error': str(e)}
|
|
|
|
def cancel_order(self, symbol: str, order_id: str) -> bool:
|
|
"""Cancel an order.
|
|
|
|
Args:
|
|
symbol: Instrument name (not used in Deribit API)
|
|
order_id: Order ID to cancel
|
|
|
|
Returns:
|
|
bool: True if successful
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
logger.error("Not authenticated - cannot cancel order")
|
|
return False
|
|
|
|
response = self.rest_client.cancel(order_id=order_id)
|
|
|
|
if response and 'result' in response:
|
|
logger.info(f"Successfully cancelled order {order_id}")
|
|
return True
|
|
else:
|
|
error_msg = response.get('error', {}).get('message', 'Unknown error') if response else 'No response'
|
|
logger.error(f"Failed to cancel order {order_id}: {error_msg}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error cancelling order {order_id}: {e}")
|
|
return False
|
|
|
|
def get_order_status(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
|
"""Get order status.
|
|
|
|
Args:
|
|
symbol: Instrument name (not used in Deribit API)
|
|
order_id: Order ID
|
|
|
|
Returns:
|
|
Dict containing order status
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
return {'error': 'Not authenticated'}
|
|
|
|
response = self.rest_client.getorderstate(order_id=order_id)
|
|
|
|
if response and 'result' in response:
|
|
order = response['result']
|
|
return {
|
|
'orderId': order['order_id'],
|
|
'symbol': order['instrument_name'],
|
|
'side': 'buy' if order['direction'] == 'buy' else 'sell',
|
|
'type': order['order_type'],
|
|
'quantity': order['amount'],
|
|
'price': order.get('price'),
|
|
'filled_quantity': order['filled_amount'],
|
|
'status': order['order_state'],
|
|
'timestamp': order['creation_timestamp']
|
|
}
|
|
else:
|
|
error_msg = response.get('error', {}).get('message', 'Unknown error') if response else 'No response'
|
|
return {'error': error_msg}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting order status for {order_id}: {e}")
|
|
return {'error': str(e)}
|
|
|
|
def get_open_orders(self, symbol: str = None) -> List[Dict[str, Any]]:
|
|
"""Get open orders.
|
|
|
|
Args:
|
|
symbol: Optional instrument name filter
|
|
|
|
Returns:
|
|
List of open orders
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
logger.warning("Not authenticated - cannot get open orders")
|
|
return []
|
|
|
|
# Get orders for each supported currency
|
|
all_orders = []
|
|
|
|
for currency in self.supported_currencies:
|
|
response = self.rest_client.getopenordersbyinstrument(
|
|
instrument_name=symbol if symbol else f"{currency}-PERPETUAL"
|
|
)
|
|
|
|
if response and 'result' in response:
|
|
orders = response['result']
|
|
for order in orders:
|
|
formatted_order = {
|
|
'orderId': order['order_id'],
|
|
'symbol': order['instrument_name'],
|
|
'side': 'buy' if order['direction'] == 'buy' else 'sell',
|
|
'type': order['order_type'],
|
|
'quantity': order['amount'],
|
|
'price': order.get('price'),
|
|
'status': order['order_state'],
|
|
'timestamp': order['creation_timestamp']
|
|
}
|
|
|
|
# Filter by symbol if specified
|
|
if not symbol or order['instrument_name'] == self._format_symbol(symbol):
|
|
all_orders.append(formatted_order)
|
|
|
|
return all_orders
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting open orders: {e}")
|
|
return []
|
|
|
|
def get_positions(self, currency: str = None) -> List[Dict[str, Any]]:
|
|
"""Get current positions.
|
|
|
|
Args:
|
|
currency: Optional currency filter ('BTC', 'ETH')
|
|
|
|
Returns:
|
|
List of positions
|
|
"""
|
|
try:
|
|
if not self.rest_client or not self.auth_token:
|
|
logger.warning("Not authenticated - cannot get positions")
|
|
return []
|
|
|
|
currencies = [currency.upper()] if currency else self.supported_currencies
|
|
all_positions = []
|
|
|
|
for curr in currencies:
|
|
response = self.rest_client.getpositions(currency=curr)
|
|
|
|
if response and 'result' in response:
|
|
positions = response['result']
|
|
for position in positions:
|
|
if position['size'] != 0: # Only return non-zero positions
|
|
formatted_position = {
|
|
'symbol': position['instrument_name'],
|
|
'side': 'long' if position['direction'] == 'buy' else 'short',
|
|
'size': abs(position['size']),
|
|
'entry_price': position['average_price'],
|
|
'mark_price': position['mark_price'],
|
|
'unrealized_pnl': position['total_profit_loss'],
|
|
'percentage': position['delta']
|
|
}
|
|
all_positions.append(formatted_position)
|
|
|
|
return all_positions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting positions: {e}")
|
|
return []
|
|
|
|
def _format_symbol(self, symbol: str) -> str:
|
|
"""Convert symbol to Deribit format.
|
|
|
|
Args:
|
|
symbol: Symbol like 'BTC/USD', 'ETH/USD', 'BTC-PERPETUAL'
|
|
|
|
Returns:
|
|
Deribit instrument name
|
|
"""
|
|
# If already in Deribit format, return as-is
|
|
if '-' in symbol and symbol.upper() in ['BTC-PERPETUAL', 'ETH-PERPETUAL']:
|
|
return symbol.upper()
|
|
|
|
# Handle slash notation
|
|
if '/' in symbol:
|
|
base, quote = symbol.split('/')
|
|
if base.upper() in ['BTC', 'ETH'] and quote.upper() in ['USD', 'USDT', 'USDC']:
|
|
return f"{base.upper()}-PERPETUAL"
|
|
|
|
# Handle direct currency symbols
|
|
if symbol.upper() in ['BTC', 'ETH']:
|
|
return f"{symbol.upper()}-PERPETUAL"
|
|
|
|
# Default to BTC perpetual if unknown
|
|
logger.warning(f"Unknown symbol format: {symbol}, defaulting to BTC-PERPETUAL")
|
|
return "BTC-PERPETUAL"
|
|
|
|
def _map_order_type(self, order_type: str) -> str:
|
|
"""Map order type to Deribit format."""
|
|
type_mapping = {
|
|
'market': 'market',
|
|
'limit': 'limit',
|
|
'stop_market': 'stop_market',
|
|
'stop_limit': 'stop_limit'
|
|
}
|
|
return type_mapping.get(order_type.lower(), 'limit')
|
|
|
|
def get_last_price(self, symbol: str) -> float:
|
|
"""Get the last traded price for a symbol."""
|
|
try:
|
|
ticker = self.get_ticker(symbol)
|
|
return ticker.get('last_price', 0.0)
|
|
except Exception as e:
|
|
logger.error(f"Error getting last price for {symbol}: {e}")
|
|
return 0.0
|
|
|
|
def get_orderbook(self, symbol: str, depth: int = 10) -> Dict[str, Any]:
|
|
"""Get orderbook for a symbol.
|
|
|
|
Args:
|
|
symbol: Instrument name
|
|
depth: Number of levels to retrieve
|
|
|
|
Returns:
|
|
Dict containing bids and asks
|
|
"""
|
|
try:
|
|
if not self.rest_client:
|
|
return {}
|
|
|
|
deribit_symbol = self._format_symbol(symbol)
|
|
|
|
response = self.rest_client.getorderbook(
|
|
instrument_name=deribit_symbol,
|
|
depth=depth
|
|
)
|
|
|
|
if response and 'result' in response:
|
|
orderbook = response['result']
|
|
return {
|
|
'symbol': symbol,
|
|
'bids': [[float(bid[0]), float(bid[1])] for bid in orderbook.get('bids', [])],
|
|
'asks': [[float(ask[0]), float(ask[1])] for ask in orderbook.get('asks', [])],
|
|
'timestamp': orderbook.get('timestamp', int(time.time() * 1000))
|
|
}
|
|
else:
|
|
logger.error(f"Failed to get orderbook for {symbol}")
|
|
return {}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting orderbook for {symbol}: {e}")
|
|
return {}
|
|
|
|
def close_position(self, symbol: str, quantity: float = None) -> Dict[str, Any]:
|
|
"""Close a position (market order).
|
|
|
|
Args:
|
|
symbol: Instrument name
|
|
quantity: Quantity to close (None for full position)
|
|
|
|
Returns:
|
|
Dict containing order result
|
|
"""
|
|
try:
|
|
positions = self.get_positions()
|
|
target_position = None
|
|
|
|
deribit_symbol = self._format_symbol(symbol)
|
|
|
|
# Find the position to close
|
|
for position in positions:
|
|
if position['symbol'] == deribit_symbol:
|
|
target_position = position
|
|
break
|
|
|
|
if not target_position:
|
|
return {'error': f'No open position found for {symbol}'}
|
|
|
|
# Determine close quantity and side
|
|
position_size = target_position['size']
|
|
close_quantity = quantity if quantity else position_size
|
|
|
|
# Close long position = sell, close short position = buy
|
|
close_side = 'sell' if target_position['side'] == 'long' else 'buy'
|
|
|
|
# Place market order to close
|
|
return self.place_order(
|
|
symbol=symbol,
|
|
side=close_side,
|
|
order_type='market',
|
|
quantity=close_quantity
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error closing position for {symbol}: {e}")
|
|
return {'error': str(e)} |