import logging
import pandas as pd

from DataCache_v3 import DataCache
from indicators import Indicators
from trade import Trades
import datetime as dt
import json
import traceback
from typing import Any

# Configure logging
logger = logging.getLogger(__name__)

class StrategyInstance:
    def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str,
                 user_id: int, generated_code: str, data_cache: Any, indicators: Any | None, trades: Any | None,
                 edm_client: Any = None, indicator_owner_id: int = None):
        """
        Initializes a StrategyInstance.

        :param strategy_instance_id: Unique identifier for this strategy execution instance.
        :param strategy_id: Identifier of the strategy definition.
        :param strategy_name: Name of the strategy.
        :param user_id: ID of the user who owns the strategy.
        :param generated_code: The generated 'next()' method code.
        :param data_cache: Reference to the DataCache instance.
        :param indicators: Reference to the Indicators manager.
        :param trades: Reference to the Trades manager.
        :param edm_client: Reference to the EDM client for candle data.
        :param indicator_owner_id: For subscribed strategies, the creator's user ID for indicator lookup.
                                   If None, uses user_id.
        """
        # Initialize the backtrader_strategy attribute
        self.backtrader_strategy = None  # Will be set by Backtrader's MappedStrategy

        self.strategy_instance_id = strategy_instance_id
        self.strategy_id = strategy_id
        self.strategy_name = strategy_name
        self.user_id = user_id
        self.generated_code = generated_code
        self.data_cache = data_cache
        self.indicators = indicators
        self.trades = trades
        self.edm_client = edm_client

        # For subscribed strategies, indicator lookup uses the creator's indicators
        self.indicator_owner_id = indicator_owner_id if indicator_owner_id is not None else user_id

        # Initialize context variables
        self.flags: dict[str, Any] = {}
        self.variables: dict[str, Any] = {}
        self.starting_balance: float = 0.0
        self.current_balance: float = 0.0
        self.available_balance: float = 0.0
        self.available_strategy_balance: float = 0.0
        self.profit_loss: float = 0.0
        self.active: bool = True
        self.paused: bool = False
        self.exit: bool = False
        self.exit_method: str = 'all'
        self.start_time = dt.datetime.now()
        self.trade_history = []  # List to store trade details (not the Trades manager)
        self.orders = []  # List to store order details
        self.profit_loss = 0.0  # Total P&L
        self.statistics = {
            'total_return': 0.0,
            'sharpe_ratio': 0.0,
            'sortino_ratio': 0.0,
            'max_drawdown': 0.0,
            'profit_factor': 0.0,
            'win_rate': 0.0,
            'loss_rate': 0.0,
        }
        # Define the local execution environment
        self.exec_context = {
            'flags': self.flags,
            'variables': self.variables,
            'exit': self.exit,
            'paused': self.paused,
            'strategy_id': self.strategy_id,
            'user_id': self.user_id,
            'get_last_candle': self.get_last_candle,
            'get_current_price': self.get_current_price,
            'trade_order': self.trade_order,
            'exit_strategy': self.exit_strategy,
            'notify_user': self.notify_user,
            'process_indicator': self.process_indicator,
            'process_signal': self.process_signal,
            'process_formation': self.process_formation,
            'get_strategy_profit_loss': self.get_strategy_profit_loss,
            'is_in_profit': self.is_in_profit,
            'is_in_loss': self.is_in_loss,
            'get_active_trades': self.get_active_trades,
            'get_starting_balance': self.get_starting_balance,
            'set_paused': self.set_paused,
            'set_exit': self.set_exit,
            'set_available_strategy_balance': self.set_available_strategy_balance,
            'get_current_balance': self.get_current_balance,
            'get_available_balance': self.get_available_balance,
            'get_available_strategy_balance': self.get_available_strategy_balance,
            'starting_balance': self.starting_balance,
            'current_balance': self.current_balance,
            'available_balance': self.available_balance,
            'available_strategy_balance': self.available_strategy_balance,
        }

        # Automatically load or initialize the context
        self._initialize_or_load_context()

    def _initialize_or_load_context(self):
        """
        Checks if a context exists for the strategy instance. If it does, load it;
        otherwise, initialize a new context.
        """
        try:
            # Fetch the context data once
            context_data = self.data_cache.get_rows_from_datacache(
                cache_name='strategy_contexts',
                filter_vals=[('strategy_instance_id', self.strategy_instance_id)]
            )

            if context_data.empty:
                self.initialize_new_context()
                logger.debug(f"Initialized new context for StrategyInstance '{self.strategy_instance_id}'.")
            else:
                self.load_context(context_data)
                logger.debug(f"Loaded existing context for StrategyInstance '{self.strategy_instance_id}'.")
        except Exception as e:
            logger.error(f"Error during initialization of StrategyInstance '{self.strategy_instance_id}': {e}",
                         exc_info=True)
            traceback.print_exc()

    def initialize_new_context(self):
        """
        Initializes a new context for the strategy instance and saves it to the cache.
        """
        self.flags = {}
        self.variables = {}
        self.profit_loss = 0.0
        self.active = True
        self.paused = False
        self.exit = False
        self.exit_method = 'all'
        self.start_time = dt.datetime.now(dt.timezone.utc)

        # Initialize balance attributes
        self.starting_balance = self.fetch_user_balance()
        self.current_balance = self.starting_balance
        self.available_balance = self.calculate_available_balance()
        self.available_strategy_balance = self.starting_balance

        # Update exec_context with new balance attributes
        self.exec_context['starting_balance'] = self.starting_balance
        self.exec_context['current_balance'] = self.current_balance
        self.exec_context['available_balance'] = self.available_balance
        self.exec_context['available_strategy_balance'] = self.available_strategy_balance

        # Insert initial context into the cache
        self.insert_context()
        logger.debug(f"New context created and inserted for StrategyInstance '{self.strategy_instance_id}'.")

    def load_context(self, context_data: pd.DataFrame):
        """
        Loads the strategy execution context from the provided context_data.
        """
        try:
            context = context_data.iloc[0].to_dict()
            self.flags = json.loads(context.get('flags', '{}'))
            self.variables = json.loads(context.get('variables', '{}'))
            self.profit_loss = context.get('profit_loss', 0.0)
            self.active = bool(context.get('active', 1))
            self.paused = bool(context.get('paused', 0))
            self.exit = bool(context.get('exit', 0))
            self.exit_method = context.get('exit_method', 'all')
            self.starting_balance = context.get('starting_balance', 0.0)
            self.current_balance = context.get('current_balance', self.starting_balance)
            self.available_balance = context.get('available_balance', self.current_balance)
            self.available_strategy_balance = context.get('available_strategy_balance', self.starting_balance)

            start_time_str = context.get('start_time')
            if start_time_str:
                self.start_time = dt.datetime.fromisoformat(start_time_str).replace(tzinfo=dt.timezone.utc)

            # Update exec_context with loaded flags, variables, and balance attributes
            self.exec_context['flags'] = self.flags
            self.exec_context['variables'] = self.variables
            self.exec_context['profit_loss'] = self.profit_loss
            self.exec_context['active'] = self.active
            self.exec_context['paused'] = self.paused
            self.exec_context['exit'] = self.exit
            self.exec_context['exit_method'] = self.exit_method
            self.exec_context['starting_balance'] = self.starting_balance
            self.exec_context['current_balance'] = self.current_balance
            self.exec_context['available_balance'] = self.available_balance
            self.exec_context['available_strategy_balance'] = self.available_strategy_balance

            logger.debug(f"Context loaded for StrategyInstance '{self.strategy_instance_id}'.")
        except Exception as e:
            logger.error(f"Error loading context for StrategyInstance '{self.strategy_instance_id}': {e}",
                         exc_info=True)
            traceback.print_exc()

    def insert_context(self):
        """
        Inserts a new context into the cache and database.
        """
        try:
            columns = (
                "strategy_instance_id", "flags", "variables", "profit_loss",
                "active", "paused", "exit", "exit_method", "start_time",
                "starting_balance", "current_balance", "available_balance", "available_strategy_balance"
            )
            values = (
                self.strategy_instance_id,
                json.dumps(self.flags),
                json.dumps(self.variables),
                self.profit_loss,
                int(self.active),
                int(self.paused),
                int(self.exit),
                self.exit_method,
                self.start_time.isoformat(),
                self.starting_balance,
                self.current_balance,
                self.available_balance,
                self.available_strategy_balance
            )

            # Insert the new context without passing 'key' to avoid adding 'tbl_key'
            self.data_cache.insert_row_into_datacache(
                cache_name='strategy_contexts',
                columns=columns,
                values=values
                # key=None is implicit
            )
            logger.debug(f"Context inserted for StrategyInstance '{self.strategy_instance_id}'.")
        except ValueError as ve:
            logger.error(f"ValueError in inserting context for '{self.strategy_instance_id}': {ve}")
            traceback.print_exc()
        except Exception as e:
            logger.error(f"Error inserting context for '{self.strategy_instance_id}': {e}")
            traceback.print_exc()

    def save_context(self):
        """
        Saves the current strategy execution context to the cache and database.
        Determines whether to insert a new row or modify an existing one.
        """
        try:
            # Check if the context exists
            existing_context = self.data_cache.get_rows_from_datacache(
                cache_name='strategy_contexts',
                filter_vals=[('strategy_instance_id', self.strategy_instance_id)]
            )

            columns = (
                "strategy_instance_id", "flags", "variables", "profit_loss",
                "active", "paused", "exit", "exit_method", "start_time",
                "starting_balance", "current_balance", "available_balance", "available_strategy_balance"
            )
            values = (
                self.strategy_instance_id,
                json.dumps(self.flags),
                json.dumps(self.variables),
                self.profit_loss,
                int(self.active),
                int(self.paused),
                int(self.exit),
                self.exit_method,
                self.start_time.isoformat(),
                self.starting_balance,
                self.current_balance,
                self.available_balance,
                self.available_strategy_balance
            )

            if existing_context.empty:
                # Insert a new context since it doesn't exist
                self.insert_context()
                logger.debug(f"Inserted new context for StrategyInstance '{self.strategy_instance_id}'.")
            else:
                # Modify the existing context without passing 'key'
                self.data_cache.modify_datacache_item(
                    cache_name='strategy_contexts',
                    filter_vals=[('strategy_instance_id', self.strategy_instance_id)],
                    field_names=columns,
                    new_values=values,
                    overwrite='strategy_instance_id'  # Ensures uniqueness
                    # Do not pass 'key'
                )
                logger.debug(f"Modified existing context for StrategyInstance '{self.strategy_instance_id}'.")
        except ValueError as ve:
            logger.error(f"ValueError in saving context for '{self.strategy_instance_id}': {ve}")
            traceback.print_exc()
        except Exception as e:
            logger.error(f"Error saving context for '{self.strategy_instance_id}': {e}")
            traceback.print_exc()

    def override_exec_context(self, key: str, value: Any):
        """
        Overrides a specific mapping in the execution context with a different method or variable.

        :param key: The key in exec_context to override.
        :param value: The new method or value to assign.
        """
        self.exec_context[key] = value
        logger.debug(f"Overridden exec_context key '{key}' with new value '{value}'.")

    def tick(self, candle_data: dict = None) -> list:
        """
        Process one iteration of the strategy on a price tick.

        This method is called by the execution loop when new price data arrives.
        It updates prices, processes the strategy logic, and returns any events.

        :param candle_data: Optional candle data dict with 'symbol', 'close', etc.
        :return: List of events (orders, fills, errors, etc.)
        """
        events = []

        # Skip if strategy is paused or exiting
        if self.paused:
            return [{'type': 'skipped', 'reason': 'paused'}]

        if self.exit:
            return [{'type': 'skipped', 'reason': 'exiting'}]

        try:
            # Update current candle data in exec context if provided
            if candle_data:
                self.exec_context['current_candle'] = candle_data
                self.exec_context['current_price'] = candle_data.get('close')
                self.exec_context['current_symbol'] = candle_data.get('symbol', 'BTC/USDT')

            # Execute the strategy's next() method
            result = self.execute()

            if result.get('success'):
                # Collect any events generated during execution
                if '_events' in self.exec_context:
                    events.extend(self.exec_context['_events'])
                    self.exec_context['_events'] = []

                events.append({
                    'type': 'tick_complete',
                    'strategy_id': self.strategy_id,
                    'profit_loss': result.get('profit_loss', 0.0),
                })
            else:
                events.append({
                    'type': 'error',
                    'strategy_id': self.strategy_id,
                    'message': result.get('message', 'Unknown error'),
                })

        except Exception as e:
            logger.error(f"Error in strategy tick: {e}", exc_info=True)
            events.append({
                'type': 'error',
                'strategy_id': self.strategy_id,
                'message': str(e),
            })

        return events

    def execute(self) -> dict[str, Any]:
        """
        Executes the strategy's 'next()' method.
        :return: Result of the execution.
        """
        try:
            # Log the generated code once for debugging
            if not hasattr(self, '_code_logged'):
                logger.info(f"Strategy {self.strategy_id} generated code:\n{self.generated_code}")
                self._code_logged = True

            # Compile the generated code with a meaningful filename
            compiled_code = compile(self.generated_code, '<strategy_code>', 'exec')
            exec(compiled_code, self.exec_context)

            # Call the 'next()' method if defined
            if 'next' in self.exec_context and callable(self.exec_context['next']):
                # Log flags before execution for debugging
                logger.debug(f"[STRATEGY EXEC] Flags before next(): {self.flags}")
                logger.debug(f"[STRATEGY EXEC] Variables before next(): {self.variables}")
                self.exec_context['next']()
                # Log flags after execution for debugging
                logger.debug(f"[STRATEGY EXEC] Flags after next(): {self.flags}")
                logger.debug(f"[STRATEGY EXEC] Variables after next(): {self.variables}")
            else:
                logger.error(
                    f"'next' method not defined in generated_code for StrategyInstance '{self.strategy_instance_id}'."
                )
                return {"success": False, "message": "'next' method not defined."}

            # Retrieve and update profit/loss
            self.profit_loss = self.exec_context.get('profit_loss', self.profit_loss)
            self.save_context()

            return {"success": True, "profit_loss": self.profit_loss}

        except Exception as e:
            # Extract full traceback
            full_tb = traceback.format_exc()

            # Extract traceback object
            tb = traceback.extract_tb(e.__traceback__)

            # Initialize variables to hold error details
            error_line_no = None
            error_code_line = None

            # Debug: Log all frames in the traceback
            logger.debug("Traceback frames:")
            for frame in tb:
                logger.debug(
                    f"Filename: {frame.filename}, Line: {frame.lineno}, Function: {frame.name}, Line Text: {frame.line}"
                )

            # Iterate through traceback to find the frame with our compiled code
            for frame in tb:
                if frame.filename == '<strategy_code>':
                    error_line_no = frame.lineno
                    error_code_line = frame.line
                    break  # Exit after finding the relevant frame

            if error_line_no:
                # Fetch the specific line from generated_code
                generated_code_lines = [line for line in self.generated_code.strip().split('\n') if line.strip()]
                logger.debug(f"Generated Code Lines Count: {len(generated_code_lines)}")

                if 1 <= error_line_no <= len(generated_code_lines):
                    problematic_line = generated_code_lines[error_line_no - 1].strip()
                    logger.debug(f"Problematic Line {error_line_no}:\n {problematic_line}\n")
                else:
                    problematic_line = "Unknown line."
                    logger.debug(f"Error line number {error_line_no} is out of bounds.")

                if error_code_line and error_code_line.strip():
                    # If frame.line has content, use it
                    problematic_line = error_code_line.strip()
                    logger.debug(f"Problematic Line from Traceback: {problematic_line}")
                else:
                    # Otherwise, use the line from generated_code
                    problematic_line = problematic_line

                # Log detailed error information
                logger.error(
                    f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}\n"
                    f"Traceback:\n{full_tb}\n"
                    f"Error occurred at line {error_line_no}:\n {problematic_line}\n"
                    f"Generated Code:\n{self.generated_code}"
                )

                # Optionally, include the problematic line in the returned message
                return {
                    "success": False,
                    "message": f"{e} at line {error_line_no}: {problematic_line}"
                }
            else:
                # If no specific frame found, log the full traceback
                logger.error(
                    f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}\n"
                    f"Traceback:\n{full_tb}"
                )
                return {"success": False, "message": str(e)}

    def set_paused(self, value: bool):
        """
        Sets the paused state of the strategy.
        :param value: True to pause, False to resume.
        """
        self.paused = value
        self.exec_context['paused'] = self.paused
        self.save_context()
        logger.debug(f"Strategy '{self.strategy_id}' paused: {self.paused}")

    def set_exit(self, exit_flag: bool, exit_method: str = 'all'):
        """
        Sets the exit state and method of the strategy.
        :param exit_flag: True to initiate exit.
        :param exit_method: Method to use for exiting ('all', 'in_profit', 'in_loss').
        """
        self.exit = exit_flag
        self.exit_method = exit_method
        self.save_context()
        logger.debug(f"Strategy '{self.strategy_id}' exit set: {self.exit} with method '{self.exit_method}'")

    def set_available_strategy_balance(self, balance: float):
        """
        Sets the available balance for the strategy.

        :param balance: The new available balance.
        """
        self.available_strategy_balance = balance
        self.exec_context['available_strategy_balance'] = self.available_strategy_balance
        self.save_context()
        logger.debug(f"Available strategy balance set to {balance}.")

    def get_current_balance(self) -> float:
        """
        Retrieves the current balance from the Trades manager.

        :return: Current balance.
        """
        try:
            # Update self.current_balance from trades
            self.current_balance = self.trades.get_current_balance(self.user_id)
            self.exec_context['current_balance'] = self.current_balance
            logger.debug(f"Current balance retrieved: {self.current_balance}.")
            return self.current_balance
        except Exception as e:
            logger.error(f"Error retrieving current balance: {e}", exc_info=True)
            return 0.0

    def get_available_strategy_balance(self) -> float:
        """
        Retrieves the available strategy balance.

        :return: Available strategy balance.
        """
        try:
            logger.debug(f"Available strategy balance retrieved: {self.available_strategy_balance}.")
            return self.available_strategy_balance
        except Exception as e:
            logger.error(f"Error retrieving available strategy balance: {e}", exc_info=True)
            return 0.0

    def fetch_user_balance(self) -> float:
        """
        Fetches the user's total balance.

        :return: User's total balance.
        """
        try:
            balance = self.trades.get_current_balance(self.user_id)
            logger.debug(f"Fetched user balance: {balance}.")
            return balance
        except Exception as e:
            logger.error(f"Error fetching user balance: {e}", exc_info=True)
            return 0.0

    def calculate_available_balance(self) -> float:
        """
        Calculates the user's available balance not tied up in trades or orders.

        :return: Available balance.
        """
        try:
            balance = self.trades.get_available_balance(self.user_id)
            logger.debug(f"Calculated available balance: {balance}.")
            return balance
        except Exception as e:
            logger.error(f"Error calculating available balance: {e}", exc_info=True)
            return 0.0

    def get_total_filled_order_volume(self) -> float:
        """
        Retrieves the total filled order volume for the strategy.
        """
        return self.trades.get_total_filled_order_volume(self.strategy_id)

    def get_total_unfilled_order_volume(self) -> float:
        """
        Retrieves the total unfilled order volume for the strategy.
        """
        return self.trades.get_total_unfilled_order_volume(self.strategy_id)

    def get_last_candle(self, candle_part: str, timeframe: str, exchange: str, symbol: str):
        """
        Retrieves the last candle data based on provided parameters.

        :param candle_part: Part of the candle data (e.g., 'close', 'open').
        :param timeframe: Timeframe of the candle.
        :param exchange: Exchange name.
        :param symbol: Trading symbol.
        :return: Last candle value.
        """
        try:
            if self.edm_client is None:
                logger.error("EDM client not initialized. Cannot fetch candle data.")
                return None

            data = self.edm_client.get_candles_sync(
                exchange=exchange.lower(),
                symbol=symbol,
                timeframe=timeframe,
                limit=1
            )
            if not data.empty:
                return data.iloc[-1][candle_part]
            else:
                logger.warning(f"No candle data found for {exchange} {symbol} {timeframe}.")
                return None
        except Exception as e:
            logger.error(f"Error retrieving last candle: {e}", exc_info=True)
            traceback.print_exc()
            return None

    def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance',
                          symbol: str = 'BTC/USD') -> float | None:
        """
        Retrieves the current market price for the specified symbol.

        :param timeframe: The timeframe of the data.
        :param exchange: The exchange name.
        :param symbol: The trading symbol.
        :return: The current price or None if unavailable.
        """
        try:
            # Assuming get_last_candle returns the last candle data as a dictionary
            last_candle = self.get_last_candle('close', timeframe, exchange, symbol)
            if last_candle is not None:
                logger.debug(f"Retrieved current price for {symbol} on {exchange} ({timeframe}): {last_candle}")
                return last_candle
            else:
                logger.warning(f"No last candle data available for {symbol} on {exchange} ({timeframe}).")
                return None
        except Exception as e:
            logger.error(f"Error retrieving current price for {symbol} on {exchange} ({timeframe}): {e}", exc_info=True)
            return None

    def trade_order(
            self,
            trade_type: str,
            size: float,
            order_type: str,
            source: dict = None,
            tif: str = 'GTC',
            stop_loss: dict = None,
            trailing_stop: dict = None,
            take_profit: dict = None,
            limit: dict = None,
            trailing_limit: dict = None,
            target_market: dict = None,
            name_order: dict = None
    ):
        """
        Unified trade order handler for executing buy and sell orders.
        """
        symbol = source['symbol'] if source and 'symbol' in source else 'Unknown'
        if trade_type == 'buy':
            logger.info(f"Executing BUY order: Size={size}, Symbol={symbol}, Order Type={order_type}")
            # Implement buy order logic here
            status, msg = self.trades.buy({
                'symbol': symbol,
                'size': size,
                'order_type': order_type,
                'tif': tif,
                # Include other parameters as needed
            }, self.user_id)
            if status == 'success':
                # Update balances
                # Assume that the trade amount is size * price
                price = self.get_current_price()
                if price:
                    trade_amount = size * price
                    self.available_strategy_balance -= trade_amount
                    self.available_balance -= trade_amount
                    self.current_balance -= trade_amount
                    self.exec_context['available_strategy_balance'] = self.available_strategy_balance
                    self.exec_context['available_balance'] = self.available_balance
                    self.exec_context['current_balance'] = self.current_balance
                    self.save_context()
            else:
                logger.error(f"Buy order failed: {msg}")
        elif trade_type == 'sell':
            logger.info(f"Executing SELL order: Size={size}, Symbol={symbol}, Order Type={order_type}")
            # Implement sell order logic here
            status, msg = self.trades.sell({
                'symbol': symbol,
                'size': size,
                'order_type': order_type,
                'tif': tif,
                # Include other parameters as needed
            }, self.user_id)
            if status == 'success':
                # Update balances accordingly
                price = self.get_current_price()
                if price:
                    trade_amount = size * price
                    self.available_strategy_balance += trade_amount
                    self.available_balance += trade_amount
                    self.current_balance += trade_amount
                    self.exec_context['available_strategy_balance'] = self.available_strategy_balance
                    self.exec_context['available_balance'] = self.available_balance
                    self.exec_context['current_balance'] = self.current_balance
                    self.save_context()
            else:
                logger.error(f"Sell order failed: {msg}")
        else:
            logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.")
            return

        # Handle trade options like stop_loss, take_profit, etc.
        if stop_loss:
            # Implement stop loss logic
            pass
        if take_profit:
            # Implement take profit logic
            pass
        # Add handling for other trade options as needed

        # Notify user about the trade execution
        self.notify_user(f"{trade_type.capitalize()} order executed for {size} {symbol} at {order_type} price.")


    def exit_strategy(self):
        """
        Exits the strategy based on the exit_method.
        """
        try:
            if self.exit_method == 'all':
                self.trades.exit_strategy_all(self.strategy_id)
                logger.info(f"Exiting all positions for strategy '{self.strategy_id}'.")
            elif self.exit_method == 'in_profit':
                self.trades.exit_strategy_in_profit(self.strategy_id)
                logger.info(f"Exiting profitable positions for strategy '{self.strategy_id}'.")
            elif self.exit_method == 'in_loss':
                self.trades.exit_strategy_in_loss(self.strategy_id)
                logger.info(f"Exiting losing positions for strategy '{self.strategy_id}'.")
            else:
                logger.warning(
                    f"Unknown exit method '{self.exit_method}' for StrategyInstance '{self.strategy_instance_id}'.")
        except Exception as e:
            logger.error(
                f"Error exiting strategy '{self.strategy_id}' in StrategyInstance '{self.strategy_instance_id}': {e}",
                exc_info=True)
            traceback.print_exc()

    def notify_user(self, message: str):
        """
        Sends a notification to the user.

        :param message: Notification message.
        """
        try:
            self.trades.notify_user(self.user_id, message)
            logger.debug(f"Notification sent to user '{self.user_id}': {message}")
        except Exception as e:
            logger.error(
                f"Error notifying user '{self.user_id}' in StrategyInstance '{self.strategy_instance_id}': {e}",
                exc_info=True)
            traceback.print_exc()

    def process_indicator(self, indicator_name: str, output_field: str) -> Any:
        """
        Retrieves the latest value of an indicator.

        For subscribed strategies, indicators are looked up using indicator_owner_id
        (the strategy creator's ID) rather than the running user's ID.

        :param indicator_name: Name of the indicator.
        :param output_field: Specific field of the indicator.
        :return: Indicator value.
        """
        # Use indicator_owner_id for lookup (creator's indicators for subscribed strategies)
        lookup_user_id = self.indicator_owner_id
        logger.debug(f"StrategyInstance is Retrieving indicator '{indicator_name}' from Indicators for user '{lookup_user_id}'.")

        try:
            user_indicators = self.indicators.get_indicator_list(user_id=lookup_user_id)
            indicator = user_indicators.get(indicator_name)
            if not indicator:
                logger.error(f"Indicator '{indicator_name}' not found for user '{lookup_user_id}'.")
                return None
            indicator_value = self.indicators.process_indicator(indicator)
            value = indicator_value.get(output_field, None)
            logger.debug(f"Processed indicator '{indicator_name}' with output field '{output_field}': {value}")
            return value
        except Exception as e:
            logger.error(
                f"Error processing indicator '{indicator_name}' in StrategyInstance '{self.strategy_instance_id}': {e}",
                exc_info=True)
            traceback.print_exc()
            return None

    def process_signal(self, signal_name: str, output_field: str = 'triggered') -> Any:
        """
        Checks the state of a user-defined signal.

        :param signal_name: Name of the signal.
        :param output_field: Either 'triggered' (returns bool) or 'value' (returns numeric).
        :return: Boolean for triggered, numeric value for value, or None on error.
        """
        try:
            # Get the signal from the signals manager
            if not hasattr(self, 'signals') or self.signals is None:
                logger.warning(f"Signals manager not available in StrategyInstance")
                return False if output_field == 'triggered' else None

            # Look up the signal by name
            signal = self.signals.get_signal_by_name(signal_name)
            if signal is None:
                logger.warning(f"Signal '{signal_name}' not found")
                return False if output_field == 'triggered' else None

            if output_field == 'triggered':
                # Return whether the signal condition is currently met
                # Signal is a dataclass with 'state' attribute
                is_triggered = getattr(signal, 'state', False)
                logger.debug(f"Signal '{signal_name}' triggered: {is_triggered}")
                return is_triggered
            else:
                # Return the numeric value of the signal (value1)
                value = getattr(signal, 'value1', None)
                logger.debug(f"Signal '{signal_name}' value: {value}")
                return value

        except Exception as e:
            logger.error(
                f"Error processing signal '{signal_name}' in StrategyInstance '{self.strategy_instance_id}': {e}",
                exc_info=True)
            traceback.print_exc()
            return False if output_field == 'triggered' else None

    def process_formation(self, tbl_key: str, property_name: str = 'line', timestamp: int = None) -> float:
        """
        Gets the price value of a formation property at a given timestamp.

        Uses formation_owner_id (not current user) for subscribed strategies.
        Parallel to indicator_owner_id pattern.

        :param tbl_key: Unique key of the formation (UUID).
        :param property_name: Property to retrieve ('line', 'upper', 'lower', 'midline', etc.).
        :param timestamp: Unix timestamp in seconds UTC. If None, uses current candle time.
        :return: Price value at the timestamp, or None on error.
        """
        try:
            # Check if formations manager is available
            if not hasattr(self, 'formations') or self.formations is None:
                logger.warning(f"Formations manager not available in StrategyInstance")
                return None

            # Default timestamp: use current candle time if available
            if timestamp is None:
                timestamp = self.get_current_candle_time()

            # Use formation_owner_id for subscribed strategies (parallel to indicator_owner_id)
            owner_id = getattr(self, 'formation_owner_id', self.user_id)

            # Look up the formation by tbl_key using owner's formations
            formation = self.formations.get_by_tbl_key_for_strategy(tbl_key, owner_id)
            if formation is None:
                logger.warning(f"Formation with tbl_key '{tbl_key}' not found for owner {owner_id}")
                return None

            # Get the property value at the timestamp
            value = self.formations.get_property_value(formation, property_name, timestamp)
            logger.debug(f"Formation '{formation.get('name')}' {property_name} at {timestamp}: {value}")
            return value

        except Exception as e:
            logger.error(
                f"Error processing formation '{tbl_key}' in StrategyInstance '{self.strategy_instance_id}': {e}",
                exc_info=True)
            traceback.print_exc()
            return None

    def get_current_candle_time(self) -> int:
        """
        Returns the current candle timestamp in seconds UTC.

        In backtest mode, this is overridden to return the historical bar time.
        In live/paper mode, returns the current time.
        """
        import time
        return int(time.time())

    def get_strategy_profit_loss(self, strategy_id: str) -> float:
        """
        Retrieves the current profit or loss of the strategy.

        :param strategy_id: Unique identifier of the strategy.
        :return: Profit or loss amount.
        """
        try:
            profit_loss = self.trades.get_profit(strategy_id)
            logger.debug(f"Retrieved profit/loss for strategy '{strategy_id}': {profit_loss}")
            return profit_loss
        except Exception as e:
            logger.error(f"Error retrieving profit/loss for StrategyInstance '{self.strategy_instance_id}': {e}",
                         exc_info=True)
            traceback.print_exc()
            return 0.0

    def is_in_profit(self) -> bool:
        """
        Determines if the strategy is currently in profit.
        """
        profit = self.profit_loss
        logger.debug(f"Checking if in profit: {profit} > 0")
        return self.profit_loss > 0

    def is_in_loss(self) -> bool:
        """
        Determines if the strategy is currently in loss.
        """
        loss = self.profit_loss
        logger.debug(f"Checking if in loss: {loss} < 0")
        return self.profit_loss < 0

    def get_active_trades(self) -> int:
        """
        Returns the number of active trades.
        """
        active_trades_count = len(self.trades.active_trades)
        logger.debug(f"Number of active trades: {active_trades_count}")
        return active_trades_count

    def get_starting_balance(self) -> float:
        """
        Returns the starting balance.
        """
        logger.debug(f"Starting balance: {self.starting_balance}")
        return self.starting_balance

    def get_filled_orders(self) -> int:
        """
        Retrieves the number of filled orders for the strategy.
        """
        return self.trades.get_filled_orders_count(self.strategy_id)

    def get_unfilled_orders(self) -> int:
        """
        Retrieves the number of unfilled orders for the strategy.
        """
        return self.trades.get_unfilled_orders_count(self.strategy_id)

    def get_available_balance(self) -> float:
        """
        Retrieves the available balance for the strategy.
        """
        return self.trades.get_available_balance(self.strategy_id)

    def accumulate_trade_fee(self, trade_value_usd: float, commission_rate: float,
                             is_profitable: bool) -> dict:
        """
        Accumulate fee for a completed trade (only called for paid strategies).

        This method is called when a trade fills to accumulate fees that will
        be settled when the strategy stops.

        :param trade_value_usd: The USD value of the trade.
        :param commission_rate: The exchange commission rate (e.g., 0.001 for 0.1%).
        :param is_profitable: Whether the trade was profitable.
        :return: Dict with fee charged amount.
        """
        # Check if this strategy has fee tracking enabled
        if not getattr(self, 'has_fee', False) or not getattr(self, 'strategy_run_id', None):
            return {'success': True, 'fee_charged': 0, 'reason': 'no_fees_enabled'}

        wallet_manager = getattr(self, 'wallet_manager', None)
        if not wallet_manager:
            return {'success': False, 'error': 'No wallet manager available'}

        # Calculate exchange fee in USD, then convert to satoshis
        # Assume 1 BTC = $50,000 for conversion (rough estimate, could be made dynamic)
        btc_price_usd = 50000  # This could be fetched from exchange
        exchange_fee_usd = trade_value_usd * commission_rate

        # Convert to satoshis: 1 BTC = 100,000,000 satoshis
        exchange_fee_btc = exchange_fee_usd / btc_price_usd
        exchange_fee_satoshis = int(exchange_fee_btc * 100_000_000)

        # Accumulate the fee
        result = wallet_manager.accumulate_trade_fee(
            strategy_run_id=self.strategy_run_id,
            exchange_fee_satoshis=exchange_fee_satoshis,
            is_profitable=is_profitable
        )

        if result.get('fee_charged', 0) > 0:
            logger.debug(f"Accumulated fee: {result['fee_charged']} sats for trade worth ${trade_value_usd:.2f}")

        return result
