# source: https://raw.githubusercontent.com/aabudeev/freqtrade/9c29560b14047786bc6cae0ac3bd0e5d8ad19b92/user_data/strategies/SignalOnlyStrategy.py
# pragma pylint: disable=missing-docstring
"""Strictly Signal-based Strategy. No automated TA entries."""

from pandas import DataFrame
from datetime import datetime, timezone
import logging

logger = logging.getLogger(__name__)
# Silence noisy Freqtrade core loggers
logging.getLogger('freqtrade.freqtradebot').setLevel(logging.WARNING)
logging.getLogger('freqtrade.worker').setLevel(logging.WARNING)
logging.getLogger('freqtrade.resolvers.strategy_resolver').setLevel(logging.WARNING)

from freqtrade.strategy import IStrategy
from freqtrade.persistence import Trade
from freqtrade.signals.queue_store import SignalQueueStore


class Github_aabudeev_freqtrade__SignalOnlyStrategy__20260605_063513(IStrategy):
    """
    Strategy for executing external signals ONLY.
    Entries are made via SignalWorker (Telegram/API).
    """

    def __init__(self, config: dict) -> None:
        super().__init__(config)
        self.signal_store = SignalQueueStore("/freqtrade/user_data/signals.db")

    INTERFACE_VERSION = 3
    can_short: bool = True

    # Entry/Exit timeouts
    unfilledtimeout = {
        'entry': 60,
        'exit': 525600,
        'exit_timeout_count': 0,
        'unit': 'minutes'
    }

    # Reconciliation throttle (5 minutes)
    _last_reconcile_ts = 0
    _reconcile_interval = 300 # seconds

    minimal_roi = {"0": 10.0}  # Effectively disabled
    stoploss = -0.99           # Fallback only
    
    # --- DYNAMIC STOPLOSS ENABLED ---
    use_custom_stoploss = False
    
    # TRAILING STOP DISABLED
    trailing_stop = False
    process_only_new_candles = False
    use_exit_signal = False
    startup_candle_count = 20

    order_types = {
        "entry": "market",
        "exit": "limit",
        "stoploss": "market",
        "stoploss_on_exchange": False,
    }
    order_time_in_force = {"entry": "GTC", "exit": "GTC"}

    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, side: str,
                 **kwargs) -> float:
        settings = self.signal_store.get_settings()
        lev = float(settings.get('signal_strategy_leverage', 50.0))
        return min(lev, max_leverage)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # No indicators for signal strategy
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Entries only via SignalWorker
        dataframe.loc[:, 'enter_long'] = 0
        dataframe.loc[:, 'enter_short'] = 0
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, "exit_long"] = 0
        dataframe.loc[:, "exit_short"] = 0
        return dataframe

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, **kwargs) -> float:
        """
        DISABLED: We rely entirely on BingX native STOP_MARKET orders.
        Using Freqtrade's internal custom_stoploss causes race conditions where 
        both Freqtrade and the Exchange try to close the trade simultaneously, 
        resulting in 101290 Reduce Only errors.
        """
        return -1.0

    def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
        """
        Reconcile missing orders on exchange (startup and loop).
        """
        if self.config['exchange']['name'].lower() != 'bingx':
            return

        try:
            from freqtrade.persistence import Trade, Order
            from datetime import datetime
            
            # Use direct CCXT API for reconciliation
            if not (self.dp and hasattr(self.dp, '_exchange') and self.dp._exchange and hasattr(self.dp._exchange, '_api')):
                return
            
            # --- INTERVAL CHECK ---
            # Run reconciliation every 5 minutes to avoid log spam and API rate limits
            now_ts = datetime.now().timestamp()
            last_check = getattr(self, '_last_reconcile_ts', 0)
            if now_ts - last_check < self._reconcile_interval:
                return
            self._last_reconcile_ts = now_ts
            
            api = self.dp._exchange._api
            open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
            
            # --- ONE-TIME DATA REPAIR ---
            # Fix trades closed by healer previously that have None in profit fields (causes REST API crash)
            try:
                bad_trades = Trade.get_trades([Trade.is_open.is_(False), Trade.close_profit.is_(None)]).all()
                for bt in bad_trades:
                    logger.warning(f"BINGX RECONCILE: Repairing data for closed trade {bt.id} ({bt.pair})")
                    bt.close_profit = 0.0
                    bt.close_profit_abs = 0.0
                    if bt.close_rate is None:
                        bt.close_rate = bt.open_rate
                if bad_trades:
                    Trade.commit()
            except Exception as e_repair:
                logger.debug(f"BINGX RECONCILE: Repair failed (ignoring): {e_repair}")
                Trade.session.rollback()

            # Fix closed trades where PnL sign disagrees with open/close rates (BingX qty mismatch)
            try:
                self._bingx_repair_closed_trade_profits()
            except Exception as e_pnl:
                logger.debug(f"BINGX RECONCILE: Closed-trade PnL repair skipped: {e_pnl}")
            
            # 1. Get all open trades from DB
            open_trades = Trade.get_open_trades()
            if not open_trades:
                return

            logger.info(f"BINGX RECONCILE: Starting aggressive check for {len(open_trades)} trades...")

            # 2. Fetch all positions once to check for orphans
            all_positions = []
            try:
                all_positions = api.fetch_positions()
            except Exception as e_pos:
                logger.error(f"BINGX RECONCILE: Could not fetch positions: {e_pos}")
                return  # SAFETY: If we can't fetch positions, do NOT assume all trades are ghosts

            # SAFETY: If exchange returned zero positions, that is completely normal if our only trade hit SL/TP.
            # We ONLY skip if the API call raised an exception (handled above).
            
            # Log what we got for debugging
            pos_symbols = [p.get('symbol', '?') for p in all_positions if float(p.get('contracts', 0) or p.get('size', 0) or 0) != 0]
            logger.info(f"BINGX RECONCILE: Active positions on exchange: {pos_symbols}")

            for trade in open_trades:
                changed = False
                if self._bingx_rescale_contract_order_fills(trade):
                    trade.recalc_trade_from_orders()
                    changed = True
                if self._bingx_repair_open_trade_value(trade):
                    changed = True
                if changed:
                    Trade.commit()

                # --- SAFETY: Skip trades younger than 10 minutes ---
                trade_age_seconds = (datetime.now(timezone.utc) - trade.open_date_utc).total_seconds() if trade.open_date_utc else 0
                if trade_age_seconds < 600:
                    logger.debug(f"BINGX RECONCILE: Trade {trade.id} ({trade.pair}) is only {trade_age_seconds:.0f}s old, skipping reconciliation.")
                    continue

                # --- LIMIT ENTRY SAFETY (disabled — back to market orders) ---
                # has_filled_entry = any(o.ft_order_side == trade.entry_side and (o.filled or 0) > 0 for o in trade.orders)
                # if not has_filled_entry:
                #     logger.debug(f"BINGX RECONCILE: Trade {trade.id} ({trade.pair}) has no filled entry orders yet. Skipping reconciliation.")
                #     continue

                # --- AGGRESSIVE BINGX RECONCILE ---
                # Match using CCXT unified symbol format (e.g. "DOGE/USDT:USDT")
                has_position = False
                for p in all_positions:
                    if p.get('symbol') == trade.pair:
                        p_contracts = float(p.get('contracts', 0) or p.get('size', 0) or 0)
                        if p_contracts != 0:
                            has_position = True
                            break
                
                if not has_position:
                    logger.warning(f"BINGX RECONCILE: Trade {trade.id} ({trade.pair}) has NO position on exchange (age: {trade_age_seconds:.0f}s). FORCE CLOSING.")
                    
                    actual_close_rate, close_src = self._bingx_resolve_close_rate_when_position_gone(
                        trade, api
                    )
                    if actual_close_rate:
                        logger.info(
                            f"BINGX RECONCILE: Close price for {trade.pair}: {actual_close_rate} "
                            f"(source={close_src})"
                        )
                    
                    if not actual_close_rate:
                        actual_close_rate = trade.open_rate
                        close_src = "open_rate_fallback"
                        logger.warning(
                            f"BINGX RECONCILE: Using open_rate as last resort for {trade.pair} "
                            f"— statistics may be inaccurate!"
                        )

                    trade.close_rate = actual_close_rate
                    trade.close_date = datetime.now(timezone.utc)
                    trade.is_open = False
                    if close_src.startswith("trade_order_limit"):
                        trade.exit_reason = "take_profit"
                    else:
                        trade.exit_reason = "reconciled_missing_position"
                        for o in trade.orders:
                            if (
                                o.ft_order_side == trade.exit_side
                                and (o.order_type or "").lower() == "limit"
                                and o.price
                                and abs((trade.close_rate or 0) - o.price) / o.price < 0.02
                            ):
                                trade.exit_reason = "take_profit"
                                break
                    trade.recalc_trade_from_orders(is_closing=True)
                    expected_ratio = trade.calc_profit_ratio(actual_close_rate)
                    if trade.close_profit is None or trade.close_profit * expected_ratio < 0:
                        trade.close_profit = expected_ratio
                        trade.close_profit_abs = trade.calc_profit(actual_close_rate)
                    
                    # --- Cancel remaining open/pending orders on exchange for this symbol ---
                    try:
                        symbol_api = trade.pair.replace("/", "-").split(":")[0]
                        # Fetch open limit orders
                        open_ord = api.fetch_open_orders(trade.pair)
                        # Fetch pending trigger orders
                        pending_ord = []
                        raw_method = getattr(api, 'swapV2PrivateGetTradePendingOrders', None)
                        if raw_method:
                            resp = raw_method({"symbol": symbol_api})
                            if isinstance(resp, dict) and 'data' in resp:
                                pending_ord = resp['data']
                        
                        all_ords = open_ord + pending_ord
                        for o in all_ords:
                            o_id = o.get('id') or o.get('orderId')
                            if o_id:
                                logger.info(f"BINGX RECONCILE: Cancelling remaining order {o_id} for {trade.pair} since position is closed.")
                                try:
                                    api.cancel_order(str(o_id), trade.pair)
                                except Exception as e_cancel:
                                    logger.warning(f"BINGX RECONCILE: Failed to cancel order {o_id}: {e_cancel}")
                    except Exception as e_clean:
                        logger.warning(f"BINGX RECONCILE: Failed to clean up remaining orders for {trade.pair}: {e_clean}")
                    
                    # Sync exit orders from exchange before marking canceled
                    ex = self.dp._exchange
                    for o in trade.orders:
                        if o.ft_order_side != trade.exit_side:
                            continue
                        try:
                            raw = api.fetch_order(str(o.order_id), trade.pair)
                            if hasattr(ex, "_order_contracts_to_amount"):
                                raw = ex._order_contracts_to_amount(raw)
                            filled = float(raw.get("filled") or 0)
                            if raw.get("status") in ("closed", "filled") and filled > 0:
                                o.status = "closed"
                                o.filled = filled
                                o.average = raw.get("average") or raw.get("price") or o.price
                                o.ft_is_open = False
                                continue
                        except Exception:
                            pass
                        if o.ft_is_open:
                            o.ft_is_open = False
                            o.status = "canceled"
                    
                    # Force commit to DB
                    from freqtrade.persistence import Trade
                    Trade.session.add(trade)
                    Trade.session.commit()
                    logger.info(f"BINGX RECONCILE: Trade {trade.id} closed at {actual_close_rate} (profit: {trade.close_profit_abs:.4f} USDT).")
                    continue

                # --- SAFETY CHECK ---
                # Skip trades that are already in the process of exiting to avoid DB conflicts
                if trade.exit_reason or any(o.ft_order_side == trade.exit_side and o.ft_is_open for o in trade.orders):
                    continue

                # Fetch orders using CCXT unified and raw methods
                symbol_api = trade.pair.replace("/", "-").split(":")[0]
                try:
                    # 1. Fetch regular open orders (Limits)

                    # Use the configured api object directly
                    open_orders = api.fetch_open_orders(trade.pair)
                    
                    # 2. Fetch pending/trigger orders (Stops)
                    pending_orders = []
                    
                    # Try raw BingX method which we know exists in this CCXT version
                    try:
                        # Use getattr for maximum safety
                        raw_method = getattr(api, 'swapV2PrivateGetTradePendingOrders', None)
                        if raw_method:
                            resp = raw_method({"symbol": symbol_api})
                            if isinstance(resp, dict) and 'data' in resp:
                                pending_orders = resp['data']
                    except Exception as e_pend:
                        logger.debug(f"BINGX RECONCILE: Pending fallback error: {e_pend}")
                    
                    all_exchange_orders = open_orders + pending_orders

                except Exception as e_fetch:
                    logger.error(f"BINGX RECONCILE: Fetch error for {trade.pair}: {e_fetch}")
                    continue

                # --- RECONCILE TAKE PROFIT (TP) ---
                has_tp = any(o.ft_order_side == trade.exit_side and o.ft_is_open for o in trade.orders)
                if not has_tp:
                    try:
                        tp_order_id = None
                        tp_target = trade.get_custom_data("signal_tp")
                        tp_side = 'SELL' if not trade.is_short else 'BUY'
                        
                        # 3. Scan ALL exchange orders for matches
                        for o in all_exchange_orders:
                            o_side = str(o.get('side', o.get('orderSide', ''))).upper()
                            o_type = str(o.get('type', o.get('orderType', ''))).upper()
                            
                            # BE AGNOSTIC: If it's a SELL order at our TP price, IT IS OUR TP.
                            # Don't strictly check for 'LIMIT' as BingX might call it 'TAKE_PROFIT_LIMIT' etc.
                            if o_side == tp_side:
                                o_price = float(o.get('price') or o.get('stopPrice') or 0)
                                o_amount = float(o.get('amount') or o.get('quantity') or 0)
                                
                                # Check price (within 0.5%) AND amount (within 0.1%)
                                price_match = tp_target and abs(o_price - float(tp_target)) / float(tp_target) < 0.005
                                amount_match = abs(o_amount - trade.amount) / trade.amount < 0.001 if trade.amount > 0 else False
                                
                                if price_match and amount_match:
                                    tp_order_id = str(o.get('id') or o.get('orderId'))
                                    logger.info(f"BINGX RECONCILE: Recognized existing TP {tp_order_id} for {trade.pair} (Type: {o_type}, Amount: {o_amount})")
                                    break
                                elif price_match and not amount_match:
                                    logger.warning(f"BINGX RECONCILE: Found TP for {trade.pair} with WRONG AMOUNT: {o_amount} vs expected {trade.amount}. Will recreate.")
                                    # We don't break here, so we might find a better match or trigger recreate below
                                    pass
                        
                        if tp_order_id:
                            self._register_order(trade, tp_order_id, 'exit', float(tp_target))
                            has_tp = True
                        
                        if not has_tp and tp_target:
                            # Log what we saw to find the "bitch"
                            logger.warning(f"BINGX RECONCILE: Missing TP for {trade.pair}! Saw orders: {[str(o.get('type')) + '@' + str(o.get('price') or o.get('stopPrice')) for o in all_exchange_orders]}")
                            tp_target_rounded = round(float(tp_target), 8)
                            logger.info(f"BINGX RECONCILE: Placing missing TP for {trade.pair} at {tp_target_rounded}")
                            symbol = trade.pair.replace("/", "-").split(":")[0]
                            pos_side = "SHORT" if trade.is_short else "LONG"
                            tp_order = api.swapV2PrivatePostTradeOrder({
                                "symbol": symbol,
                                "side": tp_side.upper(),
                                "positionSide": "BOTH",
                                "type": "LIMIT",
                                "quantity": trade.amount,
                                "price": tp_target_rounded,
                                "reduceOnly": "true"
                            })
                            if tp_order and 'data' in tp_order and isinstance(tp_order['data'], dict):
                                order_id = tp_order['data'].get('orderId')
                                if not order_id and isinstance(tp_order['data'].get('order'), dict):
                                    order_id = tp_order['data']['order'].get('orderId')
                                if order_id:
                                    self._register_order(trade, str(order_id), 'exit', float(tp_target))
                                    has_tp = True
                    except Exception as e_tp:
                        logger.error(f"BINGX RECONCILE: TP Error for {trade.pair}: {e_tp}")

                # --- RECONCILE STOP LOSS (SL) ---
                try:
                    sl_order_id = None
                    has_sl = any(o.ft_order_side == 'stoploss' and o.ft_is_open for o in trade.orders)
                    
                    # Get the 'source of truth' for SL from custom_data
                    signal_sl_str = trade.get_custom_data("signal_sl")
                    sl_price = float(signal_sl_str) if signal_sl_str else trade.stop_loss
                    
                    # If Freqtrade core has overwritten our SL with default strategy SL, fix it back
                    # Use a relative difference check (0.05%) to avoid rounding spam
                    relative_diff = abs(trade.stop_loss - sl_price) / sl_price if sl_price > 0 else 0
                    if signal_sl_str and relative_diff > 0.0005:
                        logger.warning(f"BINGX RECONCILE: Healing corrupted SL for {trade.pair}: {trade.stop_loss} -> {sl_price} (diff: {relative_diff:.4%})")
                        trade.stop_loss = sl_price
                        # Recalculate ratios so FT doesn't try to 'fix' it again
                        leverage = trade.leverage or 1.0
                        sl_ratio = abs((trade.open_rate - sl_price) / trade.open_rate)
                        sl_trade_ratio = sl_ratio * leverage
                        trade.stoploss = -sl_trade_ratio
                        trade.stop_loss_pct = -sl_trade_ratio
                        trade.initial_stop_loss = sl_price
                        trade.initial_stop_loss_pct = -sl_trade_ratio
                        # Use session commit to ensure it hits the disk
                        Trade.session.commit()

                    if not has_sl:
                        target_side = 'SELL' if not trade.is_short else 'BUY'
                        
                        # 3. Scan ALL exchange orders for matches
                        for o in all_exchange_orders:
                            o_side = str(o.get('side', o.get('orderSide', ''))).upper()
                            o_type = str(o.get('type', o.get('orderType', ''))).upper()
                            
                            if o_side == target_side:
                                # Check every possible price field for SL (stopPrice, triggerPrice, etc)
                                o_stop_price = float(o.get('stopPrice') or o.get('triggerPrice') or o.get('stop_price') or o.get('price') or 0)
                                o_amount = float(o.get('amount') or o.get('quantity') or 0)

                                # Check price (within 0.5%) AND amount (within 0.1%)
                                price_match = sl_price and abs(o_stop_price - float(sl_price)) / float(sl_price) < 0.005
                                amount_match = abs(o_amount - trade.amount) / trade.amount < 0.001 if trade.amount > 0 else False

                                if price_match:
                                    # If price matches but amount doesn't, we still recognize it but log a warning
                                    # This handles cases where user has other positions on the same pair (e.g. VST/Demo)
                                    sl_order_id = str(o.get('id') or o.get('orderId'))
                                    if not amount_match:
                                        logger.warning(f"BINGX RECONCILE: Recognized SL {sl_order_id} for {trade.pair} but AMOUNT MISMATCH: {o_amount} vs {trade.amount}. Proceeding anyway.")
                                    else:
                                        logger.info(f"BINGX RECONCILE: Recognized existing SL {sl_order_id} for {trade.pair}")
                                    break
                        
                        if sl_order_id:
                            self._register_order(trade, sl_order_id, 'stoploss', float(sl_price))
                            has_sl = True
                        
                        if not has_sl and sl_price:
                            # Only skip if we found a match above. 
                            # Previous 'non_limit_orders' check was too broad.
                            if not has_sl:
                                sl_price_rounded = round(float(sl_price), 8)
                                logger.info(f"BINGX RECONCILE: Placing missing SL for {trade.pair} at {sl_price_rounded}")
                                symbol = trade.pair.replace("/", "-").split(":")[0]
                                try:
                                    sl_order = api.swapV2PrivatePostTradeOrder({
                                        "symbol": symbol,
                                        "side": target_side.upper(),
                                        "positionSide": "BOTH",
                                        "type": "STOP_MARKET",
                                        "quantity": trade.amount,
                                        "stopPrice": sl_price_rounded,
                                        "reduceOnly": "true"
                                    })
                                    if sl_order and 'data' in sl_order and isinstance(sl_order['data'], dict):
                                        order_id = sl_order['data'].get('orderId')
                                        if order_id:
                                            self._register_order(trade, str(order_id), 'stoploss', float(sl_price))
                                            has_sl = True
                                except Exception as e_place_sl:
                                    logger.error(f"BINGX RECONCILE: Could not place SL for {trade.pair}: {e_place_sl}")
                except Exception as e_sl:
                    logger.error(f"BINGX RECONCILE: SL Error for {trade.pair}: {e_sl}")

        except Exception as e:
            logger.error(f"BINGX RECONCILE: Global error: {e}")

    def _bingx_swap_qty_multiplier(self, pair: str) -> float:
        if not (self.dp and getattr(self.dp, "_exchange", None)):
            return 1.0
        ex = self.dp._exchange
        if ex.id.lower() != "bingx":
            return 1.0
        market = ex.markets.get(pair) or {}
        if not market.get("contract"):
            return 1.0
        from freqtrade.exchange.bingx import Bingx

        return Bingx._bingx_swap_trade_min_quantity(market)

    def _bingx_rescale_contract_order_fills(self, trade: Trade) -> bool:
        """Fix Order.filled/amount persisted in contract steps (e.g. SOL 0.12 vs 4.19)."""
        from freqtrade.exchange.bingx import Bingx

        mult = self._bingx_swap_qty_multiplier(trade.pair)
        changed = False
        ref_amount = float(trade.amount or 0)
        for order in trade.orders:
            filled = float(order.filled or 0)
            amount = float(order.amount or order.ft_amount or 0)
            if filled <= 0:
                continue
            probe = {
                "filled": filled,
                "amount": amount or ref_amount,
                "average": order.average or order.price or trade.close_rate or trade.open_rate,
                "cost": order.cost,
            }
            scale = Bingx._bingx_swap_fill_scale_factor(probe)
            if scale == 1.0 and mult > 1.0:
                if Bingx._bingx_swap_order_amounts_are_contract_units(probe, mult):
                    scale = mult
            if scale == 1.0:
                continue
            order.filled = filled * scale
            if amount > 0:
                order.amount = amount * scale
            if order.remaining:
                order.remaining = float(order.remaining) * scale
            changed = True
        return changed

    def _bingx_resolve_close_rate_when_position_gone(self, trade: Trade, api) -> tuple[float | None, str]:
        """
        Position gone on BingX but trade still open in DB — resolve real exit price.

        Prefer filled TP/exit orders registered on the trade (not a random recent buy).
        """
        ex = self.dp._exchange
        exit_side = trade.exit_side
        known_ids = {str(o.order_id) for o in trade.orders if o.order_id}

        def _norm(order: dict) -> dict:
            if hasattr(ex, "_order_contracts_to_amount"):
                return ex._order_contracts_to_amount(order)
            return order

        # 1) Exit orders already linked to this trade
        for o in trade.orders:
            if o.ft_order_side != exit_side or not o.order_id:
                continue
            try:
                raw = _norm(api.fetch_order(str(o.order_id), trade.pair))
                filled = float(raw.get("filled") or 0)
                if raw.get("status") in ("closed", "filled") and filled > 0:
                    px = float(raw.get("average") or raw.get("price") or o.price or 0)
                    if px > 0:
                        tag = "trade_order_limit" if (o.order_type or "").lower() == "limit" else "trade_order"
                        return px, f"{tag}_{o.order_id}"
            except Exception as e:
                logger.debug(f"BINGX RECONCILE: fetch_order {o.order_id}: {e}")

        # 2) Closed-order history — only orders we know + best TP-like price for shorts
        try:
            closed_orders = [_norm(co) for co in api.fetch_closed_orders(trade.pair, limit=40)]
            expected_side = "buy" if trade.is_short else "sell"
            matched: list[tuple[float, str, float]] = []
            for co in closed_orders:
                if co.get("status") not in ("closed", "filled"):
                    continue
                if str(co.get("side", "")).lower() != expected_side:
                    continue
                filled = float(co.get("filled") or 0)
                if filled <= 0:
                    continue
                oid = str(co.get("id", ""))
                if known_ids and oid not in known_ids:
                    continue
                px = float(co.get("average") or co.get("price") or 0)
                if px <= 0:
                    continue
                matched.append((px, oid, filled))

            if matched:
                if trade.is_short:
                    # Short cover buy: lower price = better exit (TP side)
                    px, oid, _ = min(matched, key=lambda x: x[0])
                else:
                    px, oid, _ = max(matched, key=lambda x: x[0])
                return px, f"order_{oid}"
        except Exception as e_hist:
            logger.debug(f"BINGX RECONCILE: fetch_closed_orders {trade.pair}: {e_hist}")

        # 3) Market ticker (last resort before open_rate)
        try:
            ticker = api.fetch_ticker(trade.pair)
            px = float(ticker.get("last") or 0)
            if px > 0:
                return px, "ticker_last"
        except Exception as e_tick:
            logger.debug(f"BINGX RECONCILE: fetch_ticker {trade.pair}: {e_tick}")

        return None, "unknown"

    def _bingx_repair_open_trade_value(self, trade: Trade) -> bool:
        """Fix stale open_trade_value that inflates unrealized PnL (e.g. SOL -77% vs -11%)."""
        if not trade.is_open or not trade.amount or not trade.open_rate:
            return False
        changed = False
        expected = trade._calc_open_trade_value(trade.amount, trade.open_rate)
        if not trade.open_trade_value:
            trade.recalc_open_trade_value()
            changed = True
        err = abs((trade.open_trade_value or 0.0) - expected) / expected
        if err > 0.02:
            logger.warning(
                f"BINGX RECONCILE: Repairing open_trade_value on trade {trade.id} ({trade.pair}): "
                f"{trade.open_trade_value:.4f} -> {expected:.4f}"
            )
            trade.recalc_open_trade_value()
            changed = True
        # BingX reports realized funding separately from unrealized PnL.
        # If stale/overblown funding remains attached to an open trade, UI PnL is inflated.
        funding = float(trade.funding_fees or 0.0)
        max_reasonable = float(trade.stake_amount or 0.0) * 0.20
        if abs(funding) > max_reasonable > 0:
            logger.warning(
                f"BINGX RECONCILE: Reset abnormal funding_fees on trade {trade.id} ({trade.pair}): "
                f"{funding:.4f} -> 0.0"
            )
            trade.funding_fees = 0.0
            changed = True
        # Funding can be reintroduced from persisted order.funding_fee during recalculation.
        for order in trade.orders:
            ofee = float(order.funding_fee or 0.0)
            if abs(ofee) > max_reasonable > 0:
                logger.warning(
                    f"BINGX RECONCILE: Reset abnormal order funding on trade {trade.id} "
                    f"order {order.order_id}: {ofee:.4f} -> 0.0"
                )
                order.funding_fee = 0.0
                changed = True
        # Safety net: if live PnL is far from price-only expectation, force-reset stale components.
        try:
            current_rate = self.dp._exchange.get_rate(
                trade.pair, side="exit", is_short=trade.is_short, refresh=False
            )
            if current_rate and trade.open_rate:
                price_only_ratio = ((1 - (current_rate / trade.open_rate)) * trade.leverage)
                model_ratio = trade.calc_profit_ratio(current_rate)
                if abs(model_ratio - price_only_ratio) > 0.25:
                    logger.warning(
                        f"BINGX RECONCILE: Repairing PnL drift on trade {trade.id} ({trade.pair}): "
                        f"model={model_ratio:.4f}, price_only={price_only_ratio:.4f}"
                    )
                    # Some legacy trades got corrupted fee rates (e.g. 2-4% instead of 0.05%).
                    # That alone can keep unrealized PnL permanently distorted.
                    sane_fee = 0.0005
                    if self.dp and getattr(self.dp, "_exchange", None):
                        try:
                            sane_fee = float(
                                self.dp._exchange.get_fee(symbol=trade.pair, taker_or_maker="maker")
                            )
                        except Exception:
                            sane_fee = 0.0005
                    if (trade.fee_open or 0.0) > 0.01 or (trade.fee_close or 0.0) > 0.01:
                        logger.warning(
                            f"BINGX RECONCILE: Reset abnormal fees on trade {trade.id} ({trade.pair}): "
                            f"open={trade.fee_open}, close={trade.fee_close} -> {sane_fee}"
                        )
                        trade.fee_open = sane_fee
                        trade.fee_close = sane_fee
                    for order in trade.orders:
                        if order.funding_fee:
                            order.funding_fee = 0.0
                    trade.funding_fees = 0.0
                    trade.funding_fee_running = 0.0
                    trade.recalc_open_trade_value()
                    changed = True
        except Exception:
            pass
        return changed

    def _bingx_repair_closed_trade_profits(self) -> None:
        """Recalc or fix close_profit when exit fills were stored in contract units."""
        from freqtrade.persistence import Trade

        repaired = 0
        for trade in Trade.get_trades([Trade.is_open.is_(False)]).all():
            if trade.close_rate is None or trade.open_rate is None or trade.close_profit is None:
                continue
            expected_ratio = trade.calc_profit_ratio(trade.close_rate)
            if trade.close_profit * expected_ratio >= 0:
                continue
            logger.warning(
                f"BINGX RECONCILE: Repairing inverted PnL on trade {trade.id} ({trade.pair}): "
                f"stored={trade.close_profit:.4f}, expected={expected_ratio:.4f}"
            )
            if self._bingx_rescale_contract_order_fills(trade):
                logger.info(
                    f"BINGX RECONCILE: Rescaled contract-step fills on trade {trade.id} orders."
                )
            trade.recalc_trade_from_orders(is_closing=True)
            if trade.close_profit is None or trade.close_profit * expected_ratio < 0:
                trade.close_profit = expected_ratio
                trade.close_profit_abs = trade.calc_profit(trade.close_rate)
            repaired += 1
        if repaired:
            Trade.commit()
            logger.info(f"BINGX RECONCILE: Repaired PnL on {repaired} closed trade(s).")

    def _register_order(self, trade: Trade, order_id: str, side: str, price: float) -> None:
        from freqtrade.persistence import Order, Trade
        
        # IDEMPOTENCY CHECK: Don't register if already in trade.orders
        if any(str(o.order_id) == str(order_id) for o in trade.orders):
            return

        # Map 'exit' -> trade.exit_side ('sell' for Long, 'buy' for Short)
        # Freqtrade core only recognizes 'buy', 'sell', 'stoploss' for ft_order_side
        db_side = trade.exit_side if side == 'exit' else side

        new_order = Order(
            ft_trade_id=trade.id,
            ft_pair=trade.pair,
            ft_is_open=True,
            ft_order_side=db_side,
            ft_amount=trade.amount,
            ft_price=price,
            order_id=order_id,
            status='open',
            symbol=trade.pair,
            order_type='limit' if side == 'exit' else 'stoploss',
            side=trade.exit_side,
            amount=trade.amount,
            filled=0.0,
            remaining=trade.amount,
            price=price,
            order_date=datetime.now(timezone.utc)
        )
        trade.orders.append(new_order)
        Trade.commit()
        logger.info(f"BINGX RECONCILE: Registered exchange order {order_id} to DB for {trade.pair}")

    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
                    current_profit: float, **kwargs) -> str | bool | None:
        # We use exchange-side LIMIT orders for TP (managed in bot_loop_start reconciliation)
        # So we don't need manual TP check here anymore. 
        # This prevents market-order conflicts with existing limit orders.
        return None

    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
                           rate: float, time_in_force: str, sell_reason: str,
                           current_time: datetime, **kwargs) -> bool:
        
        # We MUST block Freqtrade's internal price-based exits (stop_loss, trailing_stop_loss, roi).
        # Since we use native BingX STOP_MARKET and LIMIT orders, the exchange will close the position.
        # If Freqtrade tries to exit at the same time, it causes 101290 Reduce Only errors (race condition).
        # By blocking them here, Freqtrade leaves the trade open until our Reconcile loop 
        # detects the missing position and cleanly finalizes the statistics.
        if sell_reason in ['stop_loss', 'trailing_stop_loss', 'roi']:
            # logger.info(f"BLOCKING NATIVE EXIT: {pair} reason: {sell_reason}. Exchange manages SL/TP.")
            return False
            
        logger.info(f"TRADE EXITING: {pair} reason: {sell_reason}")
        return True
