This commit is contained in:
Dobromir Popov
2025-07-14 17:56:09 +03:00
parent d53a2ba75d
commit 4a55c5ff03
11 changed files with 1509 additions and 118 deletions

View File

@ -0,0 +1,578 @@
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)}