← Назад
""" 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)