from pybit.unified_trading import HTTP from dotenv import load_dotenv import os import yaml import time import logging load_dotenv() logger = logging.getLogger(__name__) class BybitExchange: def __init__(self): self.settings = yaml.safe_load(open("config/settings.yaml")) API_KEY = os.getenv("BYBIT_API_KEY") API_SECRET = os.getenv("BYBIT_API_SECRET") TESTNET = os.getenv("BYBIT_TESTNET", "true").lower() == "true" if not API_KEY or not API_SECRET: raise Exception("API keys missing in .env") self.session = HTTP( api_key=API_KEY, api_secret=API_SECRET, testnet=TESTNET ) self.max_retries = 3 self.retry_delay = 1 def _retry_request(self, func, *args, **kwargs): for attempt in range(self.max_retries): try: response = func(*args, **kwargs) if "retCode" in response and response["retCode"] == 0: return response else: logger.warning(f"API call failed: {response}") if attempt < self.max_retries - 1: time.sleep(self.retry_delay * (attempt + 1)) continue raise Exception(f"API call failed after {self.max_retries} attempts: {response}") except Exception as e: logger.error(f"API request error: {e}") if attempt < self.max_retries - 1: time.sleep(self.retry_delay * (attempt + 1)) continue raise e def get_balance(self): response = self._retry_request(self.session.get_wallet_balance, accountType="UNIFIED") return response["result"]["list"][0]["coin"] def get_positions(self, symbol=None): params = {"category": "linear"} if symbol: params["symbol"] = symbol response = self._retry_request(self.session.get_positions, **params) return response["result"]["list"] def set_leverage(self, symbol, leverage=None): lev = leverage or self.settings["trading"]["leverage"] response = self._retry_request( self.session.set_leverage, category="linear", symbol=symbol, buyLeverage=str(lev), sellLeverage=str(lev) ) return response def market_order(self, symbol, side, qty, reduce_only=False): params = { "category": "linear", "symbol": symbol, "side": side.upper(), "orderType": "Market", "qty": str(qty) } if reduce_only: params["reduceOnly"] = True response = self._retry_request(self.session.place_order, **params) return response["result"] def limit_order(self, symbol, side, qty, price, reduce_only=False): params = { "category": "linear", "symbol": symbol, "side": side.upper(), "orderType": "Limit", "qty": str(qty), "price": str(price) } if reduce_only: params["reduceOnly"] = True response = self._retry_request(self.session.place_order, **params) return response["result"] def set_tp_sl(self, symbol, side, entry_price, tp_price, sl_price): response = self._retry_request( self.session.set_trading_stop, category="linear", symbol=symbol, takeProfit=str(tp_price), stopLoss=str(sl_price), tpTriggerBy="LastPrice", slTriggerBy="LastPrice" ) return response def cancel_order(self, symbol, order_id=None): params = {"category": "linear", "symbol": symbol} if order_id: params["orderId"] = order_id response = self._retry_request(self.session.cancel_order, **params) return response["result"] def cancel_all_orders(self, symbol=None): params = {"category": "linear"} if symbol: params["symbol"] = symbol response = self._retry_request(self.session.cancel_all_orders, **params) return response["result"] def close_position(self, symbol): positions = self.get_positions(symbol) if not positions: return None position = positions[0] size = float(position["size"]) if size > 0: return self.market_order(symbol, "Sell", size, reduce_only=True) elif size < 0: return self.market_order(symbol, "Buy", abs(size), reduce_only=True) return None def get_active_orders(self, symbol=None): params = {"category": "linear"} if symbol: params["symbol"] = symbol response = self._retry_request(self.session.get_open_orders, **params) return response["result"]["list"] def get_kline_data(self, symbol, interval="1", limit=200): response = self._retry_request( self.session.get_kline, category="linear", symbol=symbol, interval=interval, limit=limit ) return response["result"]["list"] def get_orderbook(self, symbol, limit=25): response = self._retry_request( self.session.get_orderbook, category="linear", symbol=symbol, limit=limit ) return response["result"] def get_ticker(self, symbol): response = self._retry_request( self.session.get_tickers, category="linear", symbol=symbol ) return response["result"]["list"][0] def get_server_time(self): response = self._retry_request(self.session.get_server_time) return response["result"]["timeSecond"]