import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pprint
import config.settings
import config.strategy
from core.utility import chunk_trades, sharpe, drawdown
from multiprocessing_on_dill import Pool #, Process, Manager
from contextlib import closing


class accountCurve():
    """
    Account curve object for Portfolio and Instrument.
    
    Calculates the positions we want to be in, based on the volatility target.
    """
    def __init__(self, portfolio, capital=500000, positions=None, panama_prices=None, nofx=False, portfolio_weights = 1, **kw):
        self.portfolio = portfolio
        self.nofx = nofx
        self.weights = portfolio_weights
        self.multiproc = kw.get('multiproc', True)
        # If working on one instrument, put it in a list
        if not isinstance(portfolio, list):
            self.portfolio = [self.portfolio]

        if isinstance(positions, pd.Series):
            positions = positions.rename(self.portfolio[0].name)

        self.capital = capital
        self.panama = panama_prices

        if positions is None:
            self.positions = self.instrument_positions()
            self.positions = self.positions.multiply(self.weights)
        else:
            self.positions = pd.DataFrame(positions)

        # Reduce all our positions so that they fit inside our target volatility when combined.
        self.positions = self.positions.multiply(self.vol_norm(),axis=0)

        # If we run out of data (for example, if the data feed is stopped), hold position for 5 trading days and then close.
        # chunk_trades() is a function that is designed to reduce the amount of trading (and hence cost)

        self.positions = chunk_trades(self.positions).ffill(limit=5).fillna(0)

    def __repr__(self):
        """
        Returns a formatted list of statistics about the account curve.
        """
        return pprint.pformat(self.stats_list())

    def inst_calc(self):
        """Calculate all the things we need on all the instruments and cache it."""
        try:
            return self.memo_inst_calc
        except:
            if len(self.portfolio)>1 and self.multiproc:
                with closing(Pool()) as pool:
                    self.memo_inst_calc = dict(pool.map(lambda x: (x.name, x.calculate()), self.portfolio))
            else:
                self.memo_inst_calc = dict(map(lambda x: (x.name, x.calculate()), self.portfolio))
            return self.memo_inst_calc

    def instrument_positions(self):
        """Position returned by the instrument objects, not the final position in the portfolio"""
        try:
            return self.memo_instrument_positions
        except:
            self.memo_instrument_positions = pd.DataFrame({k: v['position'] for k, v in self.inst_calc().items()})
            return self.memo_instrument_positions

    def rates(self):
        """
        Returns a Series or DataFrame of exchange rates.
        """
        if self.nofx==True:
            return 1
        try:
            return self.memo_rates
        except:
            self.memo_rates = pd.DataFrame({k: v['rate'] for k, v in self.inst_calc().items()})
            return self.memo_rates

    def stats_list(self):
        stats_list = ["sharpe",
                      "gross_sharpe",
                      "annual_vol",
                      "sortino",
                      "cap",
                      "avg_drawdown",
                      "worst_drawdown",
                      "time_in_drawdown",
                      "calmar",
                      "avg_return_to_drawdown"]
        return {k: getattr(self, k)() for k in stats_list}

    def returns(self):
        """
        Returns a Series/Frame of net returns after commissions, spreads and estimated slippage.
        """
        return self.position_returns() + self.transaction_returns() + self.commissions() + self.spreads()

    def position_returns(self):
        """The returns from holding the portfolio we had yesterday"""
        # We shift back 2, as self.positions is the frontier - tomorrow's ideal position.
        return (self.positions.shift(2).multiply((self.panama_prices()).diff(), axis=0).fillna(0) * self.point_values()) * self.rates()

    def transaction_returns(self):
        """Estimated returns from transactions including slippage. Uses the average settlement price of the last two days"""
        # self.positions.diff().shift(1) = today's trades
        slippage_multiplier = .5
        return (self.positions.diff().shift(1).multiply((self.panama_prices()).diff()*slippage_multiplier, axis=0).fillna(0) * self.point_values()) * self.rates()

    def commissions(self):
        commissions = pd.Series({v.name: v.commission for v in self.portfolio})
        return (self.positions.diff().shift(1).multiply(commissions)).fillna(0).abs()*-1

    def spreads(self):
        spreads = pd.Series({v.name: v.spread for v in self.portfolio})
        return (self.positions.diff().shift(1).multiply(spreads * self.point_values() * self.rates())).fillna(0).abs()*-1

    def vol_norm(self):
        return (config.strategy.daily_volatility_target * self.capital / \
                (self.returns().sum(axis=1).shift(2).ewm(span=50).std())).clip(0,1.5)

    def panama_prices(self):
        if self.panama is not None:
            return pd.DataFrame(self.panama)
        else:
            try:
                return self.memo_panama_prices
            except:
                self.memo_panama_prices =  pd.DataFrame({k: v['panama_prices'] for k, v in self.inst_calc().items()})
                return self.memo_panama_prices

    def point_values(self):
        return pd.Series({v.name: v.point_value for v in self.portfolio})

    def gross_sharpe(self):
        return sharpe(np.trim_zeros((self.position_returns() - self.transaction_returns()).sum(axis=1)))

    def sharpe(self):
        return sharpe(np.trim_zeros(self.returns().sum(axis=1)))

    def losses(self):
        return [z for z in np.trim_zeros(self.returns()).sum(axis=1) if z<0]

    def sortino(self):
        return np.trim_zeros(self.returns().sum(axis=1)).mean()/np.std(self.losses())*np.sqrt(252)

    def annual_vol(self):
        return "{0:,.4f}".format(np.trim_zeros(self.returns()).sum(axis=1).std() * np.sqrt(252)/self.capital)

    def plot(self):
        fig, axes = plt.subplots(nrows=1, ncols=1)
        # self.returns.cumsum().plot(ax=axes[0])
        ar = self.annual_returns()
        ar.plot.bar(ax=axes, figsize=(12,1.5))
        axes.set_xlabel("")
        axes.set_xticklabels([dt.strftime('%Y') for dt in ar.index.to_pydatetime()])

    def annual_returns(self):
        return np.trim_zeros(self.returns().sum(axis=1).resample(rule='A').sum()/self.capital * 100)

    def annual_sharpes(self):
        return self.returns().sum(axis=1).resample(rule='A').sum()/(self.returns().sum(axis=1).resample(rule='A').std() * np.sqrt(252))

    def drawdown(self):
        return drawdown(self.returns().sum(axis=1).cumsum())

    def avg_drawdown(self):
        dd = self.drawdown()
        # return "{0:,.0f}".format(np.nanmean(dd.values))
        return np.nanmean(dd.values)/self.capital

    def worst_drawdown(self):
        dd = self.drawdown()
        # return "{0:,.0f}".format(np.nanmin(dd.values))
        return np.nanmin(dd.values)/self.capital
    def cap(self):
        return self.capital

    def time_in_drawdown(self):
        dd = self.drawdown()
        dd = [z for z in dd.values if not np.isnan(z)]
        in_dd = float(len([z for z in dd if z < 0]))
        return "{0:,.4f}".format(in_dd / float(len(dd)))

    def instrument_count(self):
        return np.maximum.accumulate((~np.isnan(self.panama_prices())).sum(axis=1)).plot()

    def underwater(self):
        r = self.returns().sum(axis=1)
        u = (r.cumsum() - r.cumsum().cummax())/self.capital
        return np.trim_zeros(u).plot()

    def cumcapital(self):
        return np.trim_zeros((self.returns().sum(axis=1)/self.capital)+1).cumprod()

    def calmar(self):
        return self.annual_returns().mean() * 0.01 / -self.worst_drawdown()

    def avg_return_to_drawdown(self):
        return self.annual_returns().mean() * 0.01 / -self.avg_drawdown()

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pprint
import config.settings
import config.strategy
from core.utility import chunk_trades, sharpe, drawdown
from multiprocessing_on_dill import Pool #, Process, Manager
from contextlib import closing


class accountCurve():
    """
    Account curve object for Portfolio and Instrument.
    
    Calculates the positions we want to be in, based on the volatility target.
    """
    def __init__(self, portfolio, capital=500000, positions=None, panama_prices=None, nofx=False, portfolio_weights = 1, **kw):
        self.portfolio = portfolio
        self.nofx = nofx
        self.weights = portfolio_weights
        self.multiproc = kw.get('multiproc', True)
        # If working on one instrument, put it in a list
        if not isinstance(portfolio, list):
            self.portfolio = [self.portfolio]

        if isinstance(positions, pd.Series):
            positions = positions.rename(self.portfolio[0].name)

        self.capital = capital
        self.panama = panama_prices

        if positions is None:
            self.positions = self.instrument_positions()
            self.positions = self.positions.multiply(self.weights)
        else:
            self.positions = pd.DataFrame(positions)

        # Reduce all our positions so that they fit inside our target volatility when combined.
        self.positions = self.positions.multiply(self.vol_norm(),axis=0)

        # If we run out of data (for example, if the data feed is stopped), hold position for 5 trading days and then close.
        # chunk_trades() is a function that is designed to reduce the amount of trading (and hence cost)

        self.positions = chunk_trades(self.positions).ffill(limit=5).fillna(0)

    def __repr__(self):
        """
        Returns a formatted list of statistics about the account curve.
        """
        return pprint.pformat(self.stats_list())

    def inst_calc(self):
        """Calculate all the things we need on all the instruments and cache it."""
        try:
            return self.memo_inst_calc
        except:
            if len(self.portfolio)>1 and self.multiproc:
                with closing(Pool()) as pool:
                    self.memo_inst_calc = dict(pool.map(lambda x: (x.name, x.calculate()), self.portfolio))
            else:
                self.memo_inst_calc = dict(map(lambda x: (x.name, x.calculate()), self.portfolio))
            return self.memo_inst_calc

    def instrument_positions(self):
        """Position returned by the instrument objects, not the final position in the portfolio"""
        try:
            return self.memo_instrument_positions
        except:
            self.memo_instrument_positions = pd.DataFrame({k: v['position'] for k, v in self.inst_calc().items()})
            return self.memo_instrument_positions

    def rates(self):
        """
        Returns a Series or DataFrame of exchange rates.
        """
        if self.nofx==True:
            return 1
        try:
            return self.memo_rates
        except:
            self.memo_rates = pd.DataFrame({k: v['rate'] for k, v in self.inst_calc().items()})
            return self.memo_rates

    def stats_list(self):
        stats_list = ["sharpe",
                      "gross_sharpe",
                      "annual_vol",
                      "sortino",
                      "cap",
                      "avg_drawdown",
                      "worst_drawdown",
                      "time_in_drawdown",
                      "calmar",
                      "avg_return_to_drawdown"]
        return {k: getattr(self, k)() for k in stats_list}

    def returns(self):
        """
        Returns a Series/Frame of net returns after commissions, spreads and estimated slippage.
        """
        return self.position_returns() + self.transaction_returns() + self.commissions() + self.spreads()

    def position_returns(self):
        """The returns from holding the portfolio we had yesterday"""
        # We shift back 2, as self.positions is the frontier - tomorrow's ideal position.
        return (self.positions.shift(2).multiply((self.panama_prices()).diff(), axis=0).fillna(0) * self.point_values()) * self.rates()

    def transaction_returns(self):
        """Estimated returns from transactions including slippage. Uses the average settlement price of the last two days"""
        # self.positions.diff().shift(1) = today's trades
        slippage_multiplier = .5
        return (self.positions.diff().shift(1).multiply((self.panama_prices()).diff()*slippage_multiplier, axis=0).fillna(0) * self.point_values()) * self.rates()

    def commissions(self):
        commissions = pd.Series({v.name: v.commission for v in self.portfolio})
        return (self.positions.diff().shift(1).multiply(commissions)).fillna(0).abs()*-1

    def spreads(self):
        spreads = pd.Series({v.name: v.spread for v in self.portfolio})
        return (self.positions.diff().shift(1).multiply(spreads * self.point_values() * self.rates())).fillna(0).abs()*-1

    def vol_norm(self):
        return (config.strategy.daily_volatility_target * self.capital / \
                (self.returns().sum(axis=1).shift(2).ewm(span=50).std())).clip(0,1.5)

    def panama_prices(self):
        if self.panama is not None:
            return pd.DataFrame(self.panama)
        else:
            try:
                return self.memo_panama_prices
            except:
                self.memo_panama_prices =  pd.DataFrame({k: v['panama_prices'] for k, v in self.inst_calc().items()})
                return self.memo_panama_prices

    def point_values(self):
        return pd.Series({v.name: v.point_value for v in self.portfolio})

    def gross_sharpe(self):
        return sharpe(np.trim_zeros((self.position_returns() - self.transaction_returns()).sum(axis=1)))

    def sharpe(self):
        return sharpe(np.trim_zeros(self.returns().sum(axis=1)))

    def losses(self):
        return [z for z in np.trim_zeros(self.returns()).sum(axis=1) if z<0]

    def sortino(self):
        return np.trim_zeros(self.returns().sum(axis=1)).mean()/np.std(self.losses())*np.sqrt(252)

    def annual_vol(self):
        return "{0:,.4f}".format(np.trim_zeros(self.returns()).sum(axis=1).std() * np.sqrt(252)/self.capital)

    def plot(self):
        fig, axes = plt.subplots(nrows=1, ncols=1)
        # self.returns.cumsum().plot(ax=axes[0])
        ar = self.annual_returns()
        ar.plot.bar(ax=axes, figsize=(12,1.5))
        axes.set_xlabel("")
        axes.set_xticklabels([dt.strftime('%Y') for dt in ar.index.to_pydatetime()])

    def annual_returns(self):
        return np.trim_zeros(self.returns().sum(axis=1).resample(rule='A').sum()/self.capital * 100)

    def annual_sharpes(self):
        return self.returns().sum(axis=1).resample(rule='A').sum()/(self.returns().sum(axis=1).resample(rule='A').std() * np.sqrt(252))

    def drawdown(self):
        return drawdown(self.returns().sum(axis=1).cumsum())

    def avg_drawdown(self):
        dd = self.drawdown()
        # return "{0:,.0f}".format(np.nanmean(dd.values))
        return np.nanmean(dd.values)/self.capital

    def worst_drawdown(self):
        dd = self.drawdown()
        # return "{0:,.0f}".format(np.nanmin(dd.values))
        return np.nanmin(dd.values)/self.capital
    def cap(self):
        return self.capital

    def time_in_drawdown(self):
        dd = self.drawdown()
        dd = [z for z in dd.values if not np.isnan(z)]
        in_dd = float(len([z for z in dd if z < 0]))
        return "{0:,.4f}".format(in_dd / float(len(dd)))

    def instrument_count(self):
        return np.maximum.accumulate((~np.isnan(self.panama_prices())).sum(axis=1)).plot()

    def underwater(self):
        r = self.returns().sum(axis=1)
        u = (r.cumsum() - r.cumsum().cummax())/self.capital
        return np.trim_zeros(u).plot()

    def cumcapital(self):
        return np.trim_zeros((self.returns().sum(axis=1)/self.capital)+1).cumprod()

    def calmar(self):
        return self.annual_returns().mean() * 0.01 / -self.worst_drawdown()

    def avg_return_to_drawdown(self):
        return self.annual_returns().mean() * 0.01 / -self.avg_drawdown()

#, Process, Manager Account curve object for Portfolio and Instrument.          Calculates the positions we want to be in, based on the volatility target. # If working on one instrument, put it in a list # Reduce all our positions so that they fit inside our target volatility when combined. # If we run out of data (for example, if the data feed is stopped), hold position for 5 trading days and then close. # chunk_trades() is a function that is designed to reduce the amount of trading (and hence cost) Returns a formatted list of statistics about the account curve. Calculate all the things we need on all the instruments and cache it. Position returned by the instrument objects, not the final position in the portfolio Returns a Series or DataFrame of exchange rates. Returns a Series/Frame of net returns after commissions, spreads and estimated slippage. The returns from holding the portfolio we had yesterday # We shift back 2, as self.positions is the frontier - tomorrow's ideal position. Estimated returns from transactions including slippage. Uses the average settlement price of the last two days # self.positions.diff().shift(1) = today's trades # self.returns.cumsum().plot(ax=axes[0]) # return "{0:,.0f}".format(np.nanmean(dd.values)) # return "{0:,.0f}".format(np.nanmin(dd.values))