""" OKX Exchange Client for Live Trading. Handles connection to OKX API, order execution, and account management. Supports demo/sandbox mode for paper trading. """ import logging from typing import Optional from datetime import datetime, timezone import ccxt from .config import OKXConfig, TradingConfig logger = logging.getLogger(__name__) class OKXClient: """ OKX Exchange client wrapper using CCXT. Supports both live and demo (sandbox) trading modes. Demo mode uses OKX's official sandbox environment. """ def __init__(self, okx_config: OKXConfig, trading_config: TradingConfig): self.okx_config = okx_config self.trading_config = trading_config self.exchange: Optional[ccxt.okx] = None self._setup_exchange() def _setup_exchange(self) -> None: """Initialize CCXT OKX exchange instance.""" self.okx_config.validate() config = { 'apiKey': self.okx_config.api_key, 'secret': self.okx_config.secret, 'password': self.okx_config.password, 'sandbox': self.okx_config.demo_mode, 'options': { 'defaultType': 'swap', # Perpetual futures }, 'timeout': 30000, 'enableRateLimit': True, } self.exchange = ccxt.okx(config) mode_str = "DEMO/SANDBOX" if self.okx_config.demo_mode else "LIVE" logger.info(f"OKX Exchange initialized in {mode_str} mode") # Configure trading settings self._configure_trading_settings() def _configure_trading_settings(self) -> None: """Configure leverage and margin mode.""" symbol = self.trading_config.eth_symbol leverage = self.trading_config.leverage margin_mode = self.trading_config.margin_mode try: # Set position mode to one-way (net) first self.exchange.set_position_mode(False) # False = one-way mode logger.info("Position mode set to One-Way (Net)") except Exception as e: # Position mode might already be set logger.debug(f"Position mode setting: {e}") try: # Set margin mode with leverage parameter (required by OKX) self.exchange.set_margin_mode( margin_mode, symbol, params={'lever': leverage} ) logger.info( f"Margin mode set to {margin_mode} with {leverage}x leverage " f"for {symbol}" ) except Exception as e: logger.warning(f"Could not set margin mode: {e}") # Try setting leverage separately try: self.exchange.set_leverage(leverage, symbol) logger.info(f"Leverage set to {leverage}x for {symbol}") except Exception as e2: logger.warning(f"Could not set leverage: {e2}") def fetch_ohlcv( self, symbol: str, timeframe: str = "1h", limit: int = 500 ) -> list: """ Fetch OHLCV candle data. Args: symbol: Trading pair symbol (e.g., "ETH/USDT:USDT") timeframe: Candle timeframe (e.g., "1h") limit: Number of candles to fetch Returns: List of OHLCV data """ return self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit) def get_balance(self) -> dict: """ Get account balance. Returns: Balance dictionary with 'total' and 'free' USDT amounts """ balance = self.exchange.fetch_balance() return { 'total': balance.get('USDT', {}).get('total', 0), 'free': balance.get('USDT', {}).get('free', 0), } def get_positions(self) -> list: """ Get open positions. Returns: List of open position dictionaries """ positions = self.exchange.fetch_positions() return [p for p in positions if float(p.get('contracts', 0)) != 0] def get_position(self, symbol: str) -> Optional[dict]: """ Get position for a specific symbol. Args: symbol: Trading pair symbol Returns: Position dictionary or None if no position """ positions = self.get_positions() for pos in positions: if pos.get('symbol') == symbol: return pos return None def place_market_order( self, symbol: str, side: str, amount: float, reduce_only: bool = False ) -> dict: """ Place a market order. Args: symbol: Trading pair symbol side: "buy" or "sell" amount: Order amount in base currency reduce_only: If True, only reduce existing position Returns: Order result dictionary """ params = { 'tdMode': self.trading_config.margin_mode, } if reduce_only: params['reduceOnly'] = True order = self.exchange.create_market_order( symbol, side, amount, params=params ) logger.info( f"Market {side.upper()} order placed: {amount} {symbol} " f"@ market price, order_id={order['id']}" ) return order def place_limit_order( self, symbol: str, side: str, amount: float, price: float, reduce_only: bool = False ) -> dict: """ Place a limit order. Args: symbol: Trading pair symbol side: "buy" or "sell" amount: Order amount in base currency price: Limit price reduce_only: If True, only reduce existing position Returns: Order result dictionary """ params = { 'tdMode': self.trading_config.margin_mode, } if reduce_only: params['reduceOnly'] = True order = self.exchange.create_limit_order( symbol, side, amount, price, params=params ) logger.info( f"Limit {side.upper()} order placed: {amount} {symbol} " f"@ {price}, order_id={order['id']}" ) return order def set_stop_loss_take_profit( self, symbol: str, side: str, amount: float, stop_loss_price: float, take_profit_price: float ) -> tuple: """ Set stop-loss and take-profit orders. Args: symbol: Trading pair symbol side: Position side ("long" or "short") amount: Position size stop_loss_price: Stop-loss trigger price take_profit_price: Take-profit trigger price Returns: Tuple of (sl_order, tp_order) """ # For long position: SL sells, TP sells # For short position: SL buys, TP buys close_side = "sell" if side == "long" else "buy" # Stop-loss order sl_params = { 'tdMode': self.trading_config.margin_mode, 'reduceOnly': True, 'stopLossPrice': stop_loss_price, } try: sl_order = self.exchange.create_order( symbol, 'market', close_side, amount, params={ 'tdMode': self.trading_config.margin_mode, 'reduceOnly': True, 'slTriggerPx': str(stop_loss_price), 'slOrdPx': '-1', # Market price } ) logger.info(f"Stop-loss set at {stop_loss_price}") except Exception as e: logger.warning(f"Could not set stop-loss: {e}") sl_order = None # Take-profit order try: tp_order = self.exchange.create_order( symbol, 'market', close_side, amount, params={ 'tdMode': self.trading_config.margin_mode, 'reduceOnly': True, 'tpTriggerPx': str(take_profit_price), 'tpOrdPx': '-1', # Market price } ) logger.info(f"Take-profit set at {take_profit_price}") except Exception as e: logger.warning(f"Could not set take-profit: {e}") tp_order = None return sl_order, tp_order def close_position(self, symbol: str) -> Optional[dict]: """ Close an open position. Args: symbol: Trading pair symbol Returns: Order result or None if no position """ position = self.get_position(symbol) if not position: logger.info(f"No open position for {symbol}") return None contracts = abs(float(position.get('contracts', 0))) if contracts == 0: return None side = position.get('side', 'long') close_side = "sell" if side == "long" else "buy" order = self.place_market_order( symbol, close_side, contracts, reduce_only=True ) logger.info(f"Position closed for {symbol}") return order def get_ticker(self, symbol: str) -> dict: """ Get current ticker/price for a symbol. Args: symbol: Trading pair symbol Returns: Ticker dictionary with 'last', 'bid', 'ask' prices """ return self.exchange.fetch_ticker(symbol) def get_funding_rate(self, symbol: str) -> float: """ Get current funding rate for a perpetual symbol. Args: symbol: Trading pair symbol Returns: Current funding rate as decimal """ try: funding = self.exchange.fetch_funding_rate(symbol) return float(funding.get('fundingRate', 0)) except Exception as e: logger.warning(f"Could not fetch funding rate: {e}") return 0.0