"""
Order Manager — places TP/SL orders on Bybit.
Shared by all strategies.
Bybit advantage vs Binance:
- Conditional orders (stop-loss) ARE queryable via get_open_orders
- No invisible algo orders that can't be cancelled
- Can also use set_trading_stop on position level for simple strategies
For multi-TP strategies: separate limit orders (same as Binance approach).
"""
import logging
from typing import Optional
from src.exchange.client import BybitFuturesClient
from src.config import MAKER_FEE_PCT, TAKER_FEE_PCT
logger = logging.getLogger(__name__)
class OrderManager:
"""Manages exchange-side TP and SL orders on Bybit."""
def __init__(self, client: BybitFuturesClient):
self.client = client
def place_tp_sl_orders(
self,
symbol: str,
side: str,
sl_price: float,
sl_quantity: float,
tp_levels: list[tuple[float, float]],
) -> dict:
"""
Place SL + multiple TP orders on Bybit.
Args:
symbol: e.g. "BTCUSDT"
side: position side ("BUY" or "SELL")
sl_price: stop loss trigger price
sl_quantity: total position quantity for SL
tp_levels: [(tp_price, quantity), ...] — TP levels with qty per level
Returns:
{"sl_order_id": str|None, "tp_order_ids": [str|None, ...]}
"""
result = {"sl_order_id": None, "tp_order_ids": []}
# Place SL (conditional stop-market, queryable on Bybit)
sl_order = self.client.place_stop_order(symbol, side, sl_quantity, sl_price)
if sl_order:
result["sl_order_id"] = sl_order["orderId"]
logger.info(f"SL placed: {symbol} #{sl_order['orderId']} @ ${sl_price:.6f}")
else:
logger.error(f"Failed to place SL for {symbol}")
# Place TPs (regular LIMIT reduceOnly — maker fee)
closing_side = "SELL" if side.upper() == "BUY" else "BUY"
for tp_price, tp_qty in tp_levels:
tp_order = self.client.open_limit_order(
symbol, closing_side, tp_qty, tp_price, reduce_only=True
)
if tp_order:
result["tp_order_ids"].append(tp_order["orderId"])
logger.info(f"TP limit: {symbol} #{tp_order['orderId']} @ ${tp_price:.6f} qty={tp_qty}")
else:
result["tp_order_ids"].append(None)
logger.error(f"Failed to place TP for {symbol} @ ${tp_price:.6f}")
return result
def replace_sl_and_tps(
self,
symbol: str,
side: str,
new_sl_price: float,
sl_quantity: float,
remaining_tp_levels: list[tuple[float, float]],
) -> dict:
"""
Cancel all orders and re-place SL + remaining TPs.
Used after TP fills when SL needs to move.
"""
success = self.client.cancel_all_orders(symbol)
if not success:
logger.error(f"replace_sl_and_tps: cancel failed for {symbol}, placing anyway")
return self.place_tp_sl_orders(
symbol, side, new_sl_price, sl_quantity, remaining_tp_levels
)
def cancel_all_for_symbol(self, symbol: str) -> bool:
"""Cancel all open orders for a symbol."""
return self.client.cancel_all_orders(symbol)