#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-

from typing import NoReturn
import pandas
import numpy
from TradeTide.strategy import Strategy
from TradeTide.plottings import PlotTrade
from TradeTide.capital_management import LimitedCapital, UnlimitedCapital
from TradeTide.metrics import Metrics


class BackTester():
    """
    A class for backtesting forex trading strategies.

    The ForexBacktester takes a trading strategy, applies it to historical forex data, and simulates the execution
    of trades based on the signals generated by the strategy. It tracks the performance of these trades over time,
    allowing for the evaluation of the strategy's effectiveness.
    """

    def __init__(self, market: pandas.DataFrame, strategy: Strategy):
        """
        Initializes a new instance of the BackTester class with specified market data and trading strategy.

        The BackTester class is designed to simulate trading activity using historical market data and a predefined trading
        strategy. It sets up the environment for backtesting by associating the given market data and strategy with the instance,
        preparing it to execute the backtest through subsequent methods.

        Parameters:
            market (pandas.DataFrame): The historical market data to be used for backtesting. This DataFrame should include
                                       columns relevant to the trading strategy, such as 'date', 'open', 'high', 'low', 'close',
                                       and optionally 'volume', among others.
            strategy (Strategy): An instance of a trading strategy derived from the Strategy class. This strategy should
                                     implement methods for generating trading signals based on the provided market data.

        Attributes:
            market_dataframe (pandas.DataFrame): Stores the provided market data for use in backtesting.
            strategy (Strategy): The trading strategy to be backtested.
            portfolio (pandas.DataFrame or None): Initialized as None, this attribute is intended to store the results of the
                                                   backtest, including the performance of the trading strategy over time. It will
                                                   be populated and updated during the backtesting process.

        Note:
            The 'market_dataframe' must be pre-processed if necessary before passing to this initializer to ensure compatibility
            with the 'strategy'. The 'strategy' instance must be properly configured with any required parameters prior to
            initialization of the BackTester.
        """
        self.market = market
        self.strategy = strategy
        self.strategy.generate_signal(self.market)
        self.portfolio = None

    def signals_to_positions(self) -> NoReturn:
        """
        Transforms trading signals into positions by interpreting changes in the signals.

        This method updates positions based on the following rules:
        - A positive change in the signal (e.g., from 0 to 1) indicates entering a long position.
        - A negative change in the signal (e.g., from 1 to 0 or 1 to -1) indicates exiting a long position or entering a short position.
        - Continuous signals (no change) imply holding the current position.
        - Direct reversals (e.g., from -1 to 1) are allowed and indicate switching positions.

        The 'positions' column in the portfolio DataFrame is updated to reflect the current trading position.
        """
        # Calculate the difference to detect changes in the signal
        signal_changes = self.strategy.signal.diff()

        # Determine positions: 1 for long, -1 for short, 0 for no position
        self.portfolio['positions'] = numpy.select(
            [
                signal_changes > 0,  # Condition for entering a long position
                signal_changes < 0,  # Condition for entering a short position or exiting
            ],
            [
                1,  # Value for entering a long position
                numpy.where(self.strategy.signal == -1, -1, 0)  # Enter short position if signal is -1, else exit
            ],
            default=self.strategy.signal  # Default to the current signal, allowing for direct reversals and continuous holding
        )

        # Forward fill to maintain positions until the next change
        self.portfolio['positions'].ffill(inplace=True)

        # Fill initial NaN values with 0 (no position at start)
        self.portfolio['positions'].fillna(0, inplace=True)

    def backtest(self, capital_management: LimitedCapital | UnlimitedCapital) -> pandas.DataFrame:
        """
        Conducts a backtest on a trading strategy using historical market data, applying either a limited or unlimited
        capital position strategy.

        This method simulates trading operations based on the strategy's signals within the constraints of the specified
        position strategy (either LimitedCapital or UnlimitedCapital). It takes into account the initial capital, transaction
        costs, and the capital allocation per trade, to compute the performance of the trading portfolio over time.

        Parameters:
            capital_management (LimitedCapital | UnlimitedCapital): An instance of a position strategy, which determines
            the trading behavior based on capital limitations. The strategy must be one of the following:
                - LimitedCapital: A strategy that enforces a maximum limit on the capital allocated per trade.
                - UnlimitedCapital: A strategy with no limit on the capital allocation per trade, implying the availability of unlimited capital for trading.

        Returns:
            pandas.DataFrame: A DataFrame that chronicles the performance of the trading portfolio over the course of the
            backtest. This includes columns for the date, current holdings (units), cash balance, and positions (both long
            and short), reflecting changes in the portfolio's value over time.

        The method initializes the portfolio with the given initial capital and iterates through market data to apply the
        trading strategy. Trades are executed based on the strategy's signals, adjusting the portfolio's positions and cash
        balance accordingly. The final DataFrame provides a detailed account of the portfolio's performance, facilitating
        analysis of the strategy's effectiveness.

        Note:
            - The specific implementation of position management and trade execution is dependent on the `position_strategy`
              parameter, which must be provided when calling this method.
            - This method does not return additional data frames with extra backtest-related data. For detailed trade or
              market condition analysis, modifications to the method or strategy implementation may be required.
        """
        self.capital_management = capital_management

        self.portfolio = pandas.DataFrame(
            0.0,
            index=self.market.index,
            columns=['date', 'units', 'holdings', 'short_positions', 'long_positions', 'cash']
        )

        self.portfolio['date'] = self.market['date']
        self.portfolio['cash'] = float(capital_management.initial_capital)

        capital_management.manage(
            backtester=self,
            market=self.market,
        )

        self.generate_portfolio()

        self.metrics = Metrics(
            portfolio=self.portfolio,
            position_list=self.position_list,
            capital_management=self.capital_management,
            market=self.market
        )

        return self.portfolio

    def generate_portfolio(self) -> NoReturn:
        # Calculate cumulative holdings and cash, considering the units bought and the entry price
        for position in self.position_list:
            position.update_portfolio_dataframe(dataframe=self.portfolio)

        # # Total portfolio value and percentage returns
        self.portfolio['total'] = self.portfolio['cash'] + self.portfolio['holdings']
        self.portfolio['returns'] = self.portfolio['total'].ffill().pct_change()

    def plot(self, **kwargs) -> NoReturn:
        """
        Generates a visual representation of the backtest results using the PlotTrade class.

        This method constructs a figure that includes plots for market data, trading signals, and portfolio metrics,
        providing a comprehensive overview of the trading strategy's performance over the backtesting period.

        The figure is constructed and displayed immediately, utilizing the market data, portfolio results, and the
        strategy instance associated with this BackTester.

        Note:
            The method assumes that the backtest has been run and the `portfolio` attribute of the BackTester class
            has been populated with trading data and results.
        """
        plot = PlotTrade(
            backtester=self,
            market=self.market,
            portfolio=self.portfolio,
            strategy=self.strategy,
        )

        plot.plot_trading_strategy(**kwargs)


# -
