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