← Back
"""
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)