# --- START OF FILE strategy.py ---

import sys, time, logging, os
from datetime import datetime, timezone, timedelta, time as dt_time
import numpy as np
import pickle
from decimal import Decimal, ROUND_FLOOR, ROUND_CEILING, InvalidOperation
import ccxt
import hashlib
from collections import deque

try:
    from utils import safe_decimal, format_price, format_time_difference, get_utc_now
    from telegram_service import send_telegram_message
except ImportError as e:
    print(f"CRITICAL ERROR in strategy.py: Failed to import helper modules: {e}")
    logging.basicConfig(level=logging.INFO)
    logging.critical(f"CRITICAL ERROR in strategy.py: Failed to import helper modules: {e}", exc_info=True)
    raise

CONFIG_STRATEGY_NAME = "DefaultStrategy_NameNotImported"
CONFIG_TOTAL_REQUIRED_SAMPLES = 1000
CONFIG_PROGRESS_REPORT_INTERVAL = 60
CONFIG_FEATURE_FETCH_INTERVAL_SECONDS = 1

try:
    from config import (
        STRATEGY_NAME as _STRATEGY_NAME_CFG,
        TOTAL_REQUIRED_CANDLES as _TOTAL_REQUIRED_CANDLES_CFG,
        PROGRESS_REPORT_INTERVAL as _PROGRESS_REPORT_INTERVAL_CFG,
        FEATURE_FETCH_INTERVAL_SECONDS as _FEATURE_FETCH_INTERVAL_SECONDS_CFG
    )
    CONFIG_STRATEGY_NAME = _STRATEGY_NAME_CFG
    CONFIG_TOTAL_REQUIRED_SAMPLES = _TOTAL_REQUIRED_CANDLES_CFG
    CONFIG_PROGRESS_REPORT_INTERVAL = _PROGRESS_REPORT_INTERVAL_CFG
    CONFIG_FEATURE_FETCH_INTERVAL_SECONDS = _FEATURE_FETCH_INTERVAL_SECONDS_CFG
    print("✓ Strategy.py: Module-level constants updated from config.py.")
except ImportError:
    print("⚠️ Strategy.py: Could not import some module-level constants from config.py. Using internal fallbacks.")
except Exception as e_cfg_strat_mod:
    print(f"⚠️ Strategy.py: Error importing module-level constants from config.py: {e_cfg_strat_mod}. Using internal fallbacks.")

collected_samples = 0
last_progress_report_time_tg = 0
model_ready = False
last_trade_factors_report_tg = ""

def _safe_send_tg_module_level(message):
    try:
        if 'send_telegram_message' in globals() and callable(send_telegram_message):
             send_telegram_message(message)
        else:
             print(f"INFO (from _safe_send_tg_module_level): Telegram function 'send_telegram_message' not available. Msg: {message[:70]}...")
             logging.getLogger(__name__).info(f"Telegram function not callable or not in global scope for module-level send. Msg: {message[:70]}...")
    except Exception as e:
        local_logger = logging.getLogger(__name__) if __name__ != "__main__" else logging.getLogger("strategy_module_helper")
        local_logger.warning(f"Failed to send Telegram message from strategy module's helper: {type(e).__name__} - {e}")

def update_progress_counter(new_sample_collected=False):
    global collected_samples, last_progress_report_time_tg, model_ready

    if new_sample_collected and collected_samples < CONFIG_TOTAL_REQUIRED_SAMPLES:
        collected_samples += 1

    current_time = time.time()
    if collected_samples >= CONFIG_TOTAL_REQUIRED_SAMPLES:
        return

    if current_time - last_progress_report_time_tg > CONFIG_PROGRESS_REPORT_INTERVAL:
        progress_pct = min(100.0, (collected_samples / CONFIG_TOTAL_REQUIRED_SAMPLES) * 100.0) if CONFIG_TOTAL_REQUIRED_SAMPLES > 0 else 0.0
        bar_length = 20
        filled_length = int(bar_length * progress_pct / 100)
        bar = '█' * filled_length + '░' * (bar_length - filled_length)

        remaining_samples_count = max(0, CONFIG_TOTAL_REQUIRED_SAMPLES - collected_samples)
        time_remaining_str = "Calculating..."

        if remaining_samples_count > 0 and collected_samples > 5 and CONFIG_FEATURE_FETCH_INTERVAL_SECONDS > 0:
            est_seconds_remaining = remaining_samples_count * CONFIG_FEATURE_FETCH_INTERVAL_SECONDS
            est_minutes = est_seconds_remaining // 60
            if est_minutes > 120: time_remaining_str = f"~{est_minutes // 60:.1f} hours"
            elif est_minutes > 0 : time_remaining_str = f"~{int(est_minutes)} minutes"
            else: time_remaining_str = f"~{int(est_seconds_remaining)} seconds"
        elif remaining_samples_count <= 0:
            time_remaining_str = "Complete!"
        elif collected_samples <= 5:
            time_remaining_str = "Calculating (need more samples)..."

        tg_enabled_check = False
        try:
            from config import ENABLE_TG as tg_check_flag
            tg_enabled_check = tg_check_flag
        except ImportError:
             print("Warning: Could not import ENABLE_TG from config for update_progress_counter.")
             logging.getLogger(__name__).warning("Could not import ENABLE_TG from config for update_progress_counter.")

        if tg_enabled_check:
            _safe_send_tg_module_level(
                f"📊 *Data Collection Progress*\n"
                f"Collected: *{collected_samples}* / *{CONFIG_TOTAL_REQUIRED_SAMPLES}* samples\n"
                f"Progress: *{progress_pct:.1f}%*\n`{bar}` {progress_pct:.1f}%\n"
                f"Est. time remaining: *{time_remaining_str}*\n"
                f"Model status: *{'Ready' if model_ready else 'Collecting Data'}*"
            )
        last_progress_report_time_tg = current_time

def start_periodic_status():
    global model_ready
    try:
        tg_enabled_check = False
        try:
            from config import ENABLE_TG as tg_check_flag
            tg_enabled_check = tg_check_flag
        except ImportError:
            print("Warning: Could not import ENABLE_TG from config for start_periodic_status.")
            logging.getLogger(__name__).warning("Could not import ENABLE_TG from config for start_periodic_status.")

        if tg_enabled_check:
            _safe_send_tg_module_level(
                f"🔄 *Strategy '{CONFIG_STRATEGY_NAME}' Initializing Components*\n"
                f"Target Samples for initial train: *{CONFIG_TOTAL_REQUIRED_SAMPLES}*\n"
                f"Current Model Status (pre-check): *{'Assumed Ready (if loaded)' if model_ready else 'Assumed Not Ready'}*"
            )
    except Exception as e:
        print(f"ERROR in start_periodic_status: {e}")
        logging.getLogger(__name__).error(f"ERROR in start_periodic_status: {e}", exc_info=True)


class Strategy:
    def __init__(self, exchange_api, config_module, logger, send_telegram_message_func):
        self.exchange_api = exchange_api
        self.config_module = config_module
        self.logger = logger
        self.send_telegram = send_telegram_message_func if callable(send_telegram_message_func) else self._dummy_send_tg

        self.STRATEGY_NAME = getattr(self.config_module, 'STRATEGY_NAME', 'DefaultStrategy_NameMissing')
        self.logger.info(f"Initializing Strategy: {self.STRATEGY_NAME}")

        self.history_train_samples = getattr(self.config_module, 'TOTAL_REQUIRED_CANDLES', 1000)
        self.future_price_wait_seconds = getattr(self.config_module, 'FUTURE_PRICE_WAIT_SECONDS', 15)
        self.min_confidence = getattr(self.config_module, 'MIN_CONFIDENCE', 0.65)
        self.learning_rate = getattr(self.config_module, 'LEARNING_RATE', 0.03)
        self.num_epochs = getattr(self.config_module, 'NUM_EPOCHS', 150)
        self.save_training_data_flag = getattr(self.config_module, 'SAVE_TRAINING_DATA', True)
        self.model_retrain_days_limit = getattr(self.config_module, 'MODEL_RETRAIN_DAYS', 3.0)
        self.min_samples_for_train = getattr(self.config_module, 'MIN_SAMPLES_FOR_TRAIN', 75)
        self.train_early_stop_patience = getattr(self.config_module, 'TRAIN_EARLY_STOP_PATIENCE', 25)
        self.train_min_loss_improvement = getattr(self.config_module, 'TRAIN_MIN_LOSS_IMPROVEMENT', 1e-5)
        self.train_log_epoch_interval = getattr(self.config_module, 'TRAIN_LOG_EPOCH_INTERVAL', 20)

        self.base_sl_pct = safe_decimal(getattr(self.config_module, 'BASE_SL_PCT', '0.0025'))
        self.min_take_profit_pct = safe_decimal(getattr(self.config_module, 'MIN_TP_PCT', '0.0040'))
        self.risk_pct_per_trade = safe_decimal(getattr(self.config_module, 'RISK_PCT_PER_TRADE', '0.01'))
        self.min_tp_to_sl_ratio = safe_decimal(getattr(self.config_module, 'MIN_TP_TO_SL_RATIO', '2.0'))
        self.estimated_fee_pct = safe_decimal(getattr(self.config_module, 'ESTIMATED_TRADING_FEE_PCT', '0.0006'))
        self.sl_tp_safety_gap_pct = safe_decimal(getattr(self.config_module, 'SL_TP_SAFETY_GAP_PCT', '0.0005'))

        self.imb_threshold = safe_decimal(getattr(self.config_module, 'IMB_THRESHOLD', '0.10'))
        self.imb2_threshold = safe_decimal(getattr(self.config_module, 'IMB2_THRESHOLD', '0.05'))
        self.vol_threshold = safe_decimal(getattr(self.config_module, 'VOL_THRESHOLD', '0.5'))
        self.spread_limit_pct = safe_decimal(getattr(self.config_module, 'SPREAD_LIMIT_PCT', '0.0015'))

        self.reversal_stability_ticks = getattr(self.config_module, 'REVERSAL_STABILITY_TICKS', 2)
        self.profit_lock_conf_drop_threshold = getattr(self.config_module, 'PROFIT_LOCK_CONF_DROP_THRESHOLD', 0.45)
        self.profit_lock_min_pnl_pct = safe_decimal(getattr(self.config_module, 'PROFIT_LOCK_MIN_PNL_PCT', '0.0015'))
        self.exit_on_reversal_signal_flag = getattr(self.config_module, 'EXIT_ON_REVERSAL_SIGNAL', True)
        self.lock_profit_on_conf_drop_flag = getattr(self.config_module, 'LOCK_PROFIT_ON_CONF_DROP', True)
        self.active_trade_monitor_interval_seconds = getattr(self.config_module, 'ACTIVE_TRADE_MONITOR_INTERVAL_SECONDS', 7)
        self.new_trade_grace_period_seconds = getattr(self.config_module, 'NEW_TRADE_GRACE_PERIOD_SECONDS', 20)
        self.short_trade_duration_threshold_seconds = getattr(self.config_module, 'SHORT_TRADE_DURATION_THRESHOLD_SECONDS', 15)

        self.REQUIRE_TREND_MATCH = getattr(self.config_module, 'REQUIRE_TREND_MATCH', False)
        self.REQUIRE_PRICE_POSITION_MATCH = getattr(self.config_module, 'REQUIRE_PRICE_POSITION_MATCH', False)
        self.REQUIRE_SIGNAL_STABILITY = getattr(self.config_module, 'REQUIRE_SIGNAL_STABILITY', True)
        self.SIGNAL_STABILITY_COUNT = getattr(self.config_module, 'SIGNAL_STABILITY_COUNT', 2)

        self.cooldown_minutes = getattr(self.config_module, 'COOLDOWN_PERIOD_MINUTES', 5)
        self.status_report_interval = getattr(self.config_module, 'STATUS_REPORT_INTERVAL', 15 * 60)
        self.csv_log_file = getattr(self.config_module, 'CSV_LOG_FILE', "trade_log_default.csv")
        self.model_save_file = getattr(self.config_module, 'MODEL_SAVE_FILE', "model_params_default.pkl")
        self.training_data_file = getattr(self.config_module, 'TRAINING_DATA_FILE', "training_data_default.pkl")
        self.feature_fetch_interval = getattr(self.config_module, 'FEATURE_FETCH_INTERVAL_SECONDS', 1.0)
        self.ml_trend_window_size = getattr(self.config_module, 'ML_TREND_WINDOW', 5)

        self.use_stop_loss_config = getattr(self.config_module, 'USE_STOP_LOSS', True)
        self.use_take_profit_config = getattr(self.config_module, 'USE_TAKE_PROFIT', True)
        self.min_fill_ratio_market_order = safe_decimal(getattr(self.config_module, 'MIN_FILL_RATIO_MARKET_ORDER', '0.90'))
        self.max_fill_fetch_attempts = getattr(self.config_module, 'MAX_FILL_FETCH_ATTEMPTS', 6)
        self.fill_fetch_delay_seconds = getattr(self.config_module, 'FILL_FETCH_DELAY_SECONDS', 0.6)
        self.cancel_orders_wait_sec = getattr(self.config_module, 'CANCEL_ORDERS_WAIT_SEC', 0.7)
        self.post_trade_entry_delay_seconds = getattr(self.config_module, 'POST_TRADE_ENTRY_DELAY_SECONDS', 3.0)
        self.ghost_pos_check_interval_min = getattr(self.config_module, 'GHOST_POSITION_CHECK_MINUTES', 2)
        self.cooldown_after_ghost_min = getattr(self.config_module, 'COOLDOWN_AFTER_GHOST_CLOSE_MIN', 3)
        self.model_retrain_check_interval_min = getattr(self.config_module, 'MODEL_RETRAIN_CHECK_INTERVAL_MINUTES', 30)
        self.model_retry_train_interval_min = getattr(self.config_module, 'MODEL_RETRY_TRAIN_INTERVAL_MINUTES', 60)

        self.enable_time_filter = getattr(self.config_module, 'ENABLE_TIME_FILTER', False)
        self.trading_allowed_hours_utc = getattr(self.config_module, 'TRADING_ALLOWED_HOURS_UTC', [])
        self._parsed_trading_sessions = []
        if self.enable_time_filter and self.trading_allowed_hours_utc:
            try:
                for i in range(0, len(self.trading_allowed_hours_utc), 2):
                    start_tp = self.trading_allowed_hours_utc[i]
                    end_tp = self.trading_allowed_hours_utc[i+1]
                    self._parsed_trading_sessions.append(
                        (dt_time(start_tp[0], start_tp[1]), dt_time(end_tp[0], end_tp[1]))
                    )
                self.logger.info(f"Time filter enabled. Parsed trading sessions (UTC): {self._parsed_trading_sessions}")
            except Exception as e_time_parse:
                self.logger.error(f"Error parsing TRADING_ALLOWED_HOURS_UTC: {e_time_parse}. Disabling time filter.")
                self.enable_time_filter = False
        elif self.enable_time_filter and not self.trading_allowed_hours_utc:
            self.logger.warning("Time filter is ENABLED, but TRADING_ALLOWED_HOURS_UTC is empty. This means NO TRADING will occur.")

        if not hasattr(self.config_module, 'REQUIRE_TREND_MATCH'):
            self.logger.warning(f"Config 'REQUIRE_TREND_MATCH' not found, defaulting to {self.REQUIRE_TREND_MATCH}.")
        if not hasattr(self.config_module, 'REQUIRE_PRICE_POSITION_MATCH'):
            self.logger.warning(f"Config 'REQUIRE_PRICE_POSITION_MATCH' not found, defaulting to {self.REQUIRE_PRICE_POSITION_MATCH}.")
        if not hasattr(self.config_module, 'REQUIRE_SIGNAL_STABILITY'):
            self.logger.warning(f"Config 'REQUIRE_SIGNAL_STABILITY' not found, defaulting to {self.REQUIRE_SIGNAL_STABILITY}.")
        if not hasattr(self.config_module, 'SIGNAL_STABILITY_COUNT'):
            self.logger.warning(f"Config 'SIGNAL_STABILITY_COUNT' not found, defaulting to {self.SIGNAL_STABILITY_COUNT}.")

        self.logger.info(f"Filter Settings: TrendMatch={self.REQUIRE_TREND_MATCH}, PricePosMatch={self.REQUIRE_PRICE_POSITION_MATCH}, SigStability={self.REQUIRE_SIGNAL_STABILITY} (Count:{self.SIGNAL_STABILITY_COUNT}), TimeFilter={self.enable_time_filter}")

        self.model_w = None; self.model_b = None; self.feature_mean = None; self.feature_std = None
        self.model_train_time = None; self._last_train_accuracy = None
        self.model_loaded_on_startup = False
        self.last_prices = []; self.trade_history = []
        self.is_training = False; self._is_closing_trade = False
        self._closing_trade_eoid_marker = None

        self._last_status_msg_hash = ''; self._last_ghost_check_time = None; self._last_retrain_check_time = None
        self._last_status_report_sent_time = 0

        self.active_trade = self._reset_active_trade()
        self.monitor_state = {'last_signal':None,'last_confidence':0.0,'signal_stable_count':0,'last_check_time':None}

        self._last_balance_warning_time = 0
        self._last_cooldown_warning_time = 0
        self._last_model_not_ready_warning_time = 0
        self._last_time_filter_warning_time = 0
        self._last_tg_factors_report_time_no_trade = 0
        self._last_spread_filter_warning_time = 0

        self.recent_ml_p_up = deque(maxlen=self.ml_trend_window_size)

        self._position_confirmed = False
        self._position_confirm_time = None

        self._market_info = None
        try:
            self._market_info = self.exchange_api.get_market_details()
            if self._market_info: self.logger.info("Market details successfully retrieved during Strategy init.")
            else: raise ValueError("get_market_details() returned None from connector.")
        except Exception as e_market_info:
            self.logger.critical(f"CRITICAL: Failed to get market info during Strategy init: {type(e_market_info).__name__} - {e_market_info}", exc_info=True)
            raise ValueError(f"Failed to retrieve market info from connector: {e_market_info}")

        self._setup_csv_logger()
        self.logger.info("Attempting to load model on strategy initialization...")
        self.model_loaded_on_startup = self.load_model()

        global model_ready
        if self.model_loaded_on_startup and self.model_w is not None:
            if self.model_train_time:
                model_age_days = (get_utc_now() - self.model_train_time).total_seconds() / (24 * 3600)
                if model_age_days <= self.model_retrain_days_limit:
                    model_ready = True
                    self.logger.info(f"Model loaded & fresh (Age:{model_age_days:.1f}d <= {self.model_retrain_days_limit}d). Model is ready.")
                else:
                    model_ready = False
                    self.logger.info(f"Model loaded but is STALE (Age:{model_age_days:.1f}d > {self.model_retrain_days_limit}d). Needs retraining.")
            else:
                model_ready = False
                self.logger.warning("Model parameters loaded, but train_time is missing. Model considered not ready.")
        elif self.model_w is None and self.model_loaded_on_startup:
             model_ready = False
             self.logger.warning("Model file was found but parameters (e.g. weights) were not loaded correctly. Model not ready.")
        else:
            model_ready = False
            self.logger.info("No valid pre-existing model found or loaded. Data collection/training may be needed.")

    def _dummy_send_tg(self, message):
        self.logger.debug(f"Telegram disabled/dummy send. Would send: {message[:70]}...")
        pass

    def _reset_active_trade(self):
        self._position_confirmed = False
        self._position_confirm_time = None
        return {
            'side': None, 'entry_price': None, 'size': None, 'timestamp': None,
            'entry_order_id': None, 'sl_id': None, 'tp_id': None,
            'confidence': None, 'sl_price': None, 'tp_price': None,
            'sl_pct': None, 'tp_pct': None, 'entry_price_estimate': None,
            'bot_sltp_recalculated': False,
            'cooldown_until': get_utc_now() - timedelta(seconds=1)
        }

    def _setup_csv_logger(self):
        header = "time,side,entry,exit,size,pnl_pct,pnl_usd,reason,confidence,duration_secs,sl_price,tp_price,slippage_pct\n"
        try:
            if not self.csv_log_file:
                self.logger.warning("CSV Log file path not configured. Trade logging to CSV disabled.")
                return
            log_dir = os.path.dirname(self.csv_log_file)
            if log_dir and not os.path.exists(log_dir):
                os.makedirs(log_dir)
                self.logger.info(f"Created trade log directory: {log_dir}")

            if not os.path.exists(self.csv_log_file):
                with open(self.csv_log_file, 'w', newline='') as f:
                    f.write(header)
                self.logger.info(f"Created trade log CSV file: {self.csv_log_file}")
            else:
                self.logger.info(f"Trade log CSV file already exists: {self.csv_log_file}")
        except Exception as e:
            self.logger.error(f"Could not create or access trade log '{getattr(self, 'csv_log_file', 'N/A')}': {e}", exc_info=True)

    def _log_trade_to_csv(self, entry_time, side, entry_price_actual, exit_price, size,
                          pnl_pct, pnl_usd, reason, confidence, sl_price, tp_price, entry_price_estimate=None):
        if not self.csv_log_file: return
        try:
            now_utc = get_utc_now()
            duration_secs = (now_utc - entry_time).total_seconds() if isinstance(entry_time, datetime) else 0

            conf_val = confidence if confidence is not None else 0.0
            sl_p_val = sl_price if sl_price is not None else Decimal('0')
            tp_p_val = tp_price if tp_price is not None else Decimal('0')

            entry_price_d = safe_decimal(entry_price_actual)
            exit_price_d = safe_decimal(exit_price)
            size_d = safe_decimal(size)
            sl_price_d = safe_decimal(sl_p_val)
            tp_price_d = safe_decimal(tp_p_val)
            pnl_pct_d = safe_decimal(pnl_pct)
            pnl_usd_d = safe_decimal(pnl_usd)
            entry_price_est_d = safe_decimal(entry_price_estimate) if entry_price_estimate else entry_price_d

            slippage_pct_d = Decimal('0')
            if entry_price_est_d > 0 and entry_price_d > 0:
                if side == 'buy':
                    slippage_pct_d = (entry_price_d - entry_price_est_d) / entry_price_est_d
                else:
                    slippage_pct_d = (entry_price_est_d - entry_price_d) / entry_price_est_d

            entry_price_s = f"{entry_price_d:.8f}"; exit_price_s = f"{exit_price_d:.8f}"
            size_s = f"{size_d:.8f}"; sl_price_s = f"{sl_price_d:.8f}"; tp_price_s = f"{tp_price_d:.8f}"
            pnl_pct_s = f"{pnl_pct_d:.8f}"; pnl_usd_s = f"{pnl_usd_d:.4f}"
            slippage_pct_s = f"{slippage_pct_d:.8f}"

            reason_str = str(reason).replace('"',"''").replace(',',' ')
            fmt_reason = f'"{reason_str}"'

            log_entry = (f"{now_utc.strftime('%Y-%m-%d %H:%M:%S')},{side},{entry_price_s},{exit_price_s},"
                         f"{size_s},{pnl_pct_s},{pnl_usd_s},{fmt_reason},{conf_val:.4f},"
                         f"{duration_secs:.0f},{sl_price_s},{tp_price_s},{slippage_pct_s}\n")

            with open(self.csv_log_file, 'a', newline='') as f:
                f.write(log_entry)

            self.trade_history.append({
                'time': now_utc.isoformat(), 'side': side, 'entry': entry_price_d, 'exit': exit_price_d,
                'size': size_d, 'pnl_pct': pnl_pct_d, 'pnl_usd': pnl_usd_d, 'reason': reason,
                'confidence': conf_val, 'duration_secs': int(duration_secs),
                'sl_price': sl_price_d, 'tp_price': tp_price_d,
                'entry_estimate': entry_price_est_d, 'slippage_pct': slippage_pct_d
            })
            self.logger.info(f"Trade logged to CSV: {side} {size_s} {getattr(self.config_module, 'SYMBOL_BYBIT', 'N/A')} PNL {pnl_pct_d:.2%}, Slippage {slippage_pct_d:.3%}")
        except Exception as e:
            self.logger.error(f"Error logging trade to CSV: {e}",exc_info=True)

    def load_model(self):
        global model_ready
        if not os.path.exists(self.model_save_file):
            self.logger.info(f"Model file {self.model_save_file} not found. No model loaded.")
            self._reset_model_state()
            model_ready = False
            return False

        self.logger.info(f"Attempting to load model from: {self.model_save_file}")
        try:
            with open(self.model_save_file, 'rb') as f:
                model_data = pickle.load(f)

            required_keys = ['w', 'b', 'mean', 'std', 'train_time_utc']
            if not isinstance(model_data, dict) or not all(k in model_data for k in required_keys):
                self.logger.warning(f"Model file {self.model_save_file} missing required keys or incorrect format. Not loaded.")
                self._reset_model_state(); model_ready = False; return False

            loaded_w, loaded_b, loaded_mean, loaded_std = model_data['w'], model_data['b'], model_data['mean'], model_data['std']
            train_time_iso = model_data['train_time_utc']
            loaded_accuracy = model_data.get('train_accuracy')

            if not (isinstance(loaded_w, np.ndarray) and isinstance(loaded_b, (float, np.floating)) and \
                    isinstance(loaded_mean, np.ndarray) and isinstance(loaded_std, np.ndarray)):
               self.logger.error(f"Model data in {self.model_save_file} has invalid types for core components. Not loaded.")
               self._reset_model_state(); model_ready = False; return False

            if np.isnan(loaded_w).any() or np.isinf(loaded_w).any() or \
               np.isnan(loaded_b) or np.isinf(loaded_b) or \
               np.isnan(loaded_mean).any() or np.isinf(loaded_mean).any() or \
               np.isnan(loaded_std).any() or np.isinf(loaded_std).any():
                self.logger.error(f"Model data in {self.model_save_file} contains NaN or Infinity. Not loaded.")
                self._reset_model_state(); model_ready = False; return False

            try:
                loaded_train_time = datetime.fromisoformat(train_time_iso).replace(tzinfo=timezone.utc)
            except (ValueError, TypeError):
                self.logger.error(f"Invalid train_time_utc format ('{train_time_iso}') in model file. Not loaded.")
                self._reset_model_state(); model_ready = False; return False

            self.model_w = loaded_w; self.model_b = loaded_b
            self.feature_mean = loaded_mean; self.feature_std = loaded_std
            self.model_train_time = loaded_train_time
            self._last_train_accuracy = loaded_accuracy

            age_days = (get_utc_now() - loaded_train_time).total_seconds() / (24 * 3600)
            accuracy_str = f"{loaded_accuracy:.3f}" if isinstance(loaded_accuracy, (float, int)) else "N/A"
            samples_str = model_data.get('train_samples', 'N/A')

            if age_days > self.model_retrain_days_limit:
                self.logger.info(f"Model loaded from {self.model_save_file} but is STALE (Age:{age_days:.1f}d > limit {self.model_retrain_days_limit}d, Acc:{accuracy_str}, Samples:{samples_str}). Needs retraining.")
                model_ready = False
                return True
            else:
                self.logger.info(f"✅ Model loaded FRESH: {self.model_save_file} (Age:{age_days:.1f}d, Acc:{accuracy_str}, Samples:{samples_str})")
                model_ready = True
                return True

        except (pickle.UnpicklingError, EOFError) as e_pickle:
            self.logger.error(f"Error unpickling model file {self.model_save_file}: {e_pickle}. File might be corrupted.", exc_info=False)
            self._reset_model_state(); model_ready = False; return False
        except AttributeError as e_attr:
             if 'np.float_' in str(e_attr):
                 self.logger.error(f"NumPy version mismatch loading model: {e_attr}. Model may have been saved with an older/different NumPy version. Cannot load.", exc_info=False)
             else:
                 self.logger.error(f"AttributeError while loading model: {e_attr}", exc_info=True)
             self._reset_model_state(); model_ready = False; return False
        except Exception as e_load:
            self.logger.error(f"Unexpected error loading model {self.model_save_file}: {type(e_load).__name__} - {e_load}", exc_info=True)
            self._reset_model_state(); model_ready = False; return False

    def _reset_model_state(self):
        global model_ready
        self.model_w = None; self.model_b = None
        self.feature_mean = None; self.feature_std = None
        self.model_train_time = None; self._last_train_accuracy = None
        model_ready = False
        self.logger.info("Model state has been reset. Model is NOT ready.")

    def train_model(self):
        global model_ready
        if self.is_training:
            self.logger.info("Model training already in progress. Skipping current request.")
            return

        self.is_training = True
        model_ready = False
        self.logger.info("--- Starting Model Training Sequence ---")
        if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
            self.send_telegram("⏳ Model training/data collection process started...")

        self._last_train_accuracy = None
        training_attempt_successful = False

        try:
            if self.model_w is not None and self.model_train_time is not None:
                age_days = (get_utc_now() - self.model_train_time).total_seconds() / (24 * 3600)
                if age_days < self.model_retrain_days_limit:
                    self.logger.info(f"Current model is fresh enough (age: {age_days:.2f}d <= {self.model_retrain_days_limit}d). Skipping training cycle.")
                    model_ready = True
                    training_attempt_successful = True
                    self.is_training = False
                    return

            X, y = self._collect_training_data()

            if X is None or y is None or len(X) < self.min_samples_for_train:
                self.logger.warning(f"Insufficient data for training ({len(X) if X is not None else 0} samples < {self.min_samples_for_train} required). Training skipped.")
                if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                    self.send_telegram(f"⚠️ Training skipped: Insufficient data ({len(X) if X is not None else 0}/{self.min_samples_for_train}).")
                self.is_training = False
                return

            feature_mean_new = np.mean(X, axis=0)
            feature_std_new = np.std(X, axis=0)
            feature_std_new[feature_std_new < 1e-8] = 1e-8
            X_normalized = (X - feature_mean_new) / feature_std_new

            if np.isnan(X_normalized).any() or np.isinf(X_normalized).any():
                self.logger.error("NaN/Inf found in normalized training data (X_normalized). Aborting training.")
                if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                    self.send_telegram("❌ Training aborted: Invalid data (NaN/Inf) encountered in normalization.")
                self.is_training = False
                return

            num_samples, num_features = X.shape

            current_weights = np.zeros(num_features)
            current_bias = 0.0
            if self.model_w is not None and self.model_b is not None and self.model_w.shape == (num_features,):
                current_weights = self.model_w.copy()
                current_bias = self.model_b
                self.logger.info("Initializing training with previous model weights.")
            else:
                self.logger.info("Initializing training with zero weights (no compatible previous model or first train).")

            self.logger.info(f"Training logistic regression model: Samples={num_samples}, Features={num_features}. Epochs={self.num_epochs}, LR={self.learning_rate}")

            best_loss = float('inf')
            epochs_no_improve = 0
            best_w, best_b = current_weights.copy(), current_bias

            for epoch in range(self.num_epochs):
                z = np.dot(X_normalized, current_weights) + current_bias
                z_clipped = np.clip(z, -500, 500)
                predictions = 1 / (1 + np.exp(-z_clipped))

                epsilon = 1e-9
                predictions_clipped = np.clip(predictions, epsilon, 1 - epsilon)
                loss = -np.mean(y * np.log(predictions_clipped) + (1 - y) * np.log(1 - predictions_clipped))

                error = predictions - y
                dw = (np.dot(X_normalized.T, error)) / num_samples
                db = np.mean(error)

                if np.isnan(loss) or np.isinf(loss) or \
                   np.isnan(dw).any() or np.isinf(dw).any() or \
                   np.isnan(db) or np.isinf(db):
                    self.logger.error(f"NaN/Inf detected during training epoch {epoch+1}. Loss:{loss}. Reverting to best known weights and stopping.")
                    current_weights, current_bias = best_w, best_b
                    break

                current_weights -= self.learning_rate * dw
                current_bias -= self.learning_rate * db

                if loss < best_loss - self.train_min_loss_improvement:
                    best_loss = loss
                    best_w, best_b = current_weights.copy(), current_bias
                    epochs_no_improve = 0
                else:
                    epochs_no_improve += 1

                if epochs_no_improve >= self.train_early_stop_patience:
                    self.logger.info(f"Early stopping at epoch {epoch+1} due to no improvement for {self.train_early_stop_patience} epochs. Best Loss: {best_loss:.5f}")
                    current_weights, current_bias = best_w, best_b
                    break

                if (epoch + 1) % self.train_log_epoch_interval == 0 or epoch == self.num_epochs - 1:
                    self.logger.debug(f"Train Epoch {epoch+1}/{self.num_epochs}, Current Loss:{loss:.5f}, Best Loss:{best_loss:.5f}")

            final_weights, final_bias = best_w, best_b

            z_final_eval = np.clip(np.dot(X_normalized, final_weights) + final_bias, -500, 500)
            p_final_eval = 1 / (1 + np.exp(-z_final_eval))
            final_predictions_binary = (p_final_eval > 0.5).astype(int)
            accuracy = np.mean(final_predictions_binary == y)
            self._last_train_accuracy = accuracy

            self.logger.info(f"Training finished. Best Loss achieved: {best_loss:.5f}, Final Training Accuracy: {accuracy:.3f} on {num_samples} samples.")

            if not (np.isnan(final_weights).any() or np.isinf(final_weights).any() or \
                    np.isnan(final_bias) or np.isinf(final_bias)):
                self.model_w = final_weights
                self.model_b = final_bias
                self.feature_mean = feature_mean_new
                self.feature_std = feature_std_new
                self.model_train_time = get_utc_now()
                training_attempt_successful = True
                self.logger.info("✅ Model parameters successfully updated from training.")

                model_data_to_save = {
                    'w': self.model_w, 'b': self.model_b,
                    'mean': self.feature_mean, 'std': self.feature_std,
                    'train_time_utc': self.model_train_time.isoformat(),
                    'train_samples': num_samples,
                    'train_accuracy': accuracy
                }
                try:
                    with open(self.model_save_file, 'wb') as f:
                        pickle.dump(model_data_to_save, f)
                    self.logger.info(f"✅ Model saved to: {self.model_save_file}")
                    if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                        self.send_telegram(f"✅ *Model Retrained/Updated*\nAccuracy (Train): {accuracy:.3f}, Samples: {num_samples}\nModel is now active.")
                except Exception as e_save:
                    self.logger.error(f"Error saving model to {self.model_save_file}: {e_save}", exc_info=True)
                    if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                        self.send_telegram(f"⚠️ Model trained (Acc:{accuracy:.3f}) but FAILED to save: {e_save}")
            else:
                self.logger.error("Training resulted in NaN/Inf parameters. Model NOT updated from this training run.")
                if self.model_w is None:
                     self.logger.critical("🚨 CRITICAL: Initial model training failed (resulted in NaN/Inf). Bot cannot operate without a model.")
                if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                    self.send_telegram(f"❌ *Model Retrain FAILED*\nResulted in NaN/Inf parameters. Previous model (if any) remains active.")

        except Exception as e_train:
            self.logger.error(f"Unexpected error during model training process: {type(e_train).__name__} - {e_train}", exc_info=True)
            if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                self.send_telegram(f"❌ *Critical Model Retrain Error*: {e_train}")
        finally:
            self.is_training = False
            if training_attempt_successful and self.model_w is not None:
                model_ready = True
            elif self.model_w is not None and self.model_train_time is not None:
                 age_days = (get_utc_now() - self.model_train_time).total_seconds() / (24 * 3600)
                 model_ready = age_days < self.model_retrain_days_limit
            else:
                 model_ready = False

            self.logger.info(f"--- Model Training Sequence Finished (Overall Model Ready Status: {model_ready}) ---")
            if not model_ready and hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG :
                 last_acc_str = f"{self._last_train_accuracy:.3f}" if self._last_train_accuracy is not None else 'N/A (Prev or Failed)'
                 self.send_telegram(f"ℹ️ Training sequence finished. Model is currently NOT ready. Last known train Acc: {last_acc_str}")

    def _get_orderbook_features(self):
        if not self.exchange_api:
            self.logger.error("Cannot get orderbook features: Exchange API not available.")
            return None
        try:
            order_book = self.exchange_api.fetch_order_book(
                symbol_ccxt_param=getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'),
                limit_param=25
            )
            if not (order_book and order_book.get('bids') and order_book.get('asks') and \
                    len(order_book['bids']) > 0 and len(order_book['asks']) > 0):
                self.logger.warning("Orderbook data incomplete or missing when fetching features.")
                return None

            bids, asks = order_book['bids'], order_book['asks']
            if len(bids) < 5 or len(asks) < 5:
                self.logger.debug(f"Warning: Orderbook depth low for feature calc (Bids:{len(bids)}, Asks:{len(asks)}).")

            best_bid_price, best_bid_size = bids[0]
            best_ask_price, best_ask_size = asks[0]

            best_bid_price_d = safe_decimal(best_bid_price)
            best_bid_size_d = safe_decimal(best_bid_size)
            best_ask_price_d = safe_decimal(best_ask_price)
            best_ask_size_d = safe_decimal(best_ask_size)

            imb1 = (best_bid_size_d - best_ask_size_d) / (best_bid_size_d + best_ask_size_d) if (best_bid_size_d + best_ask_size_d) > 0 else Decimal(0)

            top_n_levels = 5
            vol_bids_top_n = sum(safe_decimal(s) for _, s in bids[:min(len(bids), top_n_levels)])
            vol_asks_top_n = sum(safe_decimal(s) for _, s in asks[:min(len(asks), top_n_levels)])
            imb2 = (vol_bids_top_n - vol_asks_top_n) / (vol_bids_top_n + vol_asks_top_n) if (vol_bids_top_n + vol_asks_top_n) > 0 else Decimal(0)

            deeper_start_level = top_n_levels
            deeper_end_level = 15
            vol_bids_deeper = sum(safe_decimal(s) for _, s in bids[deeper_start_level:min(len(bids), deeper_end_level)])
            vol_asks_deeper = sum(safe_decimal(s) for _, s in asks[deeper_start_level:min(len(asks), deeper_end_level)])
            imb3 = (vol_bids_deeper - vol_asks_deeper) / (vol_bids_deeper + vol_asks_deeper) if (vol_bids_deeper + vol_asks_deeper) > 0 else Decimal(0)

            spread = best_ask_price_d - best_bid_price_d
            if spread < 0:
                self.logger.warning(f"Crossed orderbook detected? Ask {best_ask_price_d} < Bid {best_bid_price_d}. Returning None for features.")
                return None

            mid_price = (best_ask_price_d + best_bid_price_d) / 2
            if mid_price <= 0:
                self.logger.warning(f"Invalid mid price {mid_price} from orderbook. Returning None for features.")
                return None

            spread_pct = spread / mid_price if mid_price > 0 else Decimal(0)

            num_bids_calc = min(len(bids), top_n_levels)
            num_asks_calc = min(len(asks), top_n_levels)

            avg_bid_size_top_n = vol_bids_top_n / num_bids_calc if num_bids_calc > 0 else Decimal(1)
            avg_ask_size_top_n = vol_asks_top_n / num_asks_calc if num_asks_calc > 0 else Decimal(1)

            max_bid_size_top_n = max(safe_decimal(s) for _, s in bids[:num_bids_calc]) if num_bids_calc > 0 else Decimal(0)
            max_ask_size_top_n = max(safe_decimal(s) for _, s in asks[:num_asks_calc]) if num_asks_calc > 0 else Decimal(0)

            bid_wall_ratio = max_bid_size_top_n / avg_bid_size_top_n if avg_bid_size_top_n > 0 else Decimal(1)
            ask_wall_ratio = max_ask_size_top_n / avg_ask_size_top_n if avg_ask_size_top_n > 0 else Decimal(1)

            bid_price_gaps = [safe_decimal(bids[i][0]) - safe_decimal(bids[i+1][0]) for i in range(min(len(bids)-1, top_n_levels-1))]
            ask_price_gaps = [safe_decimal(asks[i+1][0]) - safe_decimal(asks[i][0]) for i in range(min(len(asks)-1, top_n_levels-1))]
            avg_bid_gap = sum(bid_price_gaps) / len(bid_price_gaps) if bid_price_gaps else Decimal(0)
            avg_ask_gap = sum(ask_price_gaps) / len(ask_price_gaps) if ask_price_gaps else Decimal(0)

            self.last_prices.append(float(mid_price))
            max_historical_prices = 30
            if len(self.last_prices) > max_historical_prices:
                self.last_prices = self.last_prices[-max_historical_prices:]

            momentum_5_period = Decimal(0)
            short_term_momentum_window = 5
            if len(self.last_prices) >= short_term_momentum_window + 1:
                price_now = self.last_prices[-1]
                price_then = self.last_prices[-(short_term_momentum_window + 1)]
                momentum_5_period = safe_decimal((price_now - price_then) / price_then) if price_then != 0 else Decimal(0)

            volatility_10_period = Decimal(0)
            short_term_volatility_window = 10
            if len(self.last_prices) >= short_term_volatility_window:
                prices_for_vol_calc = self.last_prices[-short_term_volatility_window:]
                if prices_for_vol_calc and mid_price > 0:
                     volatility_10_period = safe_decimal(max(prices_for_vol_calc) - min(prices_for_vol_calc)) / mid_price
                else: volatility_10_period = Decimal(0)

            total_volume_top_n_levels = vol_bids_top_n + vol_asks_top_n

            features = {
                'imb1': float(imb1), 'imb2': float(imb2), 'imb3': float(imb3),
                'spread': float(spread), 'spread_pct': float(spread_pct),
                'mid': float(mid_price),
                'bid_wall': float(bid_wall_ratio), 'ask_wall': float(ask_wall_ratio),
                'avg_bid_gap': float(avg_bid_gap), 'avg_ask_gap': float(avg_ask_gap),
                'momentum_5': float(momentum_5_period),
                'volatility_10': float(volatility_10_period),
                'total_vol_top': float(total_volume_top_n_levels)
            }
            return features
        except TypeError as e_type:
            self.logger.error(f"TypeError in _get_orderbook_features: {e_type}", exc_info=True)
            return None
        except Exception as e:
            self.logger.error(f"Error calculating orderbook features: {type(e).__name__} - {e}", exc_info=True)
            return None

    def _label_future_move(self, start_mid_price_decimal):
        global collected_samples
        if not self.exchange_api:
            self.logger.error("Cannot label future move: Exchange API not available.")
            return None

        start_mid_price = safe_decimal(start_mid_price_decimal)
        if start_mid_price <= 0:
            self.logger.warning(f"_label_future_move called with invalid start_mid_price: {start_mid_price_decimal}.")
            return None

        try:
            wait_start_timestamp = time.time()
            label_progress_update_interval = 5
            last_label_progress_update_ts = time.time()

            self.logger.debug(f"Labeling: Waiting {self.future_price_wait_seconds}s for future price. Start price: {start_mid_price:.4f}...")

            while time.time() - wait_start_timestamp < self.future_price_wait_seconds:
                if time.time() - last_label_progress_update_ts > label_progress_update_interval:
                    if collected_samples < CONFIG_TOTAL_REQUIRED_SAMPLES:
                        update_progress_counter(new_sample_collected=False)
                    last_label_progress_update_ts = time.time()

                sleep_this_iteration = min(1.0, self.future_price_wait_seconds - (time.time() - wait_start_timestamp))
                if sleep_this_iteration > 0.05:
                    time.sleep(sleep_this_iteration)
                elif sleep_this_iteration > 0 :
                    time.sleep(0.01)
                else:
                    break

            future_ob = self.exchange_api.fetch_order_book(
                symbol_ccxt_param=getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'),
                limit_param=1
            )
            if not (future_ob and future_ob.get('bids') and future_ob.get('asks') and \
                    len(future_ob['bids']) > 0 and len(future_ob['asks']) > 0):
                self.logger.warning("Labeling: Could not fetch valid future order book after wait.")
                return None

            future_best_bid = safe_decimal(future_ob['bids'][0][0])
            future_best_ask = safe_decimal(future_ob['asks'][0][0])
            future_mid_price = (future_best_bid + future_best_ask) / 2

            if future_mid_price <= 0:
                self.logger.warning(f"Labeling: Invalid future mid price calculated: {future_mid_price}.")
                return None

            percentage_change = (future_mid_price - start_mid_price) / start_mid_price

            move_threshold_for_label = (self.min_take_profit_pct / Decimal('2.0')) \
                                       if self.min_take_profit_pct > 0 else Decimal('0.0005')

            label_value = 1 if percentage_change > move_threshold_for_label else 0

            self.logger.debug(f"Label Result: Start=${start_mid_price:.4f}, Future=${future_mid_price:.4f}, PctChg={percentage_change:+.4%}, Threshold={move_threshold_for_label:+.4%}, Label={label_value}")
            return label_value

        except Exception as e:
            self.logger.error(f"Error in _label_future_move: {type(e).__name__} - {e}", exc_info=True)
            return None

    def _collect_training_data(self):
        global collected_samples
        self.logger.info("--- Attempting to Get Training Data ---")
        can_send_tg = hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG

        if self.save_training_data_flag:
            self.logger.info(f"Checking for saved training data at: {self.training_data_file}")
            if os.path.exists(self.training_data_file):
                self.logger.info(f"Saved data file '{self.training_data_file}' found. Attempting to load...")
                try:
                    with open(self.training_data_file, 'rb') as f:
                        saved_data_dict = pickle.load(f)
                    if isinstance(saved_data_dict, dict) and 'X' in saved_data_dict and 'y' in saved_data_dict:
                        X_from_file, y_from_file = saved_data_dict['X'], saved_data_dict['y']
                        if isinstance(X_from_file, np.ndarray) and isinstance(y_from_file, np.ndarray) and \
                           X_from_file.ndim == 2 and y_from_file.ndim == 1 and \
                           len(X_from_file) == len(y_from_file) and \
                           len(X_from_file) >= self.history_train_samples:
                            self.logger.info(f"✅ Successfully loaded {len(X_from_file)} valid samples from {self.training_data_file}. "
                                             f"Using latest {self.history_train_samples} for this training run if more are present.")
                            collected_samples = CONFIG_TOTAL_REQUIRED_SAMPLES
                            update_progress_counter()
                            return X_from_file[-self.history_train_samples:], y_from_file[-self.history_train_samples:]
                        else:
                            self.logger.warning(f"Loaded data from {self.training_data_file} is invalid, insufficient "
                                                f"({len(X_from_file) if X_from_file is not None else 'NoneType'} vs {self.history_train_samples} required), or malformed. Will collect new data.")
                    else:
                        self.logger.warning(f"Data in {self.training_data_file} is not in the expected dictionary format. Will collect new data.")
                except Exception as e_load_data:
                    self.logger.error(f"Error loading training data from {self.training_data_file}: {e_load_data}. Will collect new data.", exc_info=True)
            else:
                self.logger.info(f"Saved data file '{self.training_data_file}' not found. Will proceed to collect new data.")
        else:
            self.logger.info("SAVE_TRAINING_DATA is False in config. Live data collection will proceed for training.")

        X_data_features_list, y_data_labels_list = [], []
        features_pending_labeling_buffer = []

        target_samples_to_collect_live = self.history_train_samples
        self.logger.info(f"🔄 Starting live data collection: Gathering features for {target_samples_to_collect_live} samples...")
        if can_send_tg:
            self.send_telegram(f"⏳ Starting live data collection for {target_samples_to_collect_live} samples... This may take some time.")

        collected_samples = 0

        samples_collected_this_session = 0
        max_collection_attempts_live = target_samples_to_collect_live * 5
        current_attempt_live = 0
        collection_phase_start_time = time.time()
        last_console_log_time_feat = 0

        self.logger.info(f"Feature Collection Phase: Target {target_samples_to_collect_live} feature sets.")

        while samples_collected_this_session < target_samples_to_collect_live and current_attempt_live < max_collection_attempts_live:
            current_attempt_live += 1
            loop_start_time_this_feat = time.time()

            update_progress_counter(new_sample_collected=False)

            try:
                current_feature_set = self._get_orderbook_features()
                if current_feature_set and 'mid' in current_feature_set and current_feature_set['mid'] > 0:
                    mid_price_at_feat_collection = safe_decimal(current_feature_set['mid'])
                    if mid_price_at_feat_collection > 0:
                        features_pending_labeling_buffer.append({
                            'features': current_feature_set,
                            'mid_price_dec': mid_price_at_feat_collection
                        })
                        samples_collected_this_session += 1
                        update_progress_counter(new_sample_collected=True)

                        if time.time() - last_console_log_time_feat > 30:
                             self.logger.info(f"   Feature Collection Progress: {samples_collected_this_session}/{target_samples_to_collect_live} collected...")
                             last_console_log_time_feat = time.time()

                        sleep_duration_this_feat = max(0.05, self.feature_fetch_interval - (time.time() - loop_start_time_this_feat))
                        time.sleep(sleep_duration_this_feat)
                    else:
                        self.logger.warning("Mid price from features was zero or negative. Skipping this sample."); time.sleep(self.feature_fetch_interval)
                else:
                    self.logger.debug("No valid features obtained in this iteration. Retrying."); time.sleep(self.feature_fetch_interval)
            except Exception as e_feat_coll_loop:
                self.logger.error(f"Error in feature collection loop (Attempt {current_attempt_live}): {e_feat_coll_loop}", exc_info=True)
                time.sleep(5)

        self.logger.info(f"✅ Feature collection phase complete: {samples_collected_this_session} feature sets gathered in {time.time()-collection_phase_start_time:.1f}s.")

        if not features_pending_labeling_buffer:
            self.logger.warning("No features were collected during the feature collection phase. Cannot proceed with labeling/training.")
            if can_send_tg:
                self.send_telegram("⚠️ Data Collection Failed: No features were gathered for labeling.")
            return None, None

        num_features_to_label = len(features_pending_labeling_buffer)
        estimated_labeling_time_total_min = (num_features_to_label * self.future_price_wait_seconds) / 60
        self.logger.info(f"Labeling Phase: Processing {num_features_to_label} collected feature sets. Estimated wait time per label: ~{self.future_price_wait_seconds}s (Total est: ~{estimated_labeling_time_total_min:.1f}m).")
        if can_send_tg:
            self.send_telegram(f"⏳ Labeling {num_features_to_label} samples (Est. total: ~{estimated_labeling_time_total_min:.1f}m)...")

        labeling_phase_start_time = time.time()
        last_console_log_time_labeling = 0

        for i, item_to_label_dict in enumerate(features_pending_labeling_buffer):
            label_for_item = self._label_future_move(item_to_label_dict['mid_price_dec'])
            if label_for_item is not None:
                feature_vector_np = self._extract_feature_vector(item_to_label_dict['features'])
                if feature_vector_np is not None:
                    X_data_features_list.append(feature_vector_np)
                    y_data_labels_list.append(label_for_item)

            if time.time() - last_console_log_time_labeling > 30 or (i + 1) == num_features_to_label :
                 self.logger.info(f"   Labeling Progress: {i+1}/{num_features_to_label} labeled...")
                 last_console_log_time_labeling = time.time()

        self.logger.info(f"✅ Labeling complete: {len(X_data_features_list)} usable labeled samples generated in {time.time()-labeling_phase_start_time:.1f}s.")

        if not X_data_features_list:
            self.logger.warning("No usable labeled data was generated after the labeling phase.")
            if can_send_tg: self.send_telegram("⚠️ Data Labeling Failed: No usable samples were generated.")
            return None, None

        X_final_array = np.array(X_data_features_list, dtype=float)
        y_final_array = np.array(y_data_labels_list, dtype=int)

        min_samples_to_save_new = int(target_samples_to_collect_live * 0.8)
        if self.save_training_data_flag and len(X_final_array) >= min_samples_to_save_new:
            self.logger.info(f"Attempting to save {len(X_final_array)} newly collected and labeled samples to {self.training_data_file}...")
            try:
                with open(self.training_data_file, 'wb') as f:
                    pickle.dump({'X': X_final_array, 'y': y_final_array}, f)
                self.logger.info(f"✅ Saved {len(X_final_array)} samples to {self.training_data_file}.")
                if len(X_final_array) >= target_samples_to_collect_live:
                    collected_samples = CONFIG_TOTAL_REQUIRED_SAMPLES
                    update_progress_counter()
            except Exception as e_save_new_data:
                self.logger.error(f"Error saving newly collected training data to {self.training_data_file}: {e_save_new_data}", exc_info=True)
                if can_send_tg: self.send_telegram(f"⚠️ Saving of newly collected training data failed: {e_save_new_data}")
        elif self.save_training_data_flag:
            self.logger.warning(f"Skipping save of newly collected data: {len(X_final_array)} samples < minimum {min_samples_to_save_new} required to save.")

        if len(X_final_array) >= target_samples_to_collect_live:
            collected_samples = CONFIG_TOTAL_REQUIRED_SAMPLES

        return X_final_array, y_final_array

    def _extract_feature_vector(self, features_dict):
        if features_dict is None:
            self.logger.warning("Cannot extract feature vector: input features_dict is None.")
            return None

        feature_order = [
            'imb1', 'imb2', 'imb3',
            'spread', 'spread_pct',
            'bid_wall', 'ask_wall',
            'avg_bid_gap', 'avg_ask_gap',
            'momentum_5', 'volatility_10',
            'total_vol_top'
        ]
        try:
            vector_components_list = []
            for key in feature_order:
                if key not in features_dict:
                    self.logger.warning(f"Feature extraction error: Key '{key}' missing from features_dict. Cannot create vector.")
                    return None
                vector_components_list.append(features_dict[key])

            feature_vector_np = np.array(vector_components_list, dtype=float)
            if np.isnan(feature_vector_np).any() or np.isinf(feature_vector_np).any():
                self.logger.warning(f"NaN or Infinity found in extracted feature vector: {feature_vector_np}. Discarding.")
                return None
            return feature_vector_np
        except Exception as e:
            self.logger.error(f"Error extracting feature vector: {type(e).__name__} - {e}", exc_info=True)
            return None

    def predict_signal(self, features_dict):
        global model_ready
        if self.model_w is None or self.model_b is None or \
           self.feature_mean is None or self.feature_std is None:
            self.logger.debug("Prediction skipped: Model components (weights, bias, mean, std) are missing.")
            return None, 0.0

        if not model_ready:
            self.logger.debug("Prediction skipped: Model is not marked as ready.")
            return None, 0.0

        if features_dict is None:
            self.logger.debug("Prediction skipped: Input features_dict is None.")
            return None, 0.0

        try:
            x_vector_raw = self._extract_feature_vector(features_dict)
            if x_vector_raw is None:
                self.logger.debug("Prediction skipped: Feature vector extraction failed.")
                return None, 0.0

            if len(x_vector_raw) != len(self.feature_mean) or len(x_vector_raw) != len(self.model_w):
                self.logger.error(f"Prediction failed due to feature vector length mismatch. "
                                  f"Input vector length: {len(x_vector_raw)}, "
                                  f"Model expected (weights): {len(self.model_w)}, "
                                  f"Model expected (mean/std): {len(self.feature_mean)}. Cannot proceed.")
                return None, 0.0

            safe_std_dev = np.where(np.abs(self.feature_std) < 1e-8, 1e-8, self.feature_std)
            x_vector_normalized = (x_vector_raw - self.feature_mean) / safe_std_dev

            if np.isnan(x_vector_normalized).any() or np.isinf(x_vector_normalized).any():
                self.logger.warning(f"NaN/Inf encountered after feature normalization during prediction: {x_vector_normalized}. Aborting prediction.")
                return None, 0.0

            z_value = np.clip(np.dot(x_vector_normalized, self.model_w) + self.model_b, -500, 500)
            prob_up = 1 / (1 + np.exp(-z_value))

            if hasattr(self, 'recent_ml_p_up'):
                self.recent_ml_p_up.append(prob_up)
            else:
                self.logger.error("CRITICAL: recent_ml_p_up deque not initialized in Strategy __init__. ML Trend filter will fail.")
                self.recent_ml_p_up = deque(maxlen=self.ml_trend_window_size)
                self.recent_ml_p_up.append(prob_up)

            prob_down = 1.0 - prob_up

            imb1_feat = safe_decimal(features_dict.get('imb1', 0))
            imb2_feat = safe_decimal(features_dict.get('imb2', 0))
            spread_pct_feat = safe_decimal(features_dict.get('spread_pct', 1.0))
            total_vol_feat = safe_decimal(features_dict.get('total_vol_top', 0))

            heuristics_ok_for_buy = imb1_feat > self.imb_threshold and \
                                    imb2_feat > self.imb2_threshold and \
                                    spread_pct_feat <= self.spread_limit_pct and \
                                    total_vol_feat >= self.vol_threshold

            heuristics_ok_for_sell = imb1_feat < -self.imb_threshold and \
                                     imb2_feat < -self.imb2_threshold and \
                                     spread_pct_feat <= self.spread_limit_pct and \
                                     total_vol_feat >= self.vol_threshold

            self.logger.debug(f"Predict raw: p_up={prob_up:.3f}, p_down={prob_down:.3f}. Min_Confidence_Threshold: {self.min_confidence:.3f}")
            self.logger.debug(f"Heuristics check: BuyOK?{heuristics_ok_for_buy} (imb1:{imb1_feat:.3f}>?{self.imb_threshold}, imb2:{imb2_feat:.3f}>?{self.imb2_threshold}), "
                              f"SellOK?{heuristics_ok_for_sell} (imb1:{imb1_feat:.3f}<?{-self.imb_threshold}, imb2:{imb2_feat:.3f}<?{-self.imb2_threshold}), "
                              f"Spread:{spread_pct_feat:.4%}(Limit:{self.spread_limit_pct:.4%}), Vol:{total_vol_feat:.2f}(Limit:{self.vol_threshold:.2f})")

            final_signal = None
            final_confidence = 0.0
            if prob_up > self.min_confidence and heuristics_ok_for_buy:
                final_signal = "buy"
                final_confidence = prob_up
            elif prob_down > self.min_confidence and heuristics_ok_for_sell:
                final_signal = "sell"
                final_confidence = prob_down

            return final_signal, float(final_confidence)

        except Exception as e:
            self.logger.error(f"Error in predict_signal: {type(e).__name__} - {e}", exc_info=True)
            return None, 0.0

    def calculate_dynamic_position_size(self, entry_price_dec, sl_price_dec, available_balance_dec):
        if not self._market_info:
            self.logger.error("Cannot calculate position size: Market info (self._market_info) is missing.")
            return Decimal('0')

        if not (isinstance(entry_price_dec, Decimal) and entry_price_dec > 0 and \
                isinstance(sl_price_dec, Decimal) and sl_price_dec > 0 and \
                isinstance(available_balance_dec, Decimal) and available_balance_dec > 0 and \
                entry_price_dec != sl_price_dec and \
                isinstance(self.risk_pct_per_trade, Decimal) and self.risk_pct_per_trade > 0):
            self.logger.warning(f"Invalid inputs for position size calculation. EP:{entry_price_dec}, SLP:{sl_price_dec}, AvailBal:{available_balance_dec}, RiskPct:{self.risk_pct_per_trade}. Returning size 0.")
            return Decimal('0')

        risk_amount_usdt = available_balance_dec * self.risk_pct_per_trade
        price_diff_per_unit = abs(entry_price_dec - sl_price_dec)

        if price_diff_per_unit <= Decimal('1e-8'):
            self.logger.warning(f"Price difference for SL ({price_diff_per_unit}) is too small or zero. Cannot calculate position size.")
            return Decimal('0')

        raw_size_contracts = risk_amount_usdt / price_diff_per_unit
        self.logger.debug(f"Raw position size calculated: {raw_size_contracts:.8f} contracts (RiskAmount:{risk_amount_usdt:.2f} USDT, PriceDiffPerUnit:{price_diff_per_unit:.4f})")

        adjusted_size = raw_size_contracts
        min_order_amount = Decimal('0')
        max_order_amount = Decimal('inf')
        amount_step_size_final = None

        limits_amount_market = self._market_info.get('limits', {}).get('amount', {})
        if limits_amount_market:
            min_order_amount = safe_decimal(limits_amount_market.get('min', '0'))
            max_order_amount = safe_decimal(limits_amount_market.get('max'), Decimal('inf'))

        qty_step_from_lot_filter_str = self._market_info.get('info', {}).get('lotSizeFilter', {}).get('qtyStep')
        if qty_step_from_lot_filter_str and isinstance(qty_step_from_lot_filter_str, str) and \
           qty_step_from_lot_filter_str.lower() != 'n/a' and qty_step_from_lot_filter_str.strip() != '':
            try:
                parsed_step = Decimal(str(qty_step_from_lot_filter_str))
                if parsed_step > 0:
                    amount_step_size_final = parsed_step
                    self.logger.info(f"Using qtyStep '{amount_step_size_final}' from market_info.info.lotSizeFilter.")
                else:
                    self.logger.warning(f"Parsed qtyStep '{qty_step_from_lot_filter_str}' from lotSizeFilter is not positive.")
            except (InvalidOperation, ValueError, TypeError) as e_step_parse:
                 self.logger.warning(f"Could not parse valid qtyStep '{qty_step_from_lot_filter_str}' from lotSizeFilter: {e_step_parse}. Will try other methods.")

        if amount_step_size_final is None:
            precision_amount_config = self._market_info.get('precision', {}).get('amount')
            if precision_amount_config is not None:
                try:
                    val_str_prec = str(precision_amount_config)
                    if '.' in val_str_prec or 'e-' in val_str_prec.lower() or 'E-' in val_str_prec.upper():
                        step_val_from_prec = Decimal(val_str_prec)
                        if step_val_from_prec > 0: amount_step_size_final = step_val_from_prec
                    else:
                        num_decimals_prec = int(float(val_str_prec))
                        if num_decimals_prec >= 0: amount_step_size_final = Decimal('1e-' + str(num_decimals_prec))

                    if amount_step_size_final and amount_step_size_final <= 0:
                        self.logger.warning(f"Step derived from precision.amount '{precision_amount_config}' is not positive. Resetting step.")
                        amount_step_size_final = None
                    elif amount_step_size_final:
                        self.logger.info(f"Using amount step '{amount_step_size_final}' derived from precision.amount '{precision_amount_config}'.")
                except (InvalidOperation, ValueError, TypeError) as e_prec_amt_parse:
                     self.logger.warning(f"Could not parse precision.amount '{precision_amount_config}' into a valid step size: {e_prec_amt_parse}.")

        if amount_step_size_final is None and min_order_amount > 0:
            if '.' in str(min_order_amount):
                num_digits_min_order = abs(min_order_amount.as_tuple().exponent)
            else:
                num_digits_min_order = 0
            amount_step_size_final = Decimal('1e-' + str(num_digits_min_order))
            self.logger.warning(f"Inferred amount step size: {amount_step_size_final} based on min_order_amount {min_order_amount} decimal places.")

        if amount_step_size_final is None or amount_step_size_final <= 0:
            self.logger.error(f"CRITICAL: Unable to determine a valid amount step size for symbol {self._market_info.get('symbol','N/A')}. Defaulting to a very small step (0.00001) for ETH/USDT as a last resort, but this is risky.")
            if self._market_info.get('symbol', 'N/A_SYMBOL') == 'ETH/USDT':
                amount_step_size_final = Decimal('0.00001')
            else:
                 self.logger.critical("Cannot determine a symbol-specific heuristic for amount step. Trade will likely fail.")
                 return Decimal('0')

        if amount_step_size_final > 0:
            adjusted_size = (raw_size_contracts / amount_step_size_final).quantize(Decimal('1'), rounding=ROUND_FLOOR) * amount_step_size_final
            if adjusted_size != raw_size_contracts:
                self.logger.info(f"Position size adjusted from raw {raw_size_contracts:.8f} to {adjusted_size:.8f} due to amount step {amount_step_size_final}")
        else:
            self.logger.error("Amount step size is invalid (zero or negative) even after fallbacks. Cannot adjust size."); return Decimal('0')

        if min_order_amount > 0 and adjusted_size < min_order_amount:
            self.logger.warning(f"Adjusted position size {adjusted_size:.8f} is less than minimum order amount {min_order_amount:.8f}. Returning size 0."); return Decimal('0')
        if adjusted_size > max_order_amount:
            self.logger.warning(f"Adjusted position size {adjusted_size:.8f} exceeds maximum order amount {max_order_amount:.8f}. Capping to max.")
            adjusted_size = (max_order_amount / amount_step_size_final).quantize(Decimal('1'), rounding=ROUND_FLOOR) * amount_step_size_final
            if min_order_amount > 0 and adjusted_size < min_order_amount:
                self.logger.error(f"Capped size {adjusted_size:.8f} is now below minimum order amount. Cannot trade."); return Decimal('0')

        leverage = safe_decimal(getattr(self.config_module, 'LEVERAGE', 1))
        if leverage <= 0: leverage = Decimal('1')

        estimated_cost_usdt = adjusted_size * entry_price_dec
        margin_required = estimated_cost_usdt / leverage

        if margin_required > available_balance_dec * Decimal('0.95'):
            self.logger.warning(f"Calculated position size {adjusted_size:.8f} requires margin ${margin_required:.2f} (Lvg:{leverage}x), "
                                f"which exceeds 95% of available balance ${available_balance_dec:.2f}. Reducing size.")
            reduction_factor = (available_balance_dec * Decimal('0.95')) / margin_required
            new_adjusted_size_for_margin = adjusted_size * reduction_factor
            new_adjusted_size_for_margin = (new_adjusted_size_for_margin / amount_step_size_final).quantize(Decimal('1'), rounding=ROUND_FLOOR) * amount_step_size_final

            if new_adjusted_size_for_margin < adjusted_size * Decimal('0.99'):
                 self.logger.info(f"Position size further reduced to {new_adjusted_size_for_margin:.8f} due to margin constraints.")
            adjusted_size = new_adjusted_size_for_margin

            if min_order_amount > 0 and adjusted_size < min_order_amount:
                self.logger.warning(f"Size reduced due to margin ({adjusted_size:.8f}) is now below minimum order amount. Cannot trade."); return Decimal('0')

        if adjusted_size <= 0:
            self.logger.error(f"Final calculated position size is zero or negative ({adjusted_size:.8f}). Cannot trade."); return Decimal('0')

        self.logger.info(f"Final Calculated Position Size: {adjusted_size:.8f} {self._market_info.get('base','BASE')}")
        return adjusted_size

    def calculate_dynamic_tp_sl(self, features_dict, signal, confidence, entry_price_dec):
        if not (features_dict and signal and isinstance(entry_price_dec, Decimal) and entry_price_dec > 0 and self._market_info):
            self.logger.warning(f"Invalid inputs for TP/SL calculation. Features: {bool(features_dict)}, Signal: {signal}, Entry: {entry_price_dec}, MarketInfo: {bool(self._market_info)}");
            return None, None, None, None

        volatility_from_features = safe_decimal(features_dict.get('volatility_10', 0.0))
        volatility_factor = Decimal('1.0')

        if volatility_from_features > 0 and volatility_from_features < Decimal('0.1'):
            volatility_factor = min(Decimal('2.0'), max(Decimal('0.5'), Decimal('1.0') + (volatility_from_features * Decimal('10.0'))))

        current_sl_pct_calc = self.base_sl_pct * volatility_factor
        current_tp_pct_calc = max(self.min_take_profit_pct, current_sl_pct_calc * self.min_tp_to_sl_ratio)

        safety_gap_abs = entry_price_dec * self.sl_tp_safety_gap_pct

        if signal == 'buy':
            sl_price_initial = entry_price_dec * (Decimal('1.0') - current_sl_pct_calc) - safety_gap_abs
            tp_price_initial = entry_price_dec * (Decimal('1.0') + current_tp_pct_calc) + safety_gap_abs
        elif signal == 'sell':
            sl_price_initial = entry_price_dec * (Decimal('1.0') + current_sl_pct_calc) + safety_gap_abs
            tp_price_initial = entry_price_dec * (Decimal('1.0') - current_tp_pct_calc) - safety_gap_abs
        else:
            self.logger.error(f"Invalid signal '{signal}' provided for TP/SL calculation."); return None, None, None, None

        rounded_sl_price, rounded_tp_price = sl_price_initial, tp_price_initial

        tick_size_from_market_str = self._market_info.get('info', {}).get('priceFilter', {}).get('tickSize')
        tick_size_decimal = None
        if tick_size_from_market_str and tick_size_from_market_str.lower() != 'n/a':
             try:
                 tick_size_decimal = safe_decimal(tick_size_from_market_str)
                 if tick_size_decimal <= 0: raise ValueError("Tick size from market info is not positive.")
             except (InvalidOperation, ValueError, TypeError) as e_tick_parse_err:
                 self.logger.warning(f"Error parsing tickSize '{tick_size_from_market_str}': {e_tick_parse_err}. Will use precision digits fallback."); tick_size_decimal = None

        if tick_size_decimal and tick_size_decimal > 0:
            if signal == 'buy':
                rounded_sl_price = (sl_price_initial / tick_size_decimal).quantize(Decimal('1'), rounding=ROUND_FLOOR) * tick_size_decimal
                rounded_tp_price = (tp_price_initial / tick_size_decimal).quantize(Decimal('1'), rounding=ROUND_CEILING) * tick_size_decimal
            else:
                rounded_sl_price = (sl_price_initial / tick_size_decimal).quantize(Decimal('1'), rounding=ROUND_CEILING) * tick_size_decimal
                rounded_tp_price = (tp_price_initial / tick_size_decimal).quantize(Decimal('1'), rounding=ROUND_FLOOR) * tick_size_decimal
            self.logger.debug(f"SL/TP (with safety gap, rounded by tick_size {tick_size_decimal}): SL {rounded_sl_price}, TP {rounded_tp_price}")
        else:
            price_precision_digits_market = self._market_info.get('precision', {}).get('price')
            if price_precision_digits_market is not None:
                try:
                    quantizer_val_str_prec = '1e-' + str(int(float(price_precision_digits_market)))
                    quantizer_decimal = Decimal(quantizer_val_str_prec)

                    if signal == 'buy':
                        rounded_sl_price = sl_price_initial.quantize(quantizer_decimal, rounding=ROUND_FLOOR)
                        rounded_tp_price = tp_price_initial.quantize(quantizer_decimal, rounding=ROUND_CEILING)
                    else:
                        rounded_sl_price = sl_price_initial.quantize(quantizer_decimal, rounding=ROUND_CEILING)
                        rounded_tp_price = tp_price_initial.quantize(quantizer_decimal, rounding=ROUND_FLOOR)
                    self.logger.debug(f"SL/TP (with safety gap, rounded by precision_digits {price_precision_digits_market}): SL {rounded_sl_price}, TP {rounded_tp_price}")
                except (ValueError, TypeError, InvalidOperation) as e_prec_round_err:
                     self.logger.warning(f"Error rounding SL/TP using precision_digits '{price_precision_digits_market}': {e_prec_round_err}. Using initial gapped values.");
            else:
                 self.logger.warning("Market info lacks tickSize and price precision for rounding SL/TP. Using initial gapped values.")

        if rounded_sl_price == entry_price_dec or rounded_tp_price == entry_price_dec:
            self.logger.error(f"Calculated SL ({format_price(rounded_sl_price)}) or TP ({format_price(rounded_tp_price)}) is identical to entry price ({format_price(entry_price_dec)}) after safety gap & rounding. Aborting trade setup.");
            return None, None, None, None
        if signal == 'buy' and (rounded_sl_price >= entry_price_dec or rounded_tp_price <= entry_price_dec):
            self.logger.error(f"Invalid Buy SL/TP after safety gap & rounding: Entry ${format_price(entry_price_dec)}, SL ${format_price(rounded_sl_price)}, TP ${format_price(rounded_tp_price)}. Aborting.");
            return None, None, None, None
        if signal == 'sell' and (rounded_sl_price <= entry_price_dec or rounded_tp_price >= entry_price_dec):
            self.logger.error(f"Invalid Sell SL/TP after safety gap & rounding: Entry ${format_price(entry_price_dec)}, SL ${format_price(rounded_sl_price)}, TP ${format_price(rounded_tp_price)}. Aborting.");
            return None, None, None, None

        self.logger.info(f"Calculated TP/SL for {signal.upper()} at EntryEst ~${format_price(entry_price_dec)} (Safety Gap: {self.sl_tp_safety_gap_pct:.4%}): "
                         f"SL%: {current_sl_pct_calc:.4%}, TP%: {current_tp_pct_calc:.4%}. "
                         f"SL Price (after gap & round): ${format_price(rounded_sl_price)}, TP Price (after gap & round): ${format_price(rounded_tp_price)}")
        return current_sl_pct_calc, current_tp_pct_calc, rounded_sl_price, rounded_tp_price

    def execute_trade(self, signal, size_dec, entry_price_dec_estimate, sl_pct_dec, tp_pct_dec, sl_price_dec, tp_price_dec, confidence):
        if self.active_trade['side']:
            self.logger.warning(f"execute_trade called but a trade is already active ({self.active_trade['side']}). Aborting new trade execution.")
            return False

        self.logger.info(f"--- Attempting Execute {signal.upper()} Trade ---")
        self.logger.info(f"Params: Size(ideal):{size_dec:.8f}, EntryEst:${format_price(entry_price_dec_estimate)}, SL:${format_price(sl_price_dec)}, TP:${format_price(tp_price_dec)}, Conf={confidence:.3f}")

        can_send_tg_exec = hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG
        entry_order_response_dict = None
        formatted_qty_actually_sent_str = None

        entry_order_extra_params = {}
        sl_tp_intended_with_entry_order = False
        sl_tp_successfully_sent_with_entry = False

        if self.use_stop_loss_config and sl_price_dec:
            entry_order_extra_params['stopLoss'] = str(sl_price_dec)
            entry_order_extra_params['slTriggerBy'] = 'MarkPrice'
            sl_tp_intended_with_entry_order = True

        if self.use_take_profit_config and tp_price_dec:
            entry_order_extra_params['takeProfit'] = str(tp_price_dec)
            entry_order_extra_params['tpTriggerBy'] = 'MarkPrice'
            sl_tp_intended_with_entry_order = True

        order_placement_successful = False
        max_entry_attempts = 2

        for attempt in range(1, max_entry_attempts + 1):
            params_for_this_entry_attempt = None
            log_attempt_suffix = ""

            if attempt == 1 and sl_tp_intended_with_entry_order:
                params_for_this_entry_attempt = entry_order_extra_params.copy()
                log_attempt_suffix = "with SL/TP params"
            elif attempt > 1:
                log_attempt_suffix = "without SL/TP params (retry)"

            self.logger.info(f"Placing entry market order (Attempt {attempt}/{max_entry_attempts}) {log_attempt_suffix}...")

            entry_order_response_dict, formatted_qty_actually_sent_str = self.exchange_api.place_market_order(
                symbol_ccxt_param=getattr(self.config_module, 'SYMBOL_CCXT'),
                side=signal,
                amount_param=float(size_dec),
                reduce_only=False,
                position_idx=0,
                params_override=params_for_this_entry_attempt
            )

            if entry_order_response_dict and 'id' in entry_order_response_dict:
                self.logger.info(f"Entry market order placement (Attempt {attempt}, {log_attempt_suffix}) successful. Order ID: {entry_order_response_dict['id']}")
                order_placement_successful = True
                sl_tp_successfully_sent_with_entry = (params_for_this_entry_attempt is not None and sl_tp_intended_with_entry_order)
                break
            else:
                self.logger.warning(f"Entry market order placement (Attempt {attempt}, {log_attempt_suffix}) failed at connector/API level. "
                                  f"Response: {str(entry_order_response_dict)[:200]}, QtySent: {formatted_qty_actually_sent_str}")
                if attempt == 1 and sl_tp_intended_with_entry_order:
                    self.logger.warning("Initial entry attempt with SL/TP failed. Retrying entry without SL/TP attached.")
                    if can_send_tg_exec:
                        self.send_telegram(f"⚠️ Entry order with SL/TP failed (API/Connector). Retrying entry only.")
                    time.sleep(0.5)
                    continue
                else:
                    self.logger.error("Trade Execution Failed (Entry Order): Placement failed after all attempts or critical formatting error.")
                    if can_send_tg_exec: self.send_telegram(f"❌ *Trade Failed (Entry Order)*\nReason: Placement failed after retries or formatting error.")
                    self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
                    return False

        if not order_placement_successful or not entry_order_response_dict or 'id' not in entry_order_response_dict:
            self.logger.error(f"Trade Execution Failed (Entry): No successful order placement after {max_entry_attempts} attempts. Last response: {str(entry_order_response_dict)[:200]}")
            if can_send_tg_exec: self.send_telegram(f"❌ *Trade Failed (Entry Order)*\nReason: No successful placement after all attempts.")
            self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
            return False

        entry_order_id = entry_order_response_dict['id']
        qty_log_str = formatted_qty_actually_sent_str if formatted_qty_actually_sent_str else "QTY_FORMAT_ERROR"
        self.logger.info(f"Market entry order ({signal.upper()}) placed: ID={entry_order_id} (Qty sent to exch: {qty_log_str}). Fetching fill details...")

        actual_entry_price_filled = None
        actual_size_filled = None
        fetch_fill_ok = False

        for fetch_attempt in range(self.max_fill_fetch_attempts):
            try:
                time.sleep(self.fill_fetch_delay_seconds + fetch_attempt * 0.5)
                filled_order_details = self.exchange_api.fetch_order(entry_order_id, getattr(self.config_module, 'SYMBOL_CCXT'))

                if filled_order_details:
                    status = filled_order_details.get('status')
                    filled_qty_from_order = safe_decimal(filled_order_details.get('filled', '0'))
                    avg_price_from_order = safe_decimal(filled_order_details.get('average'))

                    self.logger.info(f"Entry order {entry_order_id} fetch {fetch_attempt+1}: Status='{status}', FilledQty={filled_qty_from_order:.8f}, AvgPrice=${format_price(avg_price_from_order)}")

                    if status == 'closed' and filled_qty_from_order > 0 and avg_price_from_order and avg_price_from_order > 0:
                        actual_entry_price_filled = avg_price_from_order
                        actual_size_filled = filled_qty_from_order
                        fetch_fill_ok = True
                        break
                    if status in ('open', 'partially_filled') and filled_qty_from_order > 0 and avg_price_from_order and avg_price_from_order > 0:
                        actual_entry_price_filled = avg_price_from_order
                        actual_size_filled = filled_qty_from_order
                        fetch_fill_ok = True
                        self.logger.warning(f"Entry order {entry_order_id} status is '{status}'. Using current fill data. Ensure this is acceptable behavior.")
                        break
                    if status in ('canceled', 'rejected', 'expired'):
                        self.logger.error(f"Entry order {entry_order_id} FAILED on exchange with status '{status}'.")
                        if can_send_tg_exec: self.send_telegram(f"❌ Trade Failed: Entry order {entry_order_id} status is '{status}'.")
                        self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
                        return False
                else:
                    self.logger.warning(f"fetch_order for {entry_order_id} returned None on attempt {fetch_attempt+1}.")

            except ccxt.OrderNotFound:
                if fetch_attempt >= 1 and qty_log_str != "QTY_FORMAT_ERROR" and qty_log_str:
                    self.logger.warning(f"Entry order {entry_order_id} NOT FOUND on exchange (attempt {fetch_attempt+1}). "
                                        f"Using ESTIMATES based on QtySent='{qty_log_str}' and EntryEst='{entry_price_dec_estimate}' (RISKY).");
                    actual_size_filled = safe_decimal(qty_log_str)
                    actual_entry_price_filled = entry_price_dec_estimate
                    fetch_fill_ok = True
                    break
                elif fetch_attempt >= 1 :
                     self.logger.error(f"CRITICAL: Entry order {entry_order_id} not found (attempt {fetch_attempt+1}) AND QtySent was '{qty_log_str}'. "
                                       f"Using ideal size {size_dec} & entry estimate {entry_price_dec_estimate} as fallback (VERY RISKY).")
                     actual_size_filled = size_dec
                     actual_entry_price_filled = entry_price_dec_estimate
                     fetch_fill_ok = True
                     break
                else:
                    self.logger.warning(f"Entry order {entry_order_id} not found (attempt {fetch_attempt+1}). Retrying fetch.")
            except Exception as e_fetch_fill:
                self.logger.warning(f"Error fetching entry order {entry_order_id} details (attempt {fetch_attempt+1}): {e_fetch_fill}")
                if fetch_attempt == self.max_fill_fetch_attempts - 1 :
                     self.logger.error(f"Failed to fetch order {entry_order_id} after multiple errors. Using estimates "
                                       f"(QtySent: '{qty_log_str if qty_log_str and qty_log_str != 'QTY_FORMAT_ERROR' else str(size_dec)}', EntryEst: {entry_price_dec_estimate}) (RISKY).")
                     if qty_log_str != "QTY_FORMAT_ERROR" and qty_log_str:
                         actual_size_filled = safe_decimal(qty_log_str)
                     else:
                         actual_size_filled = size_dec
                     actual_entry_price_filled = entry_price_dec_estimate
                     fetch_fill_ok = True

        if not fetch_fill_ok or actual_size_filled is None or actual_entry_price_filled is None:
            self.logger.error(f"CRITICAL: Could not confirm fill details for entry order {entry_order_id} and no valid estimates applied. Trade failed.")
            if can_send_tg_exec: self.send_telegram(f"❌ Trade Failed: Could not confirm fill for entry {entry_order_id}.")
            try: self.exchange_api.cancel_order(entry_order_id, getattr(self.config_module,'SYMBOL_CCXT'))
            except: pass
            self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
            return False

        min_trade_size_market = Decimal('0.00000001')
        if self._market_info and self._market_info.get('limits', {}).get('amount', {}).get('min'):
             min_trade_size_market = safe_decimal(self._market_info['limits']['amount']['min'], min_trade_size_market)

        if actual_size_filled <= min_trade_size_market / Decimal('2'):
            self.logger.error(f"Entry order {entry_order_id} resulted in effectively zero or negligible filled size ({actual_size_filled:.8f}). Trade failed.");
            if can_send_tg_exec:
                self.send_telegram(f"❌ Trade Failed: Entry order {entry_order_id} had zero/negligible fill size.");
            self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
            return False

        size_sent_to_exchange_decimal = safe_decimal(qty_log_str) if qty_log_str and qty_log_str != "QTY_FORMAT_ERROR" else Decimal('0')

        if size_sent_to_exchange_decimal <= Decimal(0):
            self.logger.error(f"Cannot check fill ratio: effective quantity sent to exchange ('{qty_log_str}') is invalid or zero.")
        elif actual_size_filled < size_sent_to_exchange_decimal * self.min_fill_ratio_market_order:
            self.logger.error(f"Entry order {entry_order_id} insufficient fill: {actual_size_filled:.8f} (actual) vs {size_sent_to_exchange_decimal:.8f} (sent to exch). "
                              f"Ideal requested: {size_dec:.8f}. Below {self.min_fill_ratio_market_order*100:.2f}% threshold. Closing position if any.");
            if can_send_tg_exec:
                self.send_telegram(f"❌ Trade Failed: Insufficient fill for entry {entry_order_id} ({actual_size_filled:.6f} actual / {size_sent_to_exchange_decimal:.6f} sent). Ideal: {size_dec:.6f}.");
            if actual_size_filled > min_trade_size_market / Decimal('2'):
                try:
                    self.logger.info(f"Attempting to close partially filled position for {entry_order_id} due to insufficient fill ratio.")
                    close_side_insufficient = 'sell' if signal == 'buy' else 'buy'
                    _ , close_qty_insufficient_str = self.exchange_api.place_market_order(
                        getattr(self.config_module, 'SYMBOL_CCXT'),
                        close_side_insufficient,
                        float(actual_size_filled),
                        reduce_only=True
                    )
                    self.logger.info(f"Insufficient fill closure order placed, qty sent: {close_qty_insufficient_str if close_qty_insufficient_str else 'N/A'}")
                except Exception as close_e_insufficient:
                    self.logger.error(f"Failed to close partially filled position from insufficient fill scenario: {close_e_insufficient}")
            self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
            return False

        slippage_pct = Decimal('0')
        if entry_price_dec_estimate > 0 and actual_entry_price_filled > 0:
            if signal == 'buy':
                slippage_pct = (actual_entry_price_filled - entry_price_dec_estimate) / entry_price_dec_estimate
            else:
                slippage_pct = (entry_price_dec_estimate - actual_entry_price_filled) / entry_price_dec_estimate
            self.logger.info(f"Slippage for trade {entry_order_id}: {slippage_pct:+.4%} (Estimate: ${format_price(entry_price_dec_estimate)}, ActualFill: ${format_price(actual_entry_price_filled)})")


        sl_order_id_from_entry_resp = None
        tp_order_id_from_entry_resp = None
        if sl_tp_successfully_sent_with_entry:
            entry_order_info_dict = entry_order_response_dict.get('info', {})
            sl_order_id_from_entry_resp = entry_order_info_dict.get('stopLossOrderId') or \
                                          entry_order_info_dict.get('slOrderLinkID')
            tp_order_id_from_entry_resp = entry_order_info_dict.get('takeProfitOrderId') or \
                                          entry_order_info_dict.get('tpOrderLinkID')
            if sl_order_id_from_entry_resp: self.logger.info(f"SL Order ID from entry response: {sl_order_id_from_entry_resp}")
            if tp_order_id_from_entry_resp: self.logger.info(f"TP Order ID from entry response: {tp_order_id_from_entry_resp}")


        self.active_trade = {
            'side': signal,
            'entry_price': actual_entry_price_filled,
            'size': actual_size_filled,
            'timestamp': get_utc_now(),
            'entry_order_id': entry_order_id,
            'sl_id': sl_order_id_from_entry_resp,
            'tp_id': tp_order_id_from_entry_resp,
            'confidence': confidence,
            'sl_price': sl_price_dec,
            'tp_price': tp_price_dec,
            'sl_pct': sl_pct_dec,
            'tp_pct': tp_pct_dec,
            'entry_price_estimate': entry_price_dec_estimate,
            'bot_sltp_recalculated': False,
            'cooldown_until': get_utc_now()
        }

        self._position_confirmed = True
        self._position_confirm_time = time.time()
        self.logger.info(f"Active trade updated and position confirmed internally: {signal.upper()}, Size={actual_size_filled:.8f}, Entry=${format_price(actual_entry_price_filled)}")

        if can_send_tg_exec:
            trade_emoji = "📈" if signal == "buy" else "📉"
            base_asset_name = self._market_info.get('base', 'BASE') if self._market_info else ''

            sl_id_display_tg = ""
            tp_id_display_tg = ""

            if sl_tp_successfully_sent_with_entry:
                sl_id_display_tg = f"ID:`{sl_order_id_from_entry_resp}`" if sl_order_id_from_entry_resp else "(Positional SL Active)"
                tp_id_display_tg = f"ID:`{tp_order_id_from_entry_resp}`" if tp_order_id_from_entry_resp else "(Positional TP Active)"
            elif sl_tp_intended_with_entry_order:
                sl_id_display_tg = "(Bot Monitored)"
                tp_id_display_tg = "(Bot Monitored)"
            else:
                 sl_id_display_tg = "(Not Set in Config)" if not self.use_stop_loss_config else "(Not Set)"
                 tp_id_display_tg = "(Not Set in Config)" if not self.use_take_profit_config else "(Not Set)"

            tg_msg_trade_open = (
                f"{trade_emoji} *New Trade Opened: {signal.upper()}*\n"
                f"Symbol:`{getattr(self.config_module,'SYMBOL_BYBIT')}` (Entry ID:`{entry_order_id}`)\n"
                f"Size:`{actual_size_filled:.6f}` {base_asset_name} (Sent: `{qty_log_str}`)\n"
                f"Entry:`${format_price(actual_entry_price_filled)}` (Est: `${format_price(entry_price_dec_estimate)}`, Slip: `{slippage_pct:+.3%}`)\n"
                f"SL:`${format_price(sl_price_dec)}` {sl_id_display_tg}\n"
                f"TP:`${format_price(tp_price_dec)}` {tp_id_display_tg}\n"
                f"Conf:`{confidence:.3f}`"
            )
            if not sl_tp_successfully_sent_with_entry and sl_tp_intended_with_entry_order:
                tg_msg_trade_open += "\n*Note: SL/TP not set on exchange with entry order (retry). Bot will monitor.*"
            elif sl_tp_successfully_sent_with_entry and (not sl_order_id_from_entry_resp and not tp_order_id_from_entry_resp):
                 if self.use_stop_loss_config or self.use_take_profit_config:
                    tg_msg_trade_open += "\n*Note: SL/TP set at position level on exchange.*"

            self.send_telegram(tg_msg_trade_open)

        self.logger.info(f"--- {signal.upper()} Trade Execution Complete (Entry ID: {entry_order_id}) ---")

        if self.post_trade_entry_delay_seconds > 0 :
            self.logger.info(f"Pausing for {self.post_trade_entry_delay_seconds}s after trade confirmation to allow API position update...")
            time.sleep(self.post_trade_entry_delay_seconds)

        return True

    def close_active_trade(self, exit_price=None, reason="Unknown Closure Reason"):
        if self._is_closing_trade and self.active_trade.get('entry_order_id') == getattr(self, '_closing_trade_eoid_marker', None):
            self.logger.warning(f"close_active_trade re-entered for {self.active_trade.get('entry_order_id','N/A')} while already processing. Ignoring subsequent call.")
            return False

        active_trade_snapshot = self.active_trade.copy()
        original_side = active_trade_snapshot.get('side')
        if not original_side:
            self.logger.debug("close_active_trade called but no active trade in internal state. Resetting flags if any.")
            self._is_closing_trade = False
            self._closing_trade_eoid_marker = None
            return False

        self._is_closing_trade = True
        self._closing_trade_eoid_marker = active_trade_snapshot.get('entry_order_id')

        can_send_tg_close = hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG

        original_entry_price = active_trade_snapshot.get('entry_price')
        original_position_size = active_trade_snapshot.get('size')
        original_entry_timestamp = active_trade_snapshot.get('timestamp')
        original_sl_id = active_trade_snapshot.get('sl_id')
        original_tp_id = active_trade_snapshot.get('tp_id')
        original_confidence = active_trade_snapshot.get('confidence')
        original_sl_price_target = active_trade_snapshot.get('sl_price')
        original_tp_price_target = active_trade_snapshot.get('tp_price')
        original_entry_estimate = active_trade_snapshot.get('entry_price_estimate')
        entry_order_id_for_log = active_trade_snapshot.get('entry_order_id','N/A_InClosure')

        if not original_position_size or not original_entry_price:
            self.logger.error(f"Cannot close trade {entry_order_id_for_log}: missing original size or entry price from snapshot. Resetting trade state without PnL log.");
            self.active_trade = self._reset_active_trade()
            self._is_closing_trade = False; self._closing_trade_eoid_marker = None
            return False

        self.logger.info(f"--- Initiating Closure/Sync for {original_side.upper()} Trade {entry_order_id_for_log} (Reason: {reason}) ---")

        orders_to_cancel_explicitly = []
        if original_sl_id: orders_to_cancel_explicitly.append({'id': original_sl_id, 'type': 'SL'})
        if original_tp_id: orders_to_cancel_explicitly.append({'id': original_tp_id, 'type': 'TP'})

        if orders_to_cancel_explicitly:
            self.logger.info(f"Attempting to cancel associated separate SL/TP orders for trade {entry_order_id_for_log}: {[o['id'] for o in orders_to_cancel_explicitly]}")
            for order_info_to_cancel in orders_to_cancel_explicitly:
                try:
                    self.exchange_api.cancel_order(order_info_to_cancel['id'], getattr(self.config_module, 'SYMBOL_CCXT'))
                    self.logger.info(f"Cancel request potentially sent for {order_info_to_cancel['type']} order {order_info_to_cancel['id']}.")
                except Exception as e_cancel_sl_tp:
                    self.logger.warning(f"Error explicitly cancelling {order_info_to_cancel['type']} order {order_info_to_cancel['id']}: {e_cancel_sl_tp} (May already be inactive).")
            time.sleep(self.cancel_orders_wait_sec)
        else:
            self.logger.info(f"No separate SL/TP order IDs stored for trade {entry_order_id_for_log} to cancel (likely positional or not set).")

        closure_by_bot_successful = False
        bot_closure_error_msg = None
        actual_exit_price_for_pnl = safe_decimal(exit_price) if exit_price is not None else None

        exchange_managed_closure_reasons = [
            "SL_Hit_Exchange", "TP_Hit_Exchange",
            "PositionZeroOnExchange_Unknown", "PositionZeroOnExchange_PostGrace",
            "ClosedOnExchange", "PositionClosed_Filled",
            "BotMonitored_SL_Hit", "BotMonitored_TP_Hit"
        ]

        place_bot_market_close_order = True
        if any(emr_reason_part in reason for emr_reason_part in exchange_managed_closure_reasons):
            self.logger.info(f"Reason '{reason}' implies position likely already affected/closed. Bot may not need to send explicit market close order.");
            place_bot_market_close_order = False
            closure_by_bot_successful = True

            if actual_exit_price_for_pnl is None or actual_exit_price_for_pnl <= 0:
                self.logger.warning(f"Position {entry_order_id_for_log} (Reason: {reason}) had no exit price passed. Using stored SL/TP target or fallback for PnL.")
                if "SL_Hit" in reason.upper() and original_sl_price_target and original_sl_price_target > 0:
                    actual_exit_price_for_pnl = original_sl_price_target
                elif "TP_Hit" in reason.upper() and original_tp_price_target and original_tp_price_target > 0:
                    actual_exit_price_for_pnl = original_tp_price_target
                else:
                    try:
                        ticker_info = self.exchange_api.fetch_ticker(getattr(self.config_module, 'SYMBOL_CCXT'))
                        actual_exit_price_for_pnl = safe_decimal(ticker_info.get('last')) if ticker_info and ticker_info.get('last') else original_entry_price
                    except Exception as te_fallback:
                        actual_exit_price_for_pnl = original_entry_price
                        self.logger.error(f"Ticker fetch failed for PnL on '{reason}': {te_fallback}")

        if place_bot_market_close_order:
            if actual_exit_price_for_pnl is not None and actual_exit_price_for_pnl > 0:
                 self.logger.info(f"Bot closing trade {entry_order_id_for_log} via market order. Target exit (if from monitor) was: ${format_price(actual_exit_price_for_pnl)}")
            else:
                 self.logger.warning(f"Bot closing trade {entry_order_id_for_log} via market order, no specific exit price provided by monitor (or reason). Will use actual fill price from close order.")

            close_order_side = 'sell' if original_side == 'buy' else 'buy'
            bot_closing_market_order_id = None
            bot_close_qty_sent_str = None

            try:
                self.logger.info(f"Placing MARKET {close_order_side.upper()} order for {original_position_size:.8f} to close trade {entry_order_id_for_log}")
                close_order_resp_tuple_bot = self.exchange_api.place_market_order(
                    getattr(self.config_module, 'SYMBOL_CCXT'),
                    close_order_side,
                    float(original_position_size),
                    reduce_only=True,
                    position_idx=0
                )
                order_resp_dict_bot_close = close_order_resp_tuple_bot[0] if close_order_resp_tuple_bot else None
                bot_close_qty_sent_str = close_order_resp_tuple_bot[1] if close_order_resp_tuple_bot else None

                if order_resp_dict_bot_close and 'id' in order_resp_dict_bot_close:
                    bot_closing_market_order_id = order_resp_dict_bot_close['id']
                    self.logger.info(f"Bot's closing market order {bot_closing_market_order_id} placed (QtySent: {bot_close_qty_sent_str}). Fetching fill...")

                    fetched_bot_close_fill_price = None
                    for attempt_fetch_bot_close in range(self.max_fill_fetch_attempts):
                        time.sleep(self.fill_fetch_delay_seconds + attempt_fetch_bot_close * 0.5)
                        bot_close_filled_details = self.exchange_api.fetch_order(bot_closing_market_order_id, getattr(self.config_module, 'SYMBOL_CCXT'))
                        if bot_close_filled_details and bot_close_filled_details.get('status') == 'closed' and \
                           safe_decimal(bot_close_filled_details.get('filled',0)) > 0 and \
                           bot_close_filled_details.get('average') and safe_decimal(bot_close_filled_details.get('average')) > 0:
                            fetched_bot_close_fill_price = safe_decimal(bot_close_filled_details['average'])
                            self.logger.info(f"Bot's closing order {bot_closing_market_order_id} confirmed filled @ ${format_price(fetched_bot_close_fill_price)}.")
                            break
                        elif bot_close_filled_details and bot_close_filled_details.get('status') in ('canceled', 'rejected', 'expired'):
                            bot_closure_error_msg = f"Bot's closing market order {bot_closing_market_order_id} ended with status '{bot_close_filled_details.get('status')}'!"
                            break
                        self.logger.debug(f"Fetch attempt {attempt_fetch_bot_close+1} for bot's closing order {bot_closing_market_order_id}: Status {bot_close_filled_details.get('status') if bot_close_filled_details else 'N/A'}")

                    if fetched_bot_close_fill_price:
                        actual_exit_price_for_pnl = fetched_bot_close_fill_price
                        closure_by_bot_successful = True
                    elif actual_exit_price_for_pnl and actual_exit_price_for_pnl > 0:
                        self.logger.warning(f"Could not confirm fill for bot's closing order {bot_closing_market_order_id}. Using monitor-provided exit price ${format_price(actual_exit_price_for_pnl)} for PnL.")
                        closure_by_bot_successful = True
                    else:
                        bot_closure_error_msg = bot_closure_error_msg or f"Failed to confirm fill for bot's closing order {bot_closing_market_order_id} and no prior exit price available."

                elif order_resp_dict_bot_close is None and bot_close_qty_sent_str is not None:
                     self.logger.warning(f"Bot's closing market order for {entry_order_id_for_log} failed at exchange/connector level (no ID). Assuming position might already be zero if error was like 110017.")
                     bot_closure_error_msg = f"Bot's closing order for {entry_order_id_for_log} failed at exchange/connector level (no ID returned)."

                else:
                    bot_closure_error_msg = f"Failed to place market close order for {entry_order_id_for_log}: Connector returned unexpected response. Resp: {order_resp_dict_bot_close}, Qty: {bot_close_qty_sent_str}"

            except (ccxt.ExchangeError, ccxt.InvalidOrder) as e_exch_bot_close:
                err_str_bot_close = str(e_exch_bot_close).lower()
                is_pos_already_zero_err_bot = "110017" in err_str_bot_close or \
                                              "position is zero" in err_str_bot_close or \
                                              "position does not exist" in err_str_bot_close or \
                                              "110043" in err_str_bot_close

                if is_pos_already_zero_err_bot:
                    self.logger.info(f"Bot's close order for {entry_order_id_for_log} rejected by exchange ('{err_str_bot_close}'): position already zero. Treating as successful closure/sync.")
                    closure_by_bot_successful = True
                    if actual_exit_price_for_pnl is None or actual_exit_price_for_pnl <= 0:
                        self.logger.info(f"No exit price provided for {entry_order_id_for_log} (reason: {reason}) and close order rejected (already zero). Attempting to find actual exit via fetch_my_trades.")
                        try:
                            if original_entry_timestamp:
                                since_ms_infer = int((original_entry_timestamp - timedelta(minutes=1)).timestamp() * 1000)
                                my_trades_infer = self.exchange_api.fetch_my_trades(
                                    symbol_ccxt_param=getattr(self.config_module,'SYMBOL_CCXT'),
                                    since=since_ms_infer, limit=15
                                )
                                inferred_close_price = None
                                for t_infer in reversed(my_trades_infer):
                                    t_infer_side = t_infer.get('side','').lower()
                                    expected_fill_side_for_close = 'sell' if original_side == 'buy' else 'buy'
                                    t_infer_qty = safe_decimal(t_infer.get('amount','0'))
                                    t_infer_ts_dt = datetime.fromtimestamp(t_infer['timestamp'] / 1000, tz=timezone.utc)

                                    if t_infer_ts_dt > original_entry_timestamp and \
                                       t_infer_side == expected_fill_side_for_close and \
                                       abs(t_infer_qty - original_position_size) / original_position_size < Decimal('0.15'):
                                        inferred_close_price = safe_decimal(t_infer.get('price'))
                                        self.logger.info(f"Exit for {entry_order_id_for_log} (already zero) inferred from trade fill: {t_infer.get('id')} @ ${format_price(inferred_close_price)}")
                                        break
                                if inferred_close_price:
                                    actual_exit_price_for_pnl = inferred_close_price
                                else:
                                    self.logger.warning(f"fetch_my_trades did not find a clear closing trade fill for {entry_order_id_for_log} after 'position zero' error from bot's close attempt.")
                            else:
                                self.logger.warning("No original_entry_timestamp in snapshot to search fetch_my_trades effectively for already-zero pos.")
                        except Exception as e_fetch_trades_infer:
                            self.logger.warning(f"Error trying fetch_my_trades for {entry_order_id_for_log} after 'position zero' error: {e_fetch_trades_infer}")

                        if actual_exit_price_for_pnl is None or actual_exit_price_for_pnl <= 0:
                            if "SL" in reason.upper() and original_sl_price_target and original_sl_price_target > 0: actual_exit_price_for_pnl = original_sl_price_target
                            elif "TP" in reason.upper() and original_tp_price_target and original_tp_price_target > 0: actual_exit_price_for_pnl = original_tp_price_target
                            else:
                                current_mark_price_temp_close = None
                                try:
                                    ticker_close = self.exchange_api.fetch_ticker(getattr(self.config_module,'SYMBOL_CCXT'))
                                    current_mark_price_temp_close = safe_decimal(ticker_close['last']) if ticker_close and ticker_close.get('last') else original_entry_price
                                except Exception: current_mark_price_temp_close = original_entry_price
                                actual_exit_price_for_pnl = current_mark_price_temp_close
                            self.logger.info(f"Using fallback exit price ${format_price(actual_exit_price_for_pnl)} for already-zero position {entry_order_id_for_log} (reason: {reason}).")

                elif "30086" in err_str_bot_close :
                     self.logger.warning(f"Exchange rejected bot's closing order for {entry_order_id_for_log} due to size issue (e.g. dust): {e_exch_bot_close}. Treating as closed for PnL purposes.")
                     closure_by_bot_successful = True
                     if actual_exit_price_for_pnl is None or actual_exit_price_for_pnl <= 0: actual_exit_price_for_pnl = original_entry_price
                else:
                    bot_closure_error_msg = f"ExchangeError when bot tried to close {entry_order_id_for_log}: {e_exch_bot_close}"; closure_by_bot_successful = False

            except Exception as e_close_generic_bot:
                bot_closure_error_msg = f"Unexpected error when bot tried placing closing market order for {entry_order_id_for_log}: {e_close_generic_bot}"; closure_by_bot_successful = False
                self.logger.error(bot_closure_error_msg, exc_info=True)

        if closure_by_bot_successful:
            if actual_exit_price_for_pnl is None or actual_exit_price_for_pnl <= 0:
                self.logger.warning(f"No valid actual_exit_price after closure sequence for {entry_order_id_for_log}. Using original entry price ${format_price(original_entry_price)} for PnL (will result in zero PnL).");
                actual_exit_price_for_pnl = original_entry_price

            pnl_percentage_final, pnl_usdt_value_final = Decimal(0), Decimal(0)
            if original_entry_price and original_entry_price > 0 and \
               actual_exit_price_for_pnl and actual_exit_price_for_pnl > 0 and \
               original_position_size and original_position_size > 0:
                if original_side == 'buy':
                    pnl_percentage_final = (actual_exit_price_for_pnl - original_entry_price) / original_entry_price
                    pnl_usdt_value_final = original_position_size * (actual_exit_price_for_pnl - original_entry_price)
                else:
                    pnl_percentage_final = (original_entry_price - actual_exit_price_for_pnl) / original_entry_price
                    pnl_usdt_value_final = original_position_size * (original_entry_price - actual_exit_price_for_pnl)
            else:
                self.logger.error(f"Cannot calculate PNL for {entry_order_id_for_log}: Invalid inputs. EP:{original_entry_price}, ExitP:{actual_exit_price_for_pnl}, Size:{original_position_size}")

            pnl_emoji_final = "✅" if pnl_percentage_final >= 0 else "🔻"
            self.logger.info(f"Trade {entry_order_id_for_log} FINALIZED. Reason: {reason}, ExitPrice ~${format_price(actual_exit_price_for_pnl)}. PNL: {pnl_percentage_final:.4%} ({pnl_usdt_value_final:+.2f} USDT)")

            closure_timestamp = get_utc_now()
            self._log_trade_to_csv(
                original_entry_timestamp or closure_timestamp,
                original_side,
                original_entry_price or Decimal(0),
                actual_exit_price_for_pnl or Decimal(0),
                original_position_size or Decimal(0),
                pnl_percentage_final, pnl_usdt_value_final,
                reason, original_confidence or 0.0,
                original_sl_price_target or Decimal(0), original_tp_price_target or Decimal(0),
                original_entry_estimate
            )
            if can_send_tg_close:
                self.send_telegram(
                    f"{pnl_emoji_final} *Trade Closed: {original_side.upper()}*\n"
                    f"ID: `{entry_order_id_for_log}` | Reason: `{reason}`\n"
                    f"Entry Price: `${format_price(original_entry_price)}` | Exit Price: `${format_price(actual_exit_price_for_pnl)}`\n"
                    f"Size: `{original_position_size:.6f}`\n"
                    f"PnL: `{pnl_percentage_final:+.3%}` (`${pnl_usdt_value_final:+.2f}` USDT)\n"
                    f"Duration: `{format_time_difference(original_entry_timestamp, closure_timestamp) if original_entry_timestamp else 'N/A'}`"
                )

            self.active_trade = self._reset_active_trade()
            self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes)
            self.logger.info(f"Cooldown active until {self.active_trade['cooldown_until'].strftime('%H:%M:%S UTC')}")
            self.logger.info(f"--- Active Trade {entry_order_id_for_log} Closure/Sync Sequence Complete ---")
            self._is_closing_trade = False; self._closing_trade_eoid_marker = None; return True
        else:
            final_error_msg_for_log = bot_closure_error_msg or "Unknown error during bot's closure attempt."
            self.logger.critical(f"CRITICAL FAILURE TO CLOSE/SYNC TRADE: {final_error_msg_for_log}. Position {original_side.upper()} {original_position_size} for {entry_order_id_for_log} status uncertain, likely STILL OPEN.")
            if can_send_tg_close:
                self.send_telegram(f"🚨 CRITICAL ERROR\nFailed to close/sync trade {entry_order_id_for_log}: {final_error_msg_for_log}\nPosition *MAY STILL BE OPEN*. Manual check required!")
            self._is_closing_trade = False; self._closing_trade_eoid_marker = None; return False

    def monitor_active_trade(self):
        global model_ready

        active_trade_snapshot_mon = self.active_trade.copy()
        bot_active_side = active_trade_snapshot_mon.get('side')

        if self._is_closing_trade and active_trade_snapshot_mon.get('entry_order_id') == getattr(self, '_closing_trade_eoid_marker', None):
            self.logger.debug(f"Monitor: Skipping for {active_trade_snapshot_mon.get('entry_order_id','N/A')}, existing closure process active.")
            return

        if not bot_active_side:
            self.logger.debug("Monitor: Skipping, no active trade tracked by bot.")
            return

        now_utc_monitor = get_utc_now()
        last_monitor_check_time = self.monitor_state.get('last_check_time')
        if last_monitor_check_time is None:
            self.monitor_state['last_check_time'] = now_utc_monitor - timedelta(seconds=self.active_trade_monitor_interval_seconds + 1)
            last_monitor_check_time = self.monitor_state['last_check_time']

        if (now_utc_monitor - last_monitor_check_time) < timedelta(seconds=self.active_trade_monitor_interval_seconds):
            return
        self.monitor_state['last_check_time'] = now_utc_monitor

        entry_px_mon = active_trade_snapshot_mon.get('entry_price')
        pos_sz_mon = active_trade_snapshot_mon.get('size')
        entry_timestamp_mon = active_trade_snapshot_mon.get('timestamp')
        eoid_mon = active_trade_snapshot_mon.get('entry_order_id', 'N/A_Monitor')

        bot_sl_price_target = active_trade_snapshot_mon.get('sl_price')
        bot_tp_price_target = active_trade_snapshot_mon.get('tp_price')
        entry_price_estimate_for_trade = active_trade_snapshot_mon.get('entry_price_estimate')

        if active_trade_snapshot_mon.get('sl_id') is None and active_trade_snapshot_mon.get('tp_id') is None and \
           entry_price_estimate_for_trade and entry_px_mon and \
           abs(entry_px_mon - entry_price_estimate_for_trade) / entry_price_estimate_for_trade > Decimal('0.0001') and \
           not active_trade_snapshot_mon.get('bot_sltp_recalculated', False):

            self.logger.info(f"Monitor ({eoid_mon}): Actual entry ${format_price(entry_px_mon)} differs from estimate ${format_price(entry_price_estimate_for_trade)}. "
                             f"Recalculating bot-monitored SL/TP targets.")

            original_sl_pct = active_trade_snapshot_mon.get('sl_pct')
            original_tp_pct = active_trade_snapshot_mon.get('tp_pct')

            if original_sl_pct and original_tp_pct:
                if bot_active_side == 'buy':
                    new_sl_target = entry_px_mon * (Decimal('1') - original_sl_pct)
                    new_tp_target = entry_px_mon * (Decimal('1') + original_tp_pct)
                else:
                    new_sl_target = entry_px_mon * (Decimal('1') + original_sl_pct)
                    new_tp_target = entry_px_mon * (Decimal('1') - original_tp_pct)

                price_precision_digits = self._market_info.get('precision', {}).get('price', 2)
                quantizer = Decimal('1e-' + str(int(price_precision_digits)))

                if bot_active_side == 'buy':
                    bot_sl_price_target = new_sl_target.quantize(quantizer, rounding=ROUND_FLOOR)
                    bot_tp_price_target = new_tp_target.quantize(quantizer, rounding=ROUND_CEILING)
                else:
                    bot_sl_price_target = new_sl_target.quantize(quantizer, rounding=ROUND_CEILING)
                    bot_tp_price_target = new_tp_target.quantize(quantizer, rounding=ROUND_FLOOR)

                self.active_trade['sl_price'] = bot_sl_price_target
                self.active_trade['tp_price'] = bot_tp_price_target
                self.active_trade['bot_sltp_recalculated'] = True
                self.logger.info(f"Monitor ({eoid_mon}): Bot SL/TP targets updated based on actual entry. "
                                 f"New SL: ${format_price(bot_sl_price_target)}, New TP: ${format_price(bot_tp_price_target)}")
            else:
                self.logger.warning(f"Monitor ({eoid_mon}): Cannot recalculate SL/TP, original percentages missing from trade data.")


        if not all([bot_active_side, entry_px_mon, pos_sz_mon, entry_timestamp_mon, eoid_mon != 'N/A_Monitor']):
            self.logger.error(f"Monitor Error: Incomplete or invalid bot internal trade data: {active_trade_snapshot_mon}. Resetting trade state.")
            self.active_trade = self._reset_active_trade()
            return

        current_mark_price_check = None
        position_confirmed_on_exchange_this_tick = False

        try:
            pos_info_exch = self.exchange_api.get_position(getattr(self.config_module, 'SYMBOL_BYBIT', 'ETHUSDT'))

            if pos_info_exch and pos_info_exch.get('markPrice'):
                current_mark_price_check = safe_decimal(pos_info_exch.get('markPrice'))
            if not current_mark_price_check or current_mark_price_check <= 0:
                ticker_info_mon = self.exchange_api.fetch_ticker(getattr(self.config_module, 'SYMBOL_CCXT'))
                current_mark_price_check = safe_decimal(ticker_info_mon['last']) if ticker_info_mon and ticker_info_mon.get('last') else entry_px_mon

            self.logger.debug(f"Monitor ({eoid_mon}) Pre-Check - EntryPx: {format_price(entry_px_mon)}, MarkPx: {format_price(current_mark_price_check)}, BotSLTgt: {format_price(bot_sl_price_target)}, BotTPTgt: {format_price(bot_tp_price_target)}")

            min_meaningful_pos_size = safe_decimal(self._market_info.get('limits', {}).get('amount', {}).get('min', '0.00001')) * Decimal('0.1')

            exch_pos_size_current = safe_decimal(pos_info_exch.get('contracts', '0')) if pos_info_exch else Decimal('0')
            exch_pos_side_raw = pos_info_exch.get('side', '').lower() if pos_info_exch else ''
            expected_bot_side_on_exch = 'long' if bot_active_side == 'buy' else 'short'

            size_matches_exch_bot = abs(exch_pos_size_current - pos_sz_mon) / pos_sz_mon < Decimal('0.1') if pos_sz_mon > 0 else exch_pos_size_current.copy_abs() < min_meaningful_pos_size * 2

            if pos_info_exch and \
               exch_pos_size_current.copy_abs() >= min_meaningful_pos_size and \
               exch_pos_side_raw == expected_bot_side_on_exch and \
               size_matches_exch_bot:
                position_confirmed_on_exchange_this_tick = True
                if not self._position_confirmed:
                    self.logger.info(f"Monitor ({eoid_mon}): Position now confirmed on exchange: {exch_pos_side_raw} {exch_pos_size_current}.")
                    self._position_confirmed = True
                    self._position_confirm_time = time.time()

            if not position_confirmed_on_exchange_this_tick:
                trade_age_current_seconds = (now_utc_monitor - entry_timestamp_mon).total_seconds()

                if trade_age_current_seconds < self.new_trade_grace_period_seconds:
                    self.logger.warning(f"Monitor ({eoid_mon}): get_position shows mismatch/absence for new trade (age: {trade_age_current_seconds:.1f}s). "
                                        f"Exch (get_pos): {exch_pos_side_raw} {exch_pos_size_current}. Bot expected: {expected_bot_side_on_exch} {pos_sz_mon}. "
                                        f"Attempting fetch_order on entry ID {eoid_mon} as fallback.")

                    entry_order_details_grace = self.exchange_api.fetch_order(eoid_mon, getattr(self.config_module, 'SYMBOL_CCXT'))
                    if entry_order_details_grace and \
                       entry_order_details_grace.get('status') == 'closed' and \
                       safe_decimal(entry_order_details_grace.get('filled', '0')) >= pos_sz_mon * self.min_fill_ratio_market_order:
                        self.logger.info(f"Monitor ({eoid_mon}): Fallback fetch_order (grace period) confirms entry was filled ({entry_order_details_grace.get('filled')}). "
                                         f"Treating position as active for this cycle despite get_position lag.")
                        position_confirmed_on_exchange_this_tick = True
                        if not self._position_confirmed :
                            self._position_confirmed = True; self._position_confirm_time = time.time()
                    else:
                        self.logger.warning(f"Monitor ({eoid_mon}): Fallback fetch_order for entry ID {eoid_mon} also does not confirm sufficient fill. "
                                            f"Order details: {str(entry_order_details_grace)[:200]}. Will wait for next monitor cycle (still in grace).")
                        return

                extended_grace_for_trades_check = self.new_trade_grace_period_seconds * 3
                if not position_confirmed_on_exchange_this_tick and self._position_confirmed and trade_age_current_seconds < extended_grace_for_trades_check:
                    self.logger.warning(f"Monitor ({eoid_mon}): get_position mismatch persists past initial grace ({trade_age_current_seconds:.1f}s), "
                                        f"but entry fill was confirmed. Checking fetch_my_trades for closure.")
                    try:
                        since_ts_ms_trades = int((entry_timestamp_mon - timedelta(seconds=10)).timestamp() * 1000)
                        recent_trades_list = self.exchange_api.fetch_my_trades(
                            symbol_ccxt_param=getattr(self.config_module, 'SYMBOL_CCXT'),
                            since=since_ts_ms_trades,
                            limit=50 # MODIFIED: Increased limit
                        )
                        closing_trade_price_from_history = None
                        for trade_item in reversed(recent_trades_list):
                            trade_item_side = trade_item.get('side', '').lower()
                            trade_item_qty = safe_decimal(trade_item.get('amount', '0'))

                            is_opp_side = (bot_active_side == 'buy' and trade_item_side == 'sell') or \
                                          (bot_active_side == 'sell' and trade_item_side == 'buy')
                            is_close_qty = abs(trade_item_qty - pos_sz_mon) / pos_sz_mon < Decimal('0.1') if pos_sz_mon > 0 else False

                            if is_opp_side and is_close_qty:
                                closing_trade_price_from_history = safe_decimal(trade_item.get('price', '0'))
                                self.logger.info(f"Monitor ({eoid_mon}): Found a closing trade in fetch_my_trades. ID: {trade_item.get('id')}, Price: {format_price(closing_trade_price_from_history)}, Qty: {trade_item_qty}. Position considered closed by exchange.")
                                self.close_active_trade(exit_price=closing_trade_price_from_history, reason=f"ClosedOnExchange_Fill_{trade_item.get('id')}")
                                return
                        if closing_trade_price_from_history is None:
                             self.logger.info(f"Monitor ({eoid_mon}): No definitive closing trade found in fetch_my_trades yet for (previously confirmed) vanished position.")
                    except Exception as e_fetch_trades_mon:
                        self.logger.error(f"Monitor ({eoid_mon}): Error during fetch_my_trades check for vanished position: {type(e_fetch_trades_mon).__name__} - {e_fetch_trades_mon}", exc_info=True)

                if not position_confirmed_on_exchange_this_tick and (trade_age_current_seconds >= self.new_trade_grace_period_seconds):
                    self.logger.info(f"👻 Position {eoid_mon} ({bot_active_side.upper()}) determined to be effectively closed or absent on Exchange (past all grace/checks). "
                                     f"Exch (get_pos): {exch_pos_side_raw} {exch_pos_size_current}. Bot expected: {expected_bot_side_on_exch} {pos_sz_mon}.")

                    determined_vanished_close_reason = "PositionZeroOnExchange_PostGrace"
                    determined_vanished_close_price = None

                    sl_hit_inferred_vanished = False
                    if bot_sl_price_target and entry_px_mon:
                        if bot_active_side == 'buy' and current_mark_price_check <= bot_sl_price_target : sl_hit_inferred_vanished = True
                        if bot_active_side == 'sell' and current_mark_price_check >= bot_sl_price_target : sl_hit_inferred_vanished = True

                    tp_hit_inferred_vanished = False
                    if bot_tp_price_target and entry_px_mon:
                        is_tp_target_logical = (bot_active_side == 'buy' and bot_tp_price_target > entry_px_mon) or \
                                               (bot_active_side == 'sell' and bot_tp_price_target < entry_px_mon)
                        if is_tp_target_logical:
                            if bot_active_side == 'buy' and current_mark_price_check >= bot_tp_price_target: tp_hit_inferred_vanished = True
                            if bot_active_side == 'sell' and current_mark_price_check <= bot_tp_price_target: tp_hit_inferred_vanished = True
                        elif bot_tp_price_target :
                             self.logger.warning(f"Monitor ({eoid_mon}): Vanished. Bot's TP target ${format_price(bot_tp_price_target)} for {bot_active_side.upper()} was ILLOGICAL vs entry ${format_price(entry_px_mon)}. Not classifying as TP_Hit from this.")

                    if sl_hit_inferred_vanished and tp_hit_inferred_vanished:
                        sl_dist_van = abs(current_mark_price_check - bot_sl_price_target) if bot_sl_price_target else Decimal('inf')
                        tp_dist_van = abs(current_mark_price_check - bot_tp_price_target) if bot_tp_price_target else Decimal('inf')
                        if sl_dist_van <= tp_dist_van:
                            determined_vanished_close_reason = "SL_Hit_Exchange_Inferred"; determined_vanished_close_price = bot_sl_price_target
                        else:
                            determined_vanished_close_reason = "TP_Hit_Exchange_Inferred"; determined_vanished_close_price = bot_tp_price_target
                    elif sl_hit_inferred_vanished:
                        determined_vanished_close_reason = "SL_Hit_Exchange_Inferred"; determined_vanished_close_price = bot_sl_price_target
                    elif tp_hit_inferred_vanished:
                        determined_vanished_close_reason = "TP_Hit_Exchange_Inferred"; determined_vanished_close_price = bot_tp_price_target

                    if determined_vanished_close_price and determined_vanished_close_price > 0:
                         self.logger.info(f"Position {eoid_mon} vanished, classified as {determined_vanished_close_reason} @ target ${format_price(determined_vanished_close_price)} (Mark at detection: ${format_price(current_mark_price_check)}).")
                    else:
                        if trade_age_current_seconds < self.short_trade_duration_threshold_seconds:
                            determined_vanished_close_price = entry_px_mon
                            self.logger.warning(f"Position {eoid_mon} vanished within {trade_age_current_seconds:.1f}s. Reason: {determined_vanished_close_reason}. Using entry price ${format_price(entry_px_mon)} for PnL.")
                        else:
                            determined_vanished_close_price = current_mark_price_check
                            self.logger.warning(f"Position {eoid_mon} vanished. Reason: {determined_vanished_close_reason}. Using current mark price ${format_price(current_mark_price_check)} for PnL.")

                    if determined_vanished_close_price is None or determined_vanished_close_price <= 0:
                        determined_vanished_close_price = entry_px_mon

                    if self.close_active_trade(exit_price=determined_vanished_close_price, reason=determined_vanished_close_reason):
                        return
                    else:
                        self.logger.error(f"Monitor ({eoid_mon}): close_active_trade FAILED for vanished position. Bot state might be inconsistent."); return

        except Exception as e_outer_pos_check_mon:
            self.logger.error(f"Monitor ({eoid_mon}): Outer error during position status check/reconciliation: {type(e_outer_pos_check_mon).__name__} - {e_outer_pos_check_mon}", exc_info=True)
            return

        if not position_confirmed_on_exchange_this_tick:
             self.logger.debug(f"Monitor ({eoid_mon}): Position not confirmed on exchange this tick after all checks. Skipping bot-managed SL/TP/ML exits.")
             return

        if not current_mark_price_check or current_mark_price_check <= 0:
            self.logger.error(f"Monitor({eoid_mon}): Invalid current_mark_price ({format_price(current_mark_price_check)}) for bot-managed exits after position confirmed. Skipping."); return

        # Use the (potentially recalculated) bot_sl_price_target and bot_tp_price_target for bot monitoring
        if self.use_stop_loss_config and bot_sl_price_target and entry_px_mon:
            sl_hit_bot_managed = False
            if bot_active_side == 'buy' and bot_sl_price_target < entry_px_mon and current_mark_price_check <= bot_sl_price_target: sl_hit_bot_managed = True
            elif bot_active_side == 'sell' and bot_sl_price_target > entry_px_mon and current_mark_price_check >= bot_sl_price_target: sl_hit_bot_managed = True

            if sl_hit_bot_managed:
                self.logger.info(f"‼️ Bot-Monitored SL Hit for {eoid_mon}: Mark {format_price(current_mark_price_check)} vs SL Target {format_price(bot_sl_price_target)}")
                if self.close_active_trade(exit_price=current_mark_price_check, reason="BotMonitored_SL_Hit"): return

        if self.use_take_profit_config and bot_tp_price_target and entry_px_mon:
            tp_hit_bot_managed = False
            is_tp_target_bot_logical = (bot_active_side == 'buy' and bot_tp_price_target > entry_px_mon) or \
                                       (bot_active_side == 'sell' and bot_tp_price_target < entry_px_mon)
            if is_tp_target_bot_logical:
                if (bot_active_side == 'buy' and current_mark_price_check >= bot_tp_price_target) or \
                   (bot_active_side == 'sell' and current_mark_price_check <= bot_tp_price_target):
                    tp_hit_bot_managed = True
            elif bot_tp_price_target:
                 self.logger.debug(f"Monitor ({eoid_mon}): Bot TP target ${format_price(bot_tp_price_target)} for {bot_active_side.upper()} is ILLOGICAL vs entry ${format_price(entry_px_mon)}. Not used for bot monitoring.")

            if tp_hit_bot_managed:
                self.logger.info(f"🎯 Bot-Monitored TP Hit for {eoid_mon}: Mark {format_price(current_mark_price_check)} vs TP Target {format_price(bot_tp_price_target)}")
                if self.close_active_trade(exit_price=current_mark_price_check, reason="BotMonitored_TP_Hit"): return

        if not model_ready:
            self.logger.debug(f"Monitor({eoid_mon}): Model not ready, skipping ML-based exit checks."); return

        features_for_ml_exit = self._get_orderbook_features()
        if not features_for_ml_exit:
            self.logger.warning(f"Monitor({eoid_mon}): No features obtained for ML exit check. Skipping."); return

        ml_signal_now_exit, ml_confidence_now_exit = self.predict_signal(features_for_ml_exit)

        if ml_signal_now_exit == self.monitor_state.get('last_signal'):
            self.monitor_state['signal_stable_count'] += 1
        else:
            self.monitor_state['last_signal'] = ml_signal_now_exit
            self.monitor_state['signal_stable_count'] = 1 if ml_signal_now_exit else 0
        self.monitor_state['last_confidence'] = ml_confidence_now_exit

        if self.exit_on_reversal_signal_flag:
            is_reversal_signal = (bot_active_side == 'buy' and ml_signal_now_exit == 'sell') or \
                                 (bot_active_side == 'sell' and ml_signal_now_exit == 'buy')
            if is_reversal_signal and ml_confidence_now_exit >= self.min_confidence and \
               self.monitor_state['signal_stable_count'] >= self.reversal_stability_ticks:
                self.logger.info(f"‼️ ML Signal Reversal for {eoid_mon}: Trade was {bot_active_side.upper()}, new signal {ml_signal_now_exit.upper()} (Conf:{ml_confidence_now_exit:.2f}, StableTicks:{self.monitor_state['signal_stable_count']})")
                if self.close_active_trade(exit_price=current_mark_price_check, reason="MLSignalReversal"): return

        if self.lock_profit_on_conf_drop_flag:
            current_pnl_pct_for_lock = Decimal(0)
            if entry_px_mon and current_mark_price_check and entry_px_mon > 0:
                current_pnl_pct_for_lock = ((current_mark_price_check - entry_px_mon) / entry_px_mon) if bot_active_side == 'buy' else ((entry_px_mon - current_mark_price_check) / entry_px_mon)

            original_signal_or_neutral_for_lock = (ml_signal_now_exit == bot_active_side) or \
                                                  (ml_signal_now_exit is None) or \
                                                  (ml_confidence_now_exit < self.min_confidence)

            if current_pnl_pct_for_lock > self.profit_lock_min_pnl_pct and \
               ml_confidence_now_exit < self.profit_lock_conf_drop_threshold and \
               original_signal_or_neutral_for_lock:
                self.logger.info(f"💰 Profit Lock Triggered for {eoid_mon}: PnL={current_pnl_pct_for_lock:.2%}, Current ML Conf={ml_confidence_now_exit:.2f} (drop threshold {self.profit_lock_conf_drop_threshold})")
                if self.close_active_trade(exit_price=current_mark_price_check, reason="ProfitLock_ConfidenceDrop"): return

    def check_and_manage_ghost_position(self):
        if self.active_trade.get('side') is None and not self._is_closing_trade:
            now_ghost_check = get_utc_now()
            check_interval_ghost = timedelta(minutes=self.ghost_pos_check_interval_min)

            if self._last_ghost_check_time and now_ghost_check - self._last_ghost_check_time < check_interval_ghost:
                return

            self._last_ghost_check_time = now_ghost_check
            self.logger.debug("Performing ghost position check...")

            try:
                pos_info_ghost = self.exchange_api.get_position(getattr(self.config_module, 'SYMBOL_BYBIT', 'ETHUSDT'))

                min_meaningful_size_ghost = Decimal('0.000001')
                if self._market_info and self._market_info.get('limits',{}).get('amount',{}).get('min'):
                    min_sz_str_ghost = self._market_info['limits']['amount'].get('min')
                    if min_sz_str_ghost:
                        min_meaningful_size_ghost = safe_decimal(min_sz_str_ghost, min_meaningful_size_ghost)

                cur_pos_sz_on_exchange_ghost = safe_decimal(pos_info_ghost.get('contracts','0')) if pos_info_ghost else Decimal(0)

                if cur_pos_sz_on_exchange_ghost.copy_abs() >= min_meaningful_size_ghost:
                    pos_side_on_exchange_ghost = pos_info_ghost.get('side','unknown').lower()

                    ghost_msg_log = (f"👻 Ghost Position DETECTED! Bot state is FLAT, "
                                     f"but Exchange shows: {pos_side_on_exchange_ghost} {cur_pos_sz_on_exchange_ghost:.8f}. Attempting to close!")
                    self.logger.warning(ghost_msg_log)
                    if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                        self.send_telegram(ghost_msg_log)

                    try:
                        self.exchange_api.cancel_all_orders(getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'))
                        self.logger.info("Ghost check: Cancelled any existing orders for the symbol before attempting ghost close.")
                    except Exception as ce_ghost:
                        self.logger.error(f"Ghost check: Error cancelling orders before ghost close: {ce_ghost}")

                    close_side_for_ghost_pos = 'sell' if pos_side_on_exchange_ghost == 'long' else 'buy'

                    try:
                        ghost_close_order_resp_dict, ghost_close_qty_sent = self.exchange_api.place_market_order(
                            symbol_ccxt_param=getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'),
                            side=close_side_for_ghost_pos,
                            amount_param=float(cur_pos_sz_on_exchange_ghost.copy_abs()),
                            reduce_only=True
                        )

                        if ghost_close_order_resp_dict and 'id' in ghost_close_order_resp_dict:
                             self.logger.info(f"Ghost position closure order {ghost_close_order_resp_dict['id']} placed (Qty sent: {ghost_close_qty_sent}).")
                             if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                                 self.send_telegram(f"✅ Ghost position closure order sent (ID:{ghost_close_order_resp_dict['id']}).")
                             self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_after_ghost_min)
                        else:
                            self.logger.critical(f"CRITICAL: Ghost position closure command failed to get a valid order ID from exchange. "
                                                 f"Response: {ghost_close_order_resp_dict}, QtySent: {ghost_close_qty_sent}. Manual check REQUIRED!")
                            if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                                 self.send_telegram(f"🚨 CRITICAL ERROR\nFailed to get ID for ghost pos closure. Manual check required!")

                    except Exception as cle_ghost:
                        self.logger.critical(f"CRITICAL: Exception during ghost position closure attempt: {cle_ghost}", exc_info=True)
                        if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                             self.send_telegram(f"🚨 CRITICAL ERROR\nException closing ghost pos ({cle_ghost}). Manual intervention required!")
                else:
                    self.logger.debug("Ghost position check: No significant position found on exchange. Bot and exchange are in sync (flat).")

            except Exception as e_ghost_outer:
                self.logger.error(f"Ghost Position Check Error (Outer Try): {e_ghost_outer}", exc_info=True)

    def process_new_candle(self, candle_data_unused):
        self.send_status_report()

    def run_single_tick(self):
        global model_ready

        if self.is_training:
            self.logger.debug("Tick: Model is currently training. Skipping trading logic.")
            return

        try:
            if not self.active_trade.get('side') and not self._is_closing_trade:
                 self.check_and_manage_ghost_position()

            self.process_new_candle(None)

            if model_ready:
                if self.active_trade['side']:
                    self.monitor_active_trade()
                else:
                    cooldown_active_tick = False
                    cooldown_until_time_tick = self.active_trade.get('cooldown_until')
                    if cooldown_until_time_tick and get_utc_now() < cooldown_until_time_tick:
                        cooldown_active_tick = True

                    if not cooldown_active_tick:
                        self.check_entry_signals()
                    else:
                        if time.time() - getattr(self, '_last_cooldown_warning_time', 0) > 60:
                             self.logger.info(f"Tick: In cooldown. Remaining: {format_time_difference(get_utc_now(), cooldown_until_time_tick)}. Skipping entry check.")
                             self._last_cooldown_warning_time = time.time()
            else:
                if time.time() - getattr(self, '_last_model_not_ready_warning_time', 0) > 300:
                    self.logger.info("Tick: Model not ready. Skipping primary trade logic (monitor/entry). Will attempt retrain if scheduled.")
                    self._last_model_not_ready_warning_time = time.time()

            self.check_model_retrain_schedule()

        except Exception as e_tick_fatal:
            self.logger.critical(f"CRITICAL ERROR in strategy tick's main try-except block: {type(e_tick_fatal).__name__} - {e_tick_fatal}", exc_info=True)
            if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                self.send_telegram(f"🚨 CRITICAL BOT ERROR (Strategy Tick Level)\n`{type(e_tick_fatal).__name__}: {e_tick_fatal}`\nCheck logs immediately. Bot may be unstable.")

    def _is_trading_session_active(self):
        if not self.enable_time_filter or not self._parsed_trading_sessions:
            return True

        now_utc_time_obj = get_utc_now().time()

        for start_time_obj, end_time_obj in self._parsed_trading_sessions:
            if start_time_obj <= end_time_obj:
                if start_time_obj <= now_utc_time_obj <= end_time_obj:
                    return True
            else:
                if now_utc_time_obj >= start_time_obj or now_utc_time_obj <= end_time_obj:
                    return True
        return False


    def check_entry_signals(self):
        global model_ready, last_trade_factors_report_tg

        if not model_ready:
            if time.time() - getattr(self, '_last_model_not_ready_warning_time', 0) > 300:
                self.logger.info("check_entry_signals: Model not ready. Skipping entry signal check.")
                self._last_model_not_ready_warning_time = time.time()
            return

        if self.active_trade['side']:
            self.logger.debug("check_entry_signals: Trade already active. Skipping new entry check.")
            return

        cooldown_until = self.active_trade.get('cooldown_until')
        if cooldown_until and get_utc_now() < cooldown_until:
            if time.time() - getattr(self, '_last_cooldown_warning_time', 0) > 60:
                remaining_cooldown = format_time_difference(get_utc_now(), cooldown_until)
                self.logger.info(f"check_entry_signals: Still in cooldown ({remaining_cooldown} left). Skipping.")
                self._last_cooldown_warning_time = time.time()
            return

        if self.enable_time_filter:
            if not self._is_trading_session_active():
                if time.time() - getattr(self, '_last_time_filter_warning_time', 0) > 300:
                    self.logger.info(f"check_entry_signals: Outside defined trading hours (UTC time: {get_utc_now().strftime('%H:%M:%S')}). Skipping entry check.")
                    self._last_time_filter_warning_time = time.time()
                return

        current_features = self._get_orderbook_features()
        if not current_features or 'mid' not in current_features or current_features['mid'] <= 0:
            self.logger.debug("check_entry_signals: Could not get valid features or mid-price. Skipping.")
            return

        current_spread_pct = safe_decimal(current_features.get('spread_pct', Decimal('1.0')))
        if current_spread_pct > self.spread_limit_pct:
            if time.time() - getattr(self, '_last_spread_filter_warning_time', 0) > 60:
                self.logger.info(f"check_entry_signals: Trade REJECTED by Max Spread Filter. Current Spread: {current_spread_pct:.4%} > Limit: {self.spread_limit_pct:.4%}")
                self._last_spread_filter_warning_time = time.time()
            else:
                self.logger.debug(f"check_entry_signals: Spread filter active (Spread: {current_spread_pct:.4%}, Limit: {self.spread_limit_pct:.4%}). Silently skipping INFO log due to rate limit.")
            return


        entry_price_estimate_dec = safe_decimal(current_features['mid'])

        ml_signal, ml_confidence = self.predict_signal(current_features)
        self.logger.debug(f"check_entry_signals: ML Signal='{ml_signal}', Confidence={ml_confidence:.4f}")

        final_signal_to_act_on = None
        passed_all_filters = False

        filter_checks = {
            "ml_valid": (ml_signal is not None and ml_confidence >= self.min_confidence),
            "trend_match": True,
            "price_pos_match": True,
            "signal_stability": True
        }

        if not filter_checks["ml_valid"]:
            self.logger.debug(f"check_entry_signals: ML signal '{ml_signal}' (Conf: {ml_confidence:.3f}) below min_confidence {self.min_confidence} or signal is None. No trade.")
        else:
            if self.REQUIRE_TREND_MATCH:
                filter_checks["trend_match"] = False
                if len(self.recent_ml_p_up) < self.ml_trend_window_size:
                    self.logger.debug(f"check_entry_signals: Trend filter active, but not enough data in recent_ml_p_up ({len(self.recent_ml_p_up)}/{self.ml_trend_window_size}).")
                else:
                    avg_p_up_trend = sum(self.recent_ml_p_up) / len(self.recent_ml_p_up)
                    current_ml_trend = "neutral"
                    if avg_p_up_trend > getattr(self.config_module, 'ML_TREND_BULL_THRESH', 0.65): current_ml_trend = "bullish"
                    elif avg_p_up_trend < getattr(self.config_module, 'ML_TREND_BEAR_THRESH', 0.35): current_ml_trend = "bearish"

                    self.logger.debug(f"check_entry_signals: Trend Filter: ML Signal='{ml_signal}', Avg P_Up({self.ml_trend_window_size})={avg_p_up_trend:.3f}, Deduced Trend='{current_ml_trend}'")
                    if (ml_signal == 'buy' and current_ml_trend == 'bullish') or \
                       (ml_signal == 'sell' and current_ml_trend == 'bearish'):
                        filter_checks["trend_match"] = True
                    else:
                        self.logger.info(f"check_entry_signals: Trade REJECTED by Trend Filter. ML Signal: {ml_signal}, Deduced Trend: {current_ml_trend}")

            if self.REQUIRE_PRICE_POSITION_MATCH and filter_checks["trend_match"]:
                filter_checks["price_pos_match"] = False
                imb1_pricepos = safe_decimal(current_features.get('imb1', 0))
                imb2_pricepos = safe_decimal(current_features.get('imb2', 0))
                self.logger.debug(f"check_entry_signals: Price Position Filter: ML Signal='{ml_signal}', Imb1={imb1_pricepos:.3f}, Imb2={imb2_pricepos:.3f}")
                strong_imb1_thresh = getattr(self.config_module, 'PRICEPOS_STRONG_IMB1_THRESH', Decimal('0.30'))
                strong_imb2_thresh = getattr(self.config_module, 'PRICEPOS_STRONG_IMB2_THRESH', Decimal('0.20'))
                if ml_signal == 'buy' and imb1_pricepos > strong_imb1_thresh and imb2_pricepos > strong_imb2_thresh:
                    filter_checks["price_pos_match"] = True
                elif ml_signal == 'sell' and imb1_pricepos < -strong_imb1_thresh and imb2_pricepos < -strong_imb2_thresh:
                    filter_checks["price_pos_match"] = True
                else:
                    self.logger.info(f"check_entry_signals: Trade REJECTED by Price Position Filter. ML Signal: {ml_signal}, Imb1: {imb1_pricepos:.3f} (Th:{strong_imb1_thresh}), Imb2: {imb2_pricepos:.3f} (Th:{strong_imb2_thresh})")

            if self.REQUIRE_SIGNAL_STABILITY and filter_checks["trend_match"] and filter_checks["price_pos_match"]:
                filter_checks["signal_stability"] = False
                if ml_signal == self.monitor_state.get('last_signal'):
                    self.monitor_state['signal_stable_count'] += 1
                else:
                    self.monitor_state['last_signal'] = ml_signal
                    self.monitor_state['signal_stable_count'] = 1 if ml_signal else 0

                self.logger.debug(f"check_entry_signals: Stability Filter: Current Signal='{ml_signal}', Last Signal='{self.monitor_state.get('last_signal')}', Stable Count={self.monitor_state['signal_stable_count']}/{self.SIGNAL_STABILITY_COUNT}")
                if self.monitor_state['signal_stable_count'] >= self.SIGNAL_STABILITY_COUNT:
                    filter_checks["signal_stability"] = True
                else:
                    self.logger.info(f"check_entry_signals: Trade DEFERRED by Signal Stability Filter. Count: {self.monitor_state['signal_stable_count']}/{self.SIGNAL_STABILITY_COUNT}")

            if filter_checks["ml_valid"] and filter_checks["trend_match"] and filter_checks["price_pos_match"] and filter_checks["signal_stability"]:
                final_signal_to_act_on = ml_signal
                passed_all_filters = True
                self.logger.info(f"check_entry_signals: All filters PASSED. Final Signal: {final_signal_to_act_on}, Confidence: {ml_confidence:.4f}")

        overall_status_msg_part_report = ""
        if final_signal_to_act_on: overall_status_msg_part_report = f"*Signal: {final_signal_to_act_on.upper()} (CONFIRMED)*"
        elif not filter_checks["ml_valid"]: overall_status_msg_part_report = f"*Signal: {ml_signal or 'None'} (ML REJECT - Conf {ml_confidence:.2f} < {self.min_confidence})*"
        elif not filter_checks["trend_match"]: overall_status_msg_part_report = "*Signal: (Trend Filter REJECT)*"
        elif not filter_checks["price_pos_match"]: overall_status_msg_part_report = "*Signal: (Price Pos Filter REJECT)*"
        elif not filter_checks["signal_stability"]: overall_status_msg_part_report = "*Signal: (Stability Filter DEFER)*"
        else: overall_status_msg_part_report = "*Signal: None (No Trade)*"

        imb1_rep_val = safe_decimal(current_features.get('imb1', 0))
        imb2_rep_val = safe_decimal(current_features.get('imb2', 0))
        spread_pct_rep_val = safe_decimal(current_features.get('spread_pct', 0)) * 100
        volatility_rep_val = safe_decimal(current_features.get('volatility_10', 0)) * 100
        momentum_rep_val = safe_decimal(current_features.get('momentum_5', 0)) * 100
        total_vol_top_rep_val = safe_decimal(current_features.get('total_vol_top',0))

        factors_report_lines = [
            f"*{self.STRATEGY_NAME} - Trade Factors Evaluation*",
            f"*Overall Status:* {overall_status_msg_part_report}",
            f"  • Est. Entry Price: `${format_price(entry_price_estimate_dec)}`",
            f"  • ML Signal: `{ml_signal or 'None'}` (Conf: `{ml_confidence:.3f}`)",
            f"  • Features: Imb1=`{imb1_rep_val:.3f}`, Imb2=`{imb2_rep_val:.3f}`, Spread=`{spread_pct_rep_val:.3f}%`",
            f"              Vol10=`{volatility_rep_val:.3f}%`, Mom5=`{momentum_rep_val:+.3f}%`, TopVol=`{total_vol_top_rep_val:.2f}`"
        ]

        if self.REQUIRE_TREND_MATCH:
             avg_p_up_rep_val = sum(self.recent_ml_p_up) / len(self.recent_ml_p_up) if self.recent_ml_p_up else -1.0
             trend_status_rep_val = "Pass" if filter_checks["trend_match"] else "Fail/NotMet"
             factors_report_lines.append(f"  • Trend Filter: `{trend_status_rep_val}` (AvgPUp: `{avg_p_up_rep_val:.3f}`)")
        if self.REQUIRE_PRICE_POSITION_MATCH:
             price_pos_status_rep_val = "Pass" if filter_checks["price_pos_match"] else "Fail/NotMet"
             factors_report_lines.append(f"  • Price Pos Filter: `{price_pos_status_rep_val}`")
        if self.REQUIRE_SIGNAL_STABILITY:
             stability_status_rep_val = "Pass" if filter_checks["signal_stability"] else "Fail/Defer"
             factors_report_lines.append(f"  • Stability Filter: `{stability_status_rep_val}` (Count: `{self.monitor_state['signal_stable_count']}`)")

        last_trade_factors_report_tg = "\n".join(factors_report_lines)

        if final_signal_to_act_on: self.logger.info(f"Trade Factors Summary (Leading to trade):\n{last_trade_factors_report_tg}")
        else: self.logger.debug(f"Trade Factors Summary (No trade):\n{last_trade_factors_report_tg}")

        send_factors_to_tg_this_time = False
        if final_signal_to_act_on: send_factors_to_tg_this_time = True
        elif not filter_checks["ml_valid"] and ml_signal is not None : send_factors_to_tg_this_time = True
        elif self.REQUIRE_TREND_MATCH and not filter_checks["trend_match"] and filter_checks["ml_valid"]: send_factors_to_tg_this_time = True
        elif self.REQUIRE_PRICE_POSITION_MATCH and not filter_checks["price_pos_match"] and filter_checks["ml_valid"] and filter_checks["trend_match"]: send_factors_to_tg_this_time = True

        if send_factors_to_tg_this_time and hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
             report_interval_no_trade = getattr(self.config_module, 'TRADE_FACTOR_TG_REPORT_INTERVAL_SECONDS', 300)
             if final_signal_to_act_on or \
                (time.time() - getattr(self, '_last_tg_factors_report_time_no_trade', 0) > report_interval_no_trade) :
                 self.send_telegram(last_trade_factors_report_tg)
                 if not final_signal_to_act_on: self._last_tg_factors_report_time_no_trade = time.time()

        if final_signal_to_act_on and passed_all_filters:
            available_balance_dec = self.exchange_api.get_usdt_available_balance()
            min_bal_thresh = getattr(self.config_module, 'MIN_BALANCE_THRESHOLD', Decimal('10.0'))
            if available_balance_dec is None or available_balance_dec < min_bal_thresh:
                if time.time() - getattr(self, '_last_balance_warning_time', 0) > 300:
                    self.logger.warning(f"check_entry_signals: Insufficient balance ({available_balance_dec}) or fetch error. Min threshold: {min_bal_thresh}. Cannot trade.")
                    if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                        self.send_telegram(f"⚠️ Insufficient balance (`{available_balance_dec if available_balance_dec is not None else 'Error'}`) to trade. Min: `${min_bal_thresh}`.")
                    self._last_balance_warning_time = time.time()
                return

            sl_pct_trade, tp_pct_trade, sl_price_trade, tp_price_trade = self.calculate_dynamic_tp_sl(
                features_dict=current_features, signal=final_signal_to_act_on,
                confidence=ml_confidence, entry_price_dec=entry_price_estimate_dec
            )

            if not (sl_price_trade and tp_price_trade):
                self.logger.error("check_entry_signals: Failed to calculate valid SL/TP prices. Cannot trade.")
                return

            position_size_trade = self.calculate_dynamic_position_size(
                entry_price_dec=entry_price_estimate_dec, sl_price_dec=sl_price_trade,
                available_balance_dec=available_balance_dec
            )

            if position_size_trade <= Decimal(0):
                self.logger.error(f"check_entry_signals: Calculated position size is zero or invalid ({position_size_trade}). Cannot trade.")
                return

            trade_executed = self.execute_trade(
                signal=final_signal_to_act_on, size_dec=position_size_trade,
                entry_price_dec_estimate=entry_price_estimate_dec,
                sl_pct_dec=sl_pct_trade, tp_pct_dec=tp_pct_trade,
                sl_price_dec=sl_price_trade, tp_price_dec=tp_price_trade,
                confidence=ml_confidence
            )

            if trade_executed:
                self.logger.info(f"Trade for {final_signal_to_act_on.upper()} initiated successfully via check_entry_signals.")
                self.monitor_state['last_signal'] = None
                self.monitor_state['signal_stable_count'] = 0
            else:
                self.logger.error(f"Trade execution for {final_signal_to_act_on.upper()} FAILED from check_entry_signals.")
                self.active_trade['cooldown_until'] = get_utc_now() + timedelta(minutes=self.cooldown_minutes / 2.0)
        elif ml_signal and not passed_all_filters :
             self.logger.info(f"Signal '{ml_signal}' (Conf: {ml_confidence:.3f}) did not pass all entry filters. No trade this tick.")

    def check_model_retrain_schedule(self):
        global model_ready
        if self.is_training: return

        now_utc_retrain_check = get_utc_now()
        if self._last_retrain_check_time is None:
            self._last_retrain_check_time = now_utc_retrain_check - timedelta(minutes=self.model_retrain_check_interval_min + 1)

        if self.model_w is not None and self.model_train_time is not None:
            if now_utc_retrain_check - self._last_retrain_check_time >= timedelta(minutes=self.model_retrain_check_interval_min):
                self._last_retrain_check_time = now_utc_retrain_check

                model_age_current_days = (now_utc_retrain_check - self.model_train_time).total_seconds() / (24 * 3600)
                if model_age_current_days > self.model_retrain_days_limit:
                     self.logger.info(f"Model age ({model_age_current_days:.1f}d) exceeds limit ({self.model_retrain_days_limit}d). Triggering scheduled retraining...")
                     if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                         self.send_telegram(f"⏳ Model age ({model_age_current_days:.1f}d) exceeds limit ({self.model_retrain_days_limit}d). Initiating retraining...")
                     try: self.train_model()
                     except Exception as train_e_schedule:
                          self.logger.error(f"Scheduled retraining call failed: {train_e_schedule}", exc_info=True)
                          if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                              self.send_telegram(f"❌ Scheduled retraining process failed: {train_e_schedule}")

        elif not model_ready:
            retry_interval_not_ready = timedelta(minutes=self.model_retry_train_interval_min)
            if now_utc_retrain_check - self._last_retrain_check_time >= retry_interval_not_ready:
                self.logger.info("Model is not ready and not currently training. Attempting to train model now (retry schedule)...")
                if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                     self.send_telegram(f"ℹ️ Model not ready. Attempting periodic data collection/training cycle.")
                self._last_retrain_check_time = now_utc_retrain_check
                try:
                    self.train_model()
                except Exception as train_e_retry_not_ready:
                    self.logger.error(f"Periodic model training attempt (due to not ready) failed: {train_e_retry_not_ready}", exc_info=True)
                    if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                          self.send_telegram(f"❌ Periodic model training attempt (model not ready) failed: {train_e_retry_not_ready}")

    def send_status_report(self):
        global model_ready, last_trade_factors_report_tg

        current_unix_time = time.time()
        if current_unix_time - self._last_status_report_sent_time < self.status_report_interval:
            return

        self._last_status_report_sent_time = current_unix_time

        if not (hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG and self.exchange_api):
            return

        try:
            pos_info_status = self.exchange_api.get_position(getattr(self.config_module, 'SYMBOL_BYBIT', 'ETHUSDT'))
            balance_decimal_status = self.exchange_api.get_usdt_available_balance()
            balance_str_status = f"${balance_decimal_status:.2f}" if isinstance(balance_decimal_status, (Decimal,float,int)) else "Error/None"

            ticker_status = self.exchange_api.fetch_ticker(getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'))
            current_price_status = safe_decimal(ticker_status.get('last')) if ticker_status and ticker_status.get('last') else None

            bot_start_time_from_config = getattr(self.config_module, 'BOT_START_TIME', None)
            uptime_str_status = format_time_difference(bot_start_time_from_config) if bot_start_time_from_config else "N/A"

            model_status_display = "❌ Not Trained"; model_age_display = ""; model_accuracy_display = ""
            if self.model_w is not None and self.model_train_time is not None:
                is_model_fresh_for_display = (get_utc_now() - self.model_train_time).total_seconds() / (24 * 3600) <= self.model_retrain_days_limit

                if model_ready and is_model_fresh_for_display: model_status_display = "✅ Ready"
                elif self.is_training: model_status_display = "⏳ Training"
                elif not is_model_fresh_for_display: model_status_display = "⚠️ Stale (Needs Retrain)"
                else: model_status_display = "❓ Not Ready (Check Logs)"

                model_age_minutes_display = int((get_utc_now() - self.model_train_time).total_seconds() / 60)
                model_age_display = f" ({model_age_minutes_display}m ago)"
                accuracy_val = self._last_train_accuracy
                if accuracy_val and isinstance(accuracy_val, (float, int)): model_accuracy_display = f", Acc:{accuracy_val:.3f}"
            elif self.is_training: model_status_display = "⏳ Initial Training"

            model_full_status_display = f"{model_status_display}{model_age_display}{model_accuracy_display}"

            num_total_trades = len(self.trade_history)
            perf_summary_display_str = "Trades: `0`"
            if num_total_trades > 0:
                wins_count = sum(1 for t in self.trade_history if t.get('pnl_pct', Decimal(0)) > 0)
                losses_count = num_total_trades - wins_count
                win_rate_val = (wins_count / num_total_trades * 100) if num_total_trades > 0 else 0
                total_pnl_pct_sum = sum(t.get('pnl_pct', Decimal(0)) for t in self.trade_history)
                avg_pnl_pct_val = (total_pnl_pct_sum / num_total_trades) if num_total_trades > 0 else Decimal(0)
                total_pnl_usd_sum = sum(t.get('pnl_usd', Decimal(0)) for t in self.trade_history)
                perf_summary_display_str = (f"Trades:`{num_total_trades}` W:`{wins_count}` L:`{losses_count}` WR:`{win_rate_val:.1f}%`\n"
                                            f"PnL Sum:`{total_pnl_pct_sum:+.3%}` (`${total_pnl_usd_sum:+.2f}`)\n"
                                            f"Avg PnL:`{avg_pnl_pct_val:+.3%}`")

            position_summary_display_str = "Status: `Idle`";
            active_trade_info_status = self.active_trade
            if active_trade_info_status.get('side'):
                side_disp = active_trade_info_status['side']
                entry_px_disp = active_trade_info_status['entry_price']
                size_disp = active_trade_info_status['size']
                sl_px_disp = active_trade_info_status['sl_price']
                tp_px_disp = active_trade_info_status['tp_price']
                entry_ts_disp = active_trade_info_status['timestamp']
                eoid_disp = active_trade_info_status.get('entry_order_id', 'N/A')
                pos_duration_disp_str = format_time_difference(entry_ts_disp) if entry_ts_disp else "N/A"

                current_price_for_active_pos_pnl = current_price_status
                leverage_disp_val = getattr(self.config_module, 'LEVERAGE', 1)
                liq_price_disp_val = Decimal('0')
                position_value_disp_val = Decimal('0')

                if pos_info_status and pos_info_status.get('contracts') and safe_decimal(pos_info_status.get('contracts')) > 0 :
                    live_mark_price_from_pos = safe_decimal(pos_info_status.get('markPrice'))
                    if live_mark_price_from_pos and live_mark_price_from_pos > 0:
                        current_price_for_active_pos_pnl = live_mark_price_from_pos
                    leverage_disp_val = safe_decimal(pos_info_status.get('leverage', leverage_disp_val))
                    liq_price_disp_val = safe_decimal(pos_info_status.get('liquidationPrice', '0'))
                    position_value_disp_val = safe_decimal(pos_info_status.get('positionValue')) \
                        if pos_info_status.get('positionValue') else (size_disp * current_price_for_active_pos_pnl if size_disp and current_price_for_active_pos_pnl else Decimal(0))
                elif entry_px_disp and size_disp:
                     position_value_disp_val = size_disp * (current_price_status if current_price_status and current_price_status > 0 else entry_px_disp)

                current_pnl_pct_active_pos = Decimal(0); roi_active_pos_val = Decimal(0)
                if entry_px_disp and current_price_for_active_pos_pnl and entry_px_disp > 0 and size_disp and size_disp > 0 :
                     current_pnl_pct_active_pos = ((current_price_for_active_pos_pnl - entry_px_disp) / entry_px_disp) if side_disp == 'buy' else ((entry_px_disp - current_price_for_active_pos_pnl) / entry_px_disp);
                     if leverage_disp_val > 0: roi_active_pos_val = current_pnl_pct_active_pos * leverage_disp_val

                pnl_display_active_pos_str = f"`{current_pnl_pct_active_pos:+.3%}` (@`${format_price(current_price_for_active_pos_pnl)}`)" if current_price_for_active_pos_pnl else "`N/A`"
                roi_display_active_pos_str = f"`{roi_active_pos_val:+.2%}`" if leverage_disp_val > 0 else "`N/A`"
                pos_emoji_status_char = "🟢" if side_disp == "buy" else "🔴"

                position_summary_display_str = (f"*Status: {pos_emoji_status_char} Active {side_disp.upper()}* ID:`{eoid_disp}`\n"
                                                f"• Entry:`${format_price(entry_px_disp)}` (`{pos_duration_disp_str}` ago)\n"
                                                f"• Size:`{size_disp:.6f}` (Val:`${format_price(position_value_disp_val)}`)\n"
                                                f"• Lvg:`{leverage_disp_val:.1f}x` | Liq:`${format_price(liq_price_disp_val)}`\n"
                                                f"• SL:`${format_price(sl_px_disp)}` | TP:`${format_price(tp_px_disp)}`\n"
                                                f"• PnL:{pnl_display_active_pos_str} | ROI:{roi_display_active_pos_str}")

            diagnostics_lines_status_list = ["*⚙️ Diagnostics & Last Signal:*"]
            last_signal_monitor_state = self.monitor_state.get('last_signal')
            last_conf_monitor_state = self.monitor_state.get('last_confidence', 0.0)
            last_stability_monitor_state = self.monitor_state.get('signal_stable_count', 0)
            diagnostics_lines_status_list.append(f"• Sig (Entry Check):`{last_signal_monitor_state or 'None'}`(Conf:`{last_conf_monitor_state:.2f}`,St:`{last_stability_monitor_state}`)")

            if last_trade_factors_report_tg:
                summary_line_from_factors_report = next((line for line in last_trade_factors_report_tg.split('\n') if line.startswith('*Overall Status:*')), None)
                if summary_line_from_factors_report:
                    extracted_factor_status = summary_line_from_factors_report.split(':* ', 1)[-1] if ':* ' in summary_line_from_factors_report else "Factors N/A"
                    diagnostics_lines_status_list.append(f"• Last Factors:`{extracted_factor_status}`")

            cooldown_active_display_str = "No";
            cooldown_expiry_time_status = self.active_trade.get('cooldown_until')
            if cooldown_expiry_time_status and get_utc_now() < cooldown_expiry_time_status:
                remaining_cooldown_disp_str = format_time_difference(get_utc_now(), cooldown_expiry_time_status);
                cooldown_active_display_str = f"Yes ({remaining_cooldown_disp_str})"
            diagnostics_lines_status_list.append(f"• Cooldown:`{cooldown_active_display_str}`")

            recent_trades_display_lines_list = ["*📜 Recent Trades (Last 5):*"]
            if self.trade_history:
                for trade_hist_item in reversed(self.trade_history[-5:]):
                    side_hist_item = trade_hist_item.get('side','?').upper()[:4]
                    pnl_pct_hist_item = trade_hist_item.get('pnl_pct', Decimal(0))
                    pnl_usd_hist_item = trade_hist_item.get('pnl_usd', Decimal(0))
                    entry_f_hist_item = format_price(trade_hist_item.get('entry', Decimal(0)))
                    exit_f_hist_item = format_price(trade_hist_item.get('exit', Decimal(0)))
                    try:
                        entry_time_iso_hist = trade_hist_item.get('time')
                        entry_time_dt_hist = datetime.fromisoformat(entry_time_iso_hist.replace('Z', '+00:00')) if entry_time_iso_hist else None
                        timestamp_str_hist_item = entry_time_dt_hist.strftime('%m-%d %H:%M') if entry_time_dt_hist else "??"
                    except: timestamp_str_hist_item = "??"
                    reason_hist_item = trade_hist_item.get('reason','?')[:10]
                    duration_s_hist_item = trade_hist_item.get('duration_secs','')
                    duration_s_display_item_str = f"({duration_s_hist_item}s)" if isinstance(duration_s_hist_item, int) else ""
                    recent_trades_display_lines_list.append(
                        f"• `{timestamp_str_hist_item}` {side_hist_item} ${entry_f_hist_item}→${exit_f_hist_item} = *`{pnl_pct_hist_item:+.2%}`* (`${pnl_usd_hist_item:+.2f}`) {duration_s_display_item_str} `{reason_hist_item}`"
                    )
            else:
                recent_trades_display_lines_list.append("• _No trades logged yet_")

            status_header_emoji = "⚪️"
            if active_trade_info_status.get('side') == 'buy': status_header_emoji = "🟢"
            elif active_trade_info_status.get('side') == 'sell': status_header_emoji = "🔴"
            if self.is_training: status_header_emoji = "⏳"

            status_msg_parts_final_list = [
                f"*{status_header_emoji} {self.STRATEGY_NAME} Status*",
                f"Uptime:`{uptime_str_status}` | Bal:`{balance_str_status}` | Mkt:`{getattr(self.config_module, 'SYMBOL_BYBIT', 'N/A')}`",
                f"Model:`{model_full_status_display}`",
                "",
                perf_summary_display_str,
                "",
                position_summary_display_str,
                "",
                "\n".join(diagnostics_lines_status_list),
                "",
                "\n".join(recent_trades_display_lines_list)
            ]
            status_msg_to_send = "\n".join(status_msg_parts_final_list)

            current_msg_hash = hashlib.md5(status_msg_to_send.encode()).hexdigest()
            if current_msg_hash != self._last_status_msg_hash:
                self.send_telegram(status_msg_to_send)
                self._last_status_msg_hash = current_msg_hash
                self.logger.info("Status report sent to Telegram (content changed).")
            else:
                self.logger.debug("Status report content unchanged. Not sent to Telegram.")

        except Exception as e_status_report_gen:
            self.logger.error(f"Error generating or sending status report: {type(e_status_report_gen).__name__} - {e_status_report_gen}", exc_info=True)

    def report_trade_factors(self, signal, confidence, features):
        global last_trade_factors_report_tg
        self.logger.debug("report_trade_factors: Note: Primary factor reporting is now within check_entry_signals. Last known factors report string will be used by send_status_report.")

    def shutdown(self):
        self.logger.info(f"--- Strategy {self.STRATEGY_NAME} Shutdown Sequence Initiated ---")
        active_trade_info_shutdown = self.active_trade.copy()
        active_side_shutdown = active_trade_info_shutdown.get('side')

        if active_side_shutdown:
            self.logger.warning(f"Closing active {active_side_shutdown.upper()} position due to shutdown request...")
            exit_price_shutdown = None
            try:
                ticker_shutdown = self.exchange_api.fetch_ticker(getattr(self.config_module, 'SYMBOL_CCXT', 'ETH/USDT'))
                if ticker_shutdown and ticker_shutdown.get('last'):
                    exit_price_shutdown = safe_decimal(ticker_shutdown['last'])
                    self.logger.info(f"Using current ticker last price ${format_price(exit_price_shutdown)} for shutdown closure.")
            except Exception as e_tick_fetch_shutdown:
                self.logger.error(f"Ticker fetch error on shutdown: {e_tick_fetch_shutdown}")

            if not exit_price_shutdown :
                try:
                    pos_info_at_shutdown = self.exchange_api.get_position(getattr(self.config_module, 'SYMBOL_BYBIT', 'ETHUSDT'))
                    if pos_info_at_shutdown and pos_info_at_shutdown.get('markPrice'):
                        exit_price_shutdown = safe_decimal(pos_info_at_shutdown.get('markPrice'))
                        self.logger.info(f"Using current position mark price ${format_price(exit_price_shutdown)} for shutdown closure.")
                except Exception as e_pos_fetch_shutdown_err:
                    self.logger.error(f"Position mark price fetch error on shutdown: {e_pos_fetch_shutdown_err}")

            if not exit_price_shutdown:
                exit_price_shutdown = active_trade_info_shutdown.get('entry_price')
                self.logger.warning(f"Using original entry price ${format_price(exit_price_shutdown)} for shutdown closure as current price couldn't be reliably fetched.")

            if not self.close_active_trade(exit_price=exit_price_shutdown, reason='BotShutdown'):
                 self.logger.critical("CRITICAL: Failed to close active position during shutdown! Manual intervention REQUIRED on exchange!")
                 if hasattr(self.config_module, 'ENABLE_TG') and self.config_module.ENABLE_TG:
                     try: self.send_telegram("🚨 CRITICAL ERROR\nFailed to close active position during bot shutdown. MANUAL CHECK REQUIRED on exchange!")
                     except: pass
            else:
                self.logger.info("✔ Active position successfully closed due to shutdown.")
                time.sleep(2)
        else:
            self.logger.info("No active position to close during shutdown.")

        try:
            timestamp_shutdown_str = get_utc_now().strftime('%Y%m%d_%H%M%S')
            history_file_name_shutdown = f"trade_history_{self.STRATEGY_NAME}_shutdown_{timestamp_shutdown_str}.pkl"
            with open(history_file_name_shutdown, 'wb') as f_hist:
                pickle.dump(self.trade_history, f_hist)
            self.logger.info(f"Trade history ( {len(self.trade_history)} trades) saved to {history_file_name_shutdown}")
        except Exception as e_save_hist_shutdown:
            self.logger.error(f"Failed to save trade history during shutdown: {e_save_hist_shutdown}", exc_info=True)

        self.logger.info(f"--- Strategy {self.STRATEGY_NAME} Shutdown Complete ---")

# --- END OF FILE strategy.py ---