Enhance SL/TP calculation and order handling in LiveRegimeStrategy and OKXClient
- Updated `calculate_sl_tp` method to handle invalid entry prices and sides, returning (None, None) when necessary. - Improved logging for SL/TP values in `LiveTradingBot` to display "N/A" for invalid values. - Refined order placement in `OKXClient` to ensure guaranteed fill price retrieval, with fallback mechanisms for fetching order details and ticker prices if needed. - Added error handling for scenarios where fill prices cannot be determined.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user