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)}