← Back
"""
Exchange Order Manager — places TP/SL orders on Binance Futures.

Shared by all 3 strategies (WT, Scalp, Gerchik).

TP = regular LIMIT order (reduceOnly) → maker fee 0.02%, queryable
SL = STOP_MARKET (algo/conditional) → instant on Binance, NOT queryable
     but we detect SL fill by: position gone + TP order still unfilled

cancel_all_orders() clears both regular and algo orders.
"""

import logging
from typing import Optional

from trader import BinanceFuturesTrader

logger = logging.getLogger(__name__)

# Fees
MAKER_FEE_PCT = 0.02
TAKER_FEE_PCT = 0.04


class ExchangeOrderManager:
    """Manages exchange-side TP and SL orders."""

    def __init__(self, trader: BinanceFuturesTrader):
        self.trader = trader

    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 Binance.

        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": int|None, "tp_order_ids": [int|None, ...]}
        """
        result = {"sl_order_id": None, "tp_order_ids": []}

        # Place SL (STOP_MARKET — algo order, not queryable but executes on Binance)
        sl_order = self.trader.place_stop_market(symbol, side, sl_quantity, sl_price)
        if sl_order:
            result["sl_order_id"] = sl_order["orderId"]  # This is algoId
            logger.info(f"SL placed: {symbol} algo#{sl_order['orderId']} @ ${sl_price:.6f}")
        else:
            logger.error(f"Failed to place SL for {symbol}")

        # Place TPs (regular LIMIT reduceOnly — queryable, maker fee)
        closing_side = "SELL" if side == "BUY" else "BUY"
        for tp_price, tp_qty in tp_levels:
            tp_order = self.trader.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 cancel_and_replace_sl(
        self,
        symbol: str,
        old_sl_order_id: Optional[int],
        side: str,
        new_sl_price: float,
        quantity: float,
    ) -> Optional[int]:
        """
        Cancel ALL orders for symbol, re-place SL + remaining TPs.

        Since algo orders can't be cancelled individually, we cancel everything
        then re-place the SL. Caller must re-place any remaining TP orders too.

        Returns new SL algo order ID or None.
        """
        # Cancel everything (works for both regular LIMIT and algo STOP_MARKET)
        self.trader.cancel_all_orders(symbol)

        # Place new SL
        sl_order = self.trader.place_stop_market(symbol, side, quantity, new_sl_price)
        if sl_order:
            logger.info(f"SL replaced: {symbol} algo#{sl_order['orderId']} @ ${new_sl_price:.6f}")
            return sl_order["orderId"]

        logger.error(f"Failed to replace SL for {symbol}")
        return None

    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 TP1/TP2 fills when SL needs to move.

        Returns {"sl_order_id": int|None, "tp_order_ids": [int|None, ...]}
        """
        # Cancel all (regular + algo) — now with verification
        success = self.trader.cancel_all_orders(symbol)
        if not success:
            logger.error(f"replace_sl_and_tps: cancel failed for {symbol}, placing anyway")

        # Re-place everything
        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 (manual close, external close, time-stop)."""
        return self.trader.cancel_all_orders(symbol)

📜 Git History

c6f6bd5chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...