# source: https://raw.githubusercontent.com/aabudeev/freqtrade/d6e027ce891588012f4ccba935aec2bb910ba3d9/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__20260507_062023(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': 10,
        'exit': 525600, # 365 days — TP orders must not be auto-cancelled
        '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           # Effectively disabled
    
    # TRAILING STOP DISABLED
    trailing_stop = False
    use_custom_stoploss = 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 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
            
            # Ensure session is clean to prevent PendingRollbackError loop
            Trade.session.rollback()
            
            # --- 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()

            # 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}")

            for trade in open_trades:
                symbol_api = trade.pair.replace("/", "-").split(":")[0]
                
                # --- HEAL ORPHAN TRADES ---
                # Check if this trade has an active position on exchange
                is_orphan = True
                for p in all_positions:
                    p_symbol = p.get('symbol', '').replace(":", "-")
                    p_contracts = float(p.get('contracts', 0) or p.get('size', 0) or 0)
                    if symbol_api in p_symbol and p_contracts != 0:
                        is_orphan = False
                        break

                # Fetch orders using CCXT unified and raw methods
                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
                    
                    # --- PASSIVE MONITORING ---
                    # Only log a warning if position is missing, but NEVER close the trade in DB.
                    if is_orphan and not all_exchange_orders:
                        logger.warning(f"BINGX RECONCILE: Trade {trade.id} ({trade.pair}) appears to have NO position and NO orders on exchange! Check manually.")
                        continue  # Skip TP/SL placement for ghost trades
                    elif is_orphan and all_exchange_orders:
                        logger.debug(f"BINGX RECONCILE: Trade {trade.id} ({trade.pair}) has no position but has orders. Keeping open.")

                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)
                                if tp_target and abs(o_price - float(tp_target)) / float(tp_target) < 0.005:
                                    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})")
                                    break
                        
                        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) ---
                has_sl = any(o.ft_order_side == 'stoploss' and o.ft_is_open for o in trade.orders)
                if not has_sl:
                    try:
                        sl_order_id = None
                        sl_price = trade.stop_loss
                        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)
                                if sl_price and abs(o_stop_price - float(sl_price)) / float(sl_price) < 0.005:
                                    sl_order_id = str(o.get('id') or o.get('orderId'))
                                    logger.info(f"BINGX RECONCILE: Recognized existing SL {sl_order_id} for {trade.pair} (Type: {o_type})")
                                    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:
                            # CRITICAL: If we see ANY non-limit order, maybe it's our SL but we didn't match it perfectly?
                            # We count them and if any exist, we SKIP placing a new one to be safe.
                            non_limit_orders = [o for o in all_exchange_orders if str(o.get('type', o.get('orderType', ''))).upper() not in ['LIMIT', '']]
                            
                            if non_limit_orders:
                                logger.debug(f"BINGX RECONCILE: Found {len(non_limit_orders)} potential SL/Trigger orders for {trade.pair}. Skipping to avoid duplicates.")
                                has_sl = True
                            
                            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]
                                pos_side = "SHORT" if trade.is_short else "LONG"
                                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 _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:
        logger.info(f"TRADE EXITING: {pair} reason: {sell_reason}")
        return True
