diff --git a/NN/exchanges/bybit_interface.py b/NN/exchanges/bybit_interface.py index b182bd6..9b5e293 100644 --- a/NN/exchanges/bybit_interface.py +++ b/NN/exchanges/bybit_interface.py @@ -52,6 +52,15 @@ class BybitInterface(ExchangeInterface): self._open_orders_cache_time = 0 self._cache_timeout = 5 # 5 seconds cache timeout + # Instrument info caching for minimum order size validation + self._instrument_cache = {} + self._instrument_cache_time = 0 + self._instrument_cache_timeout = 300 # 5 minutes cache for instrument info + + # Leverage settings + self.default_leverage = 10.0 # Default 10x leverage + self.leverage_cache = {} # Cache leverage settings per symbol + # Load credentials from environment if not provided if not api_key: self.api_key = os.getenv('BYBIT_API_KEY', '') @@ -120,8 +129,12 @@ class BybitInterface(ExchangeInterface): def _load_instruments(self) -> None: """Load available trading instruments.""" try: + if not self.session: + logger.warning("No session available for loading instruments") + return + instruments_response = self.session.get_instruments_info(category=self.category) - if instruments_response.get('retCode') == 0: + if instruments_response and instruments_response.get('retCode') == 0: instruments = instruments_response.get('result', {}).get('list', []) self.supported_symbols = {instr['symbol'] for instr in instruments} logger.info(f"Loaded {len(self.supported_symbols)} instruments") @@ -140,8 +153,12 @@ class BybitInterface(ExchangeInterface): List of instrument dictionaries """ try: + if not self.session: + logger.error("No session available for getting instruments") + return [] + response = self.session.get_instruments_info(category=category) - if response.get('retCode') == 0: + if response and response.get('retCode') == 0: return response.get('result', {}).get('list', []) else: logger.error(f"Failed to get instruments: {response}") @@ -320,9 +337,219 @@ class BybitInterface(ExchangeInterface): logger.error(f"Error getting ticker for {symbol}: {e}") return {} + def get_instrument_info(self, symbol: str) -> Dict[str, Any]: + """Get instrument information including minimum order size with caching. + + Args: + symbol: Trading symbol (e.g., 'ETHUSDT') + + Returns: + Dictionary with instrument information + """ + try: + formatted_symbol = self._format_symbol(symbol) + current_time = time.time() + + # Check cache first + if (formatted_symbol in self._instrument_cache and + current_time - self._instrument_cache_time < self._instrument_cache_timeout): + logger.debug(f"Returning cached instrument info for {formatted_symbol}") + return self._instrument_cache[formatted_symbol] + + # Get fresh instrument data - check if session is available + if not self.session: + logger.error("No session available for getting instruments") + return {} + + instruments = self.get_instruments(self.category) + + # Update cache with all instruments + self._instrument_cache.clear() + for instrument in instruments: + if isinstance(instrument, dict) and 'symbol' in instrument: + self._instrument_cache[instrument['symbol']] = instrument + self._instrument_cache_time = current_time + + # Return the requested instrument + instrument_info = self._instrument_cache.get(formatted_symbol, {}) + if not instrument_info: + logger.warning(f"Instrument {formatted_symbol} not found") + + return instrument_info + + except Exception as e: + logger.error(f"Error getting instrument info for {symbol}: {e}") + return {} + + def _validate_order_size(self, symbol: str, quantity: float) -> Tuple[bool, float, str]: + """Validate and adjust order size according to instrument requirements. + + Args: + symbol: Trading symbol + quantity: Requested quantity + + Returns: + Tuple of (is_valid, adjusted_quantity, error_message) + """ + try: + instrument_info = self.get_instrument_info(symbol) + if not instrument_info: + return False, quantity, f"Could not get instrument info for {symbol}" + + lot_size_filter = instrument_info.get('lotSizeFilter', {}) + min_order_qty = float(lot_size_filter.get('minOrderQty', 0.01)) + max_order_qty = float(lot_size_filter.get('maxOrderQty', 10000)) + qty_step = float(lot_size_filter.get('qtyStep', 0.01)) + + logger.debug(f"Validation for {symbol}: min={min_order_qty}, max={max_order_qty}, step={qty_step}, requested={quantity}") + + # Check minimum order size + if quantity < min_order_qty: + adjusted_quantity = min_order_qty + logger.warning(f"Order quantity {quantity} below minimum {min_order_qty} for {symbol}, adjusting to {adjusted_quantity}") + return True, adjusted_quantity, f"Adjusted quantity from {quantity:.6f} to minimum {adjusted_quantity:.6f}" + + # Check maximum order size + if quantity > max_order_qty: + return False, quantity, f"Order quantity {quantity} exceeds maximum {max_order_qty} for {symbol}" + + # Round to correct step size + if qty_step > 0: + steps = round(quantity / qty_step) + adjusted_quantity = steps * qty_step + # Ensure we don't go below minimum after rounding + if adjusted_quantity < min_order_qty: + adjusted_quantity = min_order_qty + + if abs(adjusted_quantity - quantity) > 0.000001: # Only log if there's a meaningful difference + logger.info(f"Adjusted quantity for step size: {quantity:.6f} -> {adjusted_quantity:.6f}") + + return True, adjusted_quantity, "" + + return True, quantity, "" + + except Exception as e: + logger.error(f"Error validating order size for {symbol}: {e}") + return False, quantity, f"Validation error: {e}" + + def set_leverage(self, symbol: str, leverage: float) -> bool: + """Set leverage for a symbol. + + Args: + symbol: Trading symbol (e.g., 'ETHUSDT') + leverage: Leverage value (e.g., 10.0 for 10x) + + Returns: + bool: True if successful, False otherwise + """ + try: + if not self.session: + logger.error("No session available for setting leverage") + return False + + formatted_symbol = self._format_symbol(symbol) + + # Validate leverage value + if leverage < 1.0 or leverage > 100.0: + logger.error(f"Invalid leverage value: {leverage}. Must be between 1.0 and 100.0") + return False + + # Set leverage via Bybit API + response = self.session.set_leverage( + category=self.category, + symbol=formatted_symbol, + buyLeverage=str(leverage), + sellLeverage=str(leverage) + ) + + if response.get('retCode') == 0: + # Cache the leverage setting + self.leverage_cache[formatted_symbol] = leverage + logger.info(f"Successfully set leverage for {symbol} to {leverage}x") + return True + else: + error_msg = response.get('retMsg', 'Unknown error') + logger.error(f"Failed to set leverage for {symbol}: {error_msg}") + return False + + except Exception as e: + logger.error(f"Error setting leverage for {symbol}: {e}") + return False + + def get_leverage(self, symbol: str) -> float: + """Get current leverage for a symbol. + + Args: + symbol: Trading symbol (e.g., 'ETHUSDT') + + Returns: + float: Current leverage value + """ + try: + if not self.session: + logger.error("No session available for getting leverage") + return self.default_leverage + + formatted_symbol = self._format_symbol(symbol) + + # Check cache first + if formatted_symbol in self.leverage_cache: + return self.leverage_cache[formatted_symbol] + + # Get leverage from API + response = self.session.get_positions( + category=self.category, + symbol=formatted_symbol + ) + + if response.get('retCode') == 0: + positions = response.get('result', {}).get('list', []) + for position in positions: + if position.get('symbol') == formatted_symbol: + leverage = float(position.get('leverage', self.default_leverage)) + # Cache the leverage + self.leverage_cache[formatted_symbol] = leverage + logger.debug(f"Current leverage for {symbol}: {leverage}x") + return leverage + + # If no position found, return default + logger.debug(f"No position found for {symbol}, using default leverage: {self.default_leverage}x") + return self.default_leverage + + except Exception as e: + logger.error(f"Error getting leverage for {symbol}: {e}") + return self.default_leverage + + def ensure_leverage(self, symbol: str, target_leverage: float = None) -> bool: + """Ensure symbol has the target leverage set. + + Args: + symbol: Trading symbol + target_leverage: Target leverage (uses default if None) + + Returns: + bool: True if leverage is set correctly + """ + try: + if target_leverage is None: + target_leverage = self.default_leverage + + current_leverage = self.get_leverage(symbol) + + if abs(current_leverage - target_leverage) < 0.1: # Allow small tolerance + logger.debug(f"Leverage for {symbol} already set to {current_leverage}x (target: {target_leverage}x)") + return True + else: + logger.info(f"Setting leverage for {symbol} from {current_leverage}x to {target_leverage}x") + return self.set_leverage(symbol, target_leverage) + + except Exception as e: + logger.error(f"Error ensuring leverage for {symbol}: {e}") + return False + def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: float = None) -> Dict[str, Any]: - """Place an order. + """Place an order with minimum size validation and leverage support. Args: symbol: Trading symbol (e.g., 'BTCUSDT') @@ -336,6 +563,21 @@ class BybitInterface(ExchangeInterface): """ try: formatted_symbol = self._format_symbol(symbol) + + # Ensure leverage is set before placing order + if not self.ensure_leverage(symbol, self.default_leverage): + logger.warning(f"Failed to set leverage for {symbol}, proceeding with order anyway") + + # Validate and adjust order size + is_valid, adjusted_quantity, error_msg = self._validate_order_size(formatted_symbol, quantity) + if not is_valid: + logger.error(f"Order validation failed: {error_msg}") + return {'error': error_msg} + + # Log adjustment if made + if adjusted_quantity != quantity: + logger.info(f"BYBIT ORDER SIZE ADJUSTMENT: {symbol} quantity {quantity:.6f} -> {adjusted_quantity:.6f}") + bybit_side = side.capitalize() # 'Buy' or 'Sell' bybit_order_type = self._map_order_type(order_type) @@ -344,7 +586,7 @@ class BybitInterface(ExchangeInterface): 'symbol': formatted_symbol, 'side': bybit_side, 'orderType': bybit_order_type, - 'qty': str(quantity), + 'qty': str(adjusted_quantity), } if order_type.lower() == 'limit' and price is not None: @@ -360,13 +602,14 @@ class BybitInterface(ExchangeInterface): 'symbol': symbol, 'side': side, 'type': order_type, - 'quantity': quantity, + 'quantity': adjusted_quantity, # Return the actual quantity used 'price': price, 'status': 'submitted', 'timestamp': int(time.time() * 1000) } - logger.info(f"Successfully placed {order_type} {side} order for {quantity} {symbol}") + current_leverage = self.get_leverage(symbol) + logger.info(f"Successfully placed {order_type} {side} order for {adjusted_quantity} {symbol} at {current_leverage}x leverage") return order_info else: error_msg = response.get('retMsg', 'Unknown error') diff --git a/_dev/dev_notes.md b/_dev/dev_notes.md index 3918f57..eed7af4 100644 --- a/_dev/dev_notes.md +++ b/_dev/dev_notes.md @@ -85,4 +85,9 @@ we should load the models in a way that we do a back propagation and other model +also, adjust our bybit api so we trade with usdt futures - where we can have up to 50x leverage. on spots we can have 10x max + + + +