import numpy as np
from abc import ABCMeta, abstractmethod
from ..utilities.params import TradeTypes, PriceFields, IOFiles
from ..events import OrderEvent, SignalEvent


class AbstractPortfolio(object):
    """Abstract Portfolio Class

    The abstract portfolio class serves as a template for all of the event
    handling that impacts an individual portfolio. For instance, how will the
    portfolio respond to updated market data? The class also implements logic
    for processing signal events received from the strategy objects, or fill
    events generated by the execution handler.

    Subclasses may provide additional functionality streaming data to update the
    equity of a relevant portfolio or computing historical performance metrics.

    Parameters
    ----------
    data_handler: Object inheriting from the abstract data handler class.
        This object supplies the data to update the prices of held positions and
        provide new bar data with which to construct features.
    position_handler: Object inheriting from the abstract position handler
        class.
        The position handler determines how much of an asset to acquire or to
        relinquish when trading signals are processed into orders.
    portfolio_handler: Portfolio handler object.
        The portfolio handler keeps track of positions that are currently held
        by the portfolio as well as the current amount of equity and capital in
        the portfolio.
    """
    __metaclass__ = ABCMeta

    def __init__(self, data_handler, position_handler, portfolio_handler):
        """Initialize parameters of the abstract portfolio object."""
        self.data_handler = data_handler
        self.position_handler = position_handler
        self.portfolio_handler = portfolio_handler

    def process_post_events(self):
        """Called after all events have been processed but before new bar and
        price data is requested. This method is useful for recording attributes
        of the portfolio.
        """
        raise NotImplementedError()

    def process_market_event(self, market_event):
        """This function iterates through the positions of the portfolio and
        updates critical quantities such as the current market value of the
        position and the number of trading days that the position has been held.

        Parameters
        ----------
        market_event: Market event object.
            The market event that the portfolio will process.
        """
        # Iterate over each position currently held in the portfolio.
        for pos in self.portfolio_handler.filled_positions.values():
            # When we value the position, we'll value it using the close price
            # of the time period.
            s = pos.symbol
            if s in self.data_handler.prices.minor_axis:
                price = self.data_handler.prices.ix[
                    PriceFields.current_price.value, 0, s
                ]
                # Update the value of the position using the new market data.
                pos.update_market_value(price)
            else:
                # If the position is held in the portfolio, but we do not have
                # data for it, then raise an error.
                raise ValueError(
                    "Position {} in portfolio {} does not have associated price"
                    " data on {}.".format(
                        s, self.portfolio_handler.portfolio_id,
                        market_event.datetime.strftime(
                            IOFiles.date_format.value
                        )
                    )
                )

    def process_signal_event(self, signal_event):
        """This function interprets a signal event received from the portfolio
        and, on the basis of portfolio logic, determines whether or not the
        signal should be acted upon. In particular, a nonzero number of shares
        need to be transacted in order for the trade to be actionable and, in
        the event of creating a new position, there needs to be capacity in the
        portfolio.

        Parameters
        ----------
        signal_event: Signal event object.
            The signal event that the portfolio will process.
        """
        symbol = signal_event.symbol
        trade_type = signal_event.trade_type
        # Only trade if there is simultaneously capacity in the portfolio (i.e.
        # the number of allowable positions is not exceeded) and if the amount
        # of the underlying asset to trade is at least one.
        if self.portfolio_handler.is_tradeable(trade_type):
            quantity = self.position_handler.order_sizer[trade_type](
                signal_event, self.portfolio_handler
            )

            if quantity > 0:
                order_event = OrderEvent.from_signal_event(
                    signal_event, quantity
                )
                self.data_handler.events.put(order_event)
                if trade_type == TradeTypes.buy_trade:
                    self.portfolio_handler.add_pending_position(order_event)

    def process_fill_event(self, fill_event):
        """This function updates the portfolio holdings to reflect the results
        of a recent fill. In particular, we update the value of the position to
        reflect the fill price of the order, and incorporate the commission
        fees.

        Depending on whether or not the fill event corresponds to entering or
        exiting a position, the function will either add a position to the
        dictionary of filled positions or it will remove one. The corresponding
        pending position is also removed.

        Parameters
        ----------
        fill_event: Fill event object.
            The fill event that the portfolio will process.
        """
        # Extract variables for the trade type, the cost of filling the position
        # and the commission incurred from the transaction.
        trade_type = fill_event.trade_type
        symbol = fill_event.symbol

        if symbol not in self.portfolio_handler.filled_positions:
            # If we are entering a position, then we subtract from the amount of
            # free capital and add a new position to the dictionary of fills.
            self.portfolio_handler.add_filled_position(fill_event)
        elif symbol in self.portfolio_handler.filled_positions:
            # If we are exiting from a position, then we add money back to the
            # free capital amount and delete the position from the dictionary of
            # fills.
            self.portfolio_handler.modify_filled_position(fill_event)
