"""Main module of the package"""
# Standard library imports

# Third party imports
import pandas as pd

# Local imports

# Global constants


class Backtester():

    def __init__(
            self,
            float_percent_const_trading_fees=0.01,
    ) -> None:
        """Initialize backtester object

        Args:
            float_percent_const_trading_fees (float): \
                Trading fees. Defaults to 0.01.
        """
        self._float_percent_const_trading_fees = \
            float_percent_const_trading_fees

    def backtest(
            self,
            df_positions_short,
            df_prices_full,
            is_to_neutralize=True,
            td_trading_delay=None,
            td_execution_duration=None,
            orderbook=None,
            **dict_orderbook_kwargs,
    ):
        """Backtest positions to understand if they can generate PNL

        Args:
            df_positions_short (pd.DataFrame): Positions we want to take
            df_prices_full (pd.DataFrame): Prices of assets in higher resolution
            is_to_neutralize (bool): Flag if to have long-short equal positions
            td_trading_delay (datetime.timedelta): \
                Delay needed to calculate the wanted positions
            td_execution_duration ([datetime.timedelta]): \
                How long should the execution take
            orderbook ([TBD]): orderbook object to calculate execution price by it

        Returns:
            pd.DataFrame: columns with different PNLs generated at every tick
        """
        # print(df_positions_short.T.tail(3).T.tail(3).to_html())
        # print(df_prices_full.T.tail(3).T.tail(3).to_html())
        df_backtest_res = pd.DataFrame()
        # Neutralize
        if is_to_neutralize:
            df_positions_short = self._neutralize(df_positions_short)
        # Scale to 1.0
        df_positions_short = self._scale(df_positions_short)
        # Add delay which was needed to generate this positions
        if td_trading_delay is not None:
            df_positions_short = df_positions_short.shift(freq=td_trading_delay)
        # Convert full df with prices to short format of reachable prices
        # at the moments of position change (No bias)
        df_prices_short = self._convert_full_prices_df_to_short(
            df_positions_short, df_prices_full)
        # Calculate holding pnl
        # which is a pnl generated by positions taken in the past
        df_backtest_res["PNL_before_costs"] = self._calc_ser_holding_pnl(
            df_positions_short, df_prices_short)
        # DataFrame how we would like to change our current positions
        df_pos_change_wanted = df_positions_short - df_positions_short.shift(1)
        # Calculate prices by which we can execute our position change
        if orderbook is None:
            df_exec_prices_short = self.calc_execution_price_rough(
                df_positions_short,
                df_prices_full,
                td_execution_duration
            )
        else:
            df_exec_prices_short = self.calc_execution_price_by_orderbook(
                df_positions_short,
                df_prices_full,
                td_execution_duration,
                orderbook=orderbook,
                **dict_orderbook_kwargs,
            )
        # Execution fee which is paid because
        # we can't execute by the best available price now
        df_execution_fee = \
            df_pos_change_wanted * (df_exec_prices_short - df_prices_short)
        df_backtest_res["execution_fee_pnl"] = df_execution_fee.sum(axis=1)
        # Volume traded at the current tick
        df_backtest_res["trading_volume"] = \
            df_pos_change_wanted.abs().sum(axis=1)
        # Brokers trading commission
        df_backtest_res["const_trading_fee_pnl"] = (
            df_backtest_res["trading_volume"] *
            self._float_percent_const_trading_fees / 100.0
        )
        # Add column with with final PNL results
        df_backtest_res["PNL_after_costs"] = (
            df_backtest_res["PNL_before_costs"] -
            df_backtest_res["execution_fee_pnl"] -
            df_backtest_res["const_trading_fee_pnl"]
        )
        df_backtest_res["PNL_half_costs"] = (
            df_backtest_res["PNL_before_costs"] +
            df_backtest_res["PNL_after_costs"]
        ) / 2.0

        df_backtest_res["booksize"] = 1.0

        return df_backtest_res

    def calc_execution_price_rough(
            self,
            df_positions_short,
            df_prices_full,
            td_execution_duration
    ):
        """Get execution price as mean price over execution duration

        Args:
            df_positions_short (pd.DataFrame): Positions we want to take
            df_prices_full (pd.DataFrame): Prices of assets in higher resolution
            td_execution_duration ([datetime.timedelta]): \
                How long should the execution take

        Returns:
            pd.DataFrame: Prices by which asset can be bought at any moment
        """
        # Reverse the df with prices because rolling mean price
        # Should go into future not the past and then at the end reverse back


        if td_execution_duration:
            df_exec_price_full = df_prices_full[::-1].rolling(
                td_execution_duration, min_periods=2).mean()[::-1]
        else:
            df_exec_price_full = df_prices_full[::-1].rolling(2).mean()[::-1]
        # Convert Execution prices to the short format
        df_exec_prices_short = self._convert_full_prices_df_to_short(
            df_positions_short, df_exec_price_full)
        return df_exec_prices_short

    def calc_execution_price_by_orderbook(
            self,
            df_positions_short,
            df_prices_full,
            td_execution_duration,
            orderbook,
            **dict_orderbook_kwargs,
    ):
        """
        Get execution price for the asked position
        """
        raise ValueError("backtest by orderbook is not implemented yet")

    @staticmethod
    def _calc_ser_holding_pnl(df_positions_short, df_prices_short):
        """Calculate PNL generated by holding positions taken at last tick"""
        df_previous_pos = df_positions_short.shift(1)
        df_prices_change_pct = df_prices_short/ df_prices_short.shift(1) - 1.0
        df_holding_pnl = df_previous_pos.multiply(df_prices_change_pct)
        return df_holding_pnl.sum(axis=1)

    @staticmethod
    def _convert_full_prices_df_to_short(df_positions_short, df_prices_full):
        """Convert prices in high resolution to the resolution of short DFs"""
        df_positions_empty = pd.DataFrame(
            index=df_positions_short.index, columns=df_positions_short.columns)
        df_prices_full_size = pd.concat(
            [df_prices_full, df_positions_empty], axis=1)
        df_prices_full_size.sort_index(inplace=True)
        df_prices_full_size.bfill(inplace=True)
        df_prices_short = df_prices_full_size.loc[df_positions_short.index]
        return df_prices_short

    @staticmethod
    def _neutralize(df_positions_short):
        """Neutralize positions to be long-short equal"""
        return df_positions_short.sub(df_positions_short.mean(axis=1), axis=0)

    @staticmethod
    def _scale(df_positions_short):
        """
        Scale to have sum of absolute positions at every tick equals to 1.0
        """
        return df_positions_short.div(df_positions_short.abs().sum(axis=1), axis=0)
