diff --git a/live_trading/live_regime_strategy.py b/live_trading/live_regime_strategy.py index b33c31a..c182a90 100644 --- a/live_trading/live_regime_strategy.py +++ b/live_trading/live_regime_strategy.py @@ -261,9 +261,9 @@ class LiveRegimeStrategy: def calculate_sl_tp( self, - entry_price: float, + entry_price: Optional[float], side: str - ) -> tuple[float, float]: + ) -> tuple[Optional[float], Optional[float]]: """ Calculate stop-loss and take-profit prices. @@ -272,10 +272,23 @@ class LiveRegimeStrategy: side: "long" or "short" Returns: - Tuple of (stop_loss_price, take_profit_price) + Tuple of (stop_loss_price, take_profit_price), or (None, None) if + entry_price is invalid + + Raises: + ValueError: If side is not "long" or "short" """ + if entry_price is None or entry_price <= 0: + logger.error( + f"Invalid entry_price for SL/TP calculation: {entry_price}" + ) + return None, None + + if side not in ("long", "short"): + raise ValueError(f"Invalid side: {side}. Must be 'long' or 'short'") + sl_pct = self.config.stop_loss_pct - tp_pct = self.config.take_profit_pct + tp_pct = self.config.take_profit_pct if side == "long": stop_loss = entry_price * (1 - sl_pct) diff --git a/live_trading/main.py b/live_trading/main.py index 4df79f6..ae0061d 100644 --- a/live_trading/main.py +++ b/live_trading/main.py @@ -200,34 +200,33 @@ class LiveTradingBot: size_eth = size_usdt / current_price - # Calculate SL/TP + # Calculate SL/TP for logging stop_loss, take_profit = self.strategy.calculate_sl_tp(current_price, side) + sl_str = f"{stop_loss:.2f}" if stop_loss else "N/A" + tp_str = f"{take_profit:.2f}" if take_profit else "N/A" self.logger.info( f"Executing {side.upper()} entry: {size_eth:.4f} ETH @ {current_price:.2f} " - f"(${size_usdt:.2f}), SL={stop_loss:.2f}, TP={take_profit:.2f}" + f"(${size_usdt:.2f}), SL={sl_str}, TP={tp_str}" ) try: - # Place market order + # Place market order (guaranteed to have fill price or raises) order_side = "buy" if side == "long" else "sell" order = self.okx_client.place_market_order(symbol, order_side, size_eth) - # Get filled price (handle None values from OKX response) - filled_price = order.get('average') or order.get('price') or current_price - filled_amount = order.get('filled') or order.get('amount') or size_eth + # Get filled price and amount (guaranteed by OKX client) + filled_price = order['average'] + filled_amount = order.get('filled') or size_eth - # Ensure we have valid numeric values - if filled_price is None or filled_price == 0: - self.logger.warning(f"No fill price in order response, using current price: {current_price}") - filled_price = current_price - if filled_amount is None or filled_amount == 0: - self.logger.warning(f"No fill amount in order response, using requested: {size_eth}") - filled_amount = size_eth - - # Recalculate SL/TP with filled price + # Calculate SL/TP with filled price stop_loss, take_profit = self.strategy.calculate_sl_tp(filled_price, side) + if stop_loss is None or take_profit is None: + raise RuntimeError( + f"Failed to calculate SL/TP: filled_price={filled_price}, side={side}" + ) + # Get order ID from response order_id = order.get('id', '') diff --git a/live_trading/okx_client.py b/live_trading/okx_client.py index e0e97e5..b226f3d 100644 --- a/live_trading/okx_client.py +++ b/live_trading/okx_client.py @@ -153,7 +153,7 @@ class OKXClient: reduce_only: bool = False ) -> dict: """ - Place a market order. + Place a market order and fetch the fill price. Args: symbol: Trading pair symbol @@ -162,7 +162,10 @@ class OKXClient: reduce_only: If True, only reduce existing position Returns: - Order result dictionary + Order result dictionary with guaranteed 'average' fill price + + Raises: + RuntimeError: If order placement fails or fill price unavailable """ params = { 'tdMode': self.trading_config.margin_mode, @@ -173,10 +176,48 @@ class OKXClient: order = self.exchange.create_market_order( symbol, side, amount, params=params ) + + order_id = order.get('id') + if not order_id: + raise RuntimeError(f"Order placement failed: no order ID returned") + logger.info( f"Market {side.upper()} order placed: {amount} {symbol} " - f"@ market price, order_id={order['id']}" + f"@ market price, order_id={order_id}" ) + + # Fetch order to get actual fill price if not in initial response + fill_price = order.get('average') + if fill_price is None or fill_price == 0: + logger.info(f"Fetching order {order_id} for fill price...") + try: + fetched_order = self.exchange.fetch_order(order_id, symbol) + fill_price = fetched_order.get('average') + order['average'] = fill_price + order['filled'] = fetched_order.get('filled', order.get('filled')) + order['status'] = fetched_order.get('status', order.get('status')) + except Exception as e: + logger.warning(f"Could not fetch order details: {e}") + + # Final fallback: use current ticker price + if fill_price is None or fill_price == 0: + logger.warning( + f"No fill price from order response, fetching ticker..." + ) + try: + ticker = self.get_ticker(symbol) + fill_price = ticker.get('last') + order['average'] = fill_price + except Exception as e: + logger.error(f"Could not fetch ticker: {e}") + + if fill_price is None or fill_price <= 0: + raise RuntimeError( + f"Could not determine fill price for order {order_id}. " + f"Order response: {order}" + ) + + logger.info(f"Order {order_id} filled at {fill_price}") return order def place_limit_order(