__author__ = "saeedamen"  # Saeed Amen

#
# Copyright 2016 Cuemacro
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on a "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#

import datetime
import functools
import math

import numpy
import pandas as pd
import pandas.tseries.offsets

try:
    from pandas.stats.api import ols
except:
    # temporary fix to get compilation, need to rewrite regression code to get
    # this to work
    # later versions of pandas no longer support OLS
    #
    # fails with SciPy 1.3.0 unless we have the very latest version of
    # StatsModels pip install statsmodels==0.10.0rc2 --pre
    #
    try:
        from statsmodels.formula.api import ols
    except:
        pass

from findatapy.timeseries import Filter, Calendar, Timezone
from findatapy.util.dataconstants import DataConstants
from findatapy.util.loggermanager import LoggerManager

from pandas import compat
import numpy as np

import copy
from datetime import timedelta

constants = DataConstants()


class Calculations(object):
    """Calculations on time series, such as calculating strategy returns and
    various wrappers on pd for rolling sums etc.

    """

    ##### calculate

    def calculate_signal_tc(self, signal_data_frame, tc, period_shift=1):
        """Calculates the transaction costs for a particular signal

        Parameters
        ----------
        signal_data_frame : DataFrame
            contains trading signals
        tc : float
            transaction costs
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return (signal_data_frame.shift(
            period_shift) - signal_data_frame).abs().multiply(tc)

    def calculate_entry_tc(self, entry_data_frame, tc, period_shift=1):
        """Calculates the transaction costs for defined trading points

        Parameters
        ----------
        entry_data_frame : DataFrame
            contains points where we enter/exit trades
        tc : float
            transaction costs
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return entry_data_frame.abs().multiply(tc)

    def calculate_signal_returns(self, signal_data_frame, returns_data_frame,
                                 period_shift=1):
        """Calculates the trading startegy returns for given signal and asset

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals
        returns_data_frame: DataFrame
            returns of asset to be traded
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """

        # can cause issues, if the names of the columns are not identical
        return signal_data_frame.shift(period_shift) * returns_data_frame

    def calculate_signal_returns_as_matrix(self, signal_data_frame,
                                           returns_data_frame, period_shift=1):

        return pd.DataFrame(
            signal_data_frame.shift(
                period_shift).values * returns_data_frame.values,
            index=returns_data_frame.index,
            columns=returns_data_frame.columns)

    def calculate_individual_trade_gains(self, signal_data_frame,
                                         strategy_returns_data_frame):
        """Calculates profits on every trade (experimental code)

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals
        strategy_returns_data_frame: DataFrame
            returns of strategy to be tested

        Returns
        -------
        DataFrame contains the P&L for every trade
        """

        # signal need to be aligned to NEXT period for returns
        # signal_data_frame_pushed = signal_data_frame.shift(1)

        # Find all the trade points
        trade_points = ((signal_data_frame - signal_data_frame.shift(1)).abs())
        cumulative = self.create_mult_index(strategy_returns_data_frame)

        indices = trade_points > 0
        indices.columns = cumulative.columns

        # Get P&L for every trade (from the end point - start point)
        trade_returns = numpy.nan * cumulative
        trade_points_cumulative = cumulative[indices]

        # for each set of signals/returns, calculate the trade returns - where there isn't a trade
        # assign a NaN
        # TODO do in one vectorised step without for loop
        for col_name in trade_points_cumulative:
            col = trade_points_cumulative[col_name]
            col = col.dropna()
            col = col / col.shift(1) - 1

            # TODO experiment with quicker ways of writing below?
            # for val in col.index:
            # trade_returns.set_value(val, col_name, col[val])
            # trade_returns[col_name][val] = col[val]

            date_indices = trade_returns.index.searchsorted(col.index)
            # trade_returns[col_name][date_indices] = col

            # For Pandas > 3
            trade_returns.iloc[date_indices, trade_returns.columns.get_loc(col_name)] = col

        return trade_returns

    def calculate_cum_rets_trades(self, signal_data_frame,
                                  strategy_returns_data_frame):
        """Calculates cumulative returns resetting at each new trade

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals
        strategy_returns_data_frame: DataFrame
            returns of strategy to be tested
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """

        # Signal need to be aligned to NEXT period for returns
        signal_data_frame_pushed = signal_data_frame.shift(1)

        # Find all the trade points
        reset_points = ((
                                    signal_data_frame_pushed - signal_data_frame_pushed.shift(
                                1)).abs())

        reset_points = reset_points.cumsum()

        # Make sure they have the same column names (otherwise issues around
        # pd calc - assume same ordering for cols)
        old_cols = strategy_returns_data_frame.columns
        strategy_returns_data_frame.columns = signal_data_frame_pushed.columns

        for c in reset_points.columns:
            strategy_returns_data_frame[c + 'cumsum'] = reset_points[c]
            strategy_returns_data_frame[c] = \
            strategy_returns_data_frame.groupby([c + 'cumsum'])[c].cumsum()
            strategy_returns_data_frame = strategy_returns_data_frame.drop(
                [c + 'cumsum'], axis=1)

        strategy_returns_data_frame.columns = old_cols

        return strategy_returns_data_frame

    def calculate_trade_no(self, signal_data_frame):

        ####### how many trades have there been (ignore size of the trades)
        trades = abs(signal_data_frame - signal_data_frame.shift(-1))
        trades = trades[trades > 0].count()

        signal_data_frame = pd.DataFrame(index=trades.index,
                                         columns=['Trades'], data=trades)

        return signal_data_frame

    def calculate_trade_duration(self, signal_data_frame):
        """Calculates cumulative trade durations

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals

        Returns
        -------
        DataFrame
        """

        # TODO
        # # signal need to be aligned to NEXT period for returns
        # signal_data_frame_pushed = signal_data_frame.shift(1)
        #
        # # find all the trade points
        # reset_points = ((signal_data_frame_pushed - signal_data_frame_pushed.shift(1)).abs())
        #
        # reset_points = reset_points.cumsum()
        #
        # time_data_frame = pd.DataFrame(index = signal_data_frame.index, columns = signal_data_frame.columns,
        #                                    data=numpy.ones([len(signal_data_frame.index), len(signal_data_frame.columns)]))
        #
        # # make sure they have the same column names (otherwise issues around pd calc - assume same ordering for cols)
        # old_cols = time_data_frame.columns
        # time_data_frame.columns = signal_data_frame_pushed.columns
        #
        # for c in reset_points.columns:
        #     time_data_frame[c + 'cumperiods'] = reset_points[c]
        #     time_data_frame[c] = time_data_frame.groupby([c + 'cumperiods'])[c].cumsum()
        #     time_data_frame = time_data_frame.drop([c + 'cumperiods'], axis=1)
        #
        # time_data_frame.columns = old_cols
        #
        # return time_data_frame

    def calculate_final_trade_duration(self, signal_data_frame):
        """Calculates cumulative trade durations

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals

        Returns
        -------
        DataFrame
        """

        # Signal need to be aligned to NEXT period for returns
        signal_data_frame_pushed = signal_data_frame.shift(1)

        # Find all the trade points
        reset_points = ((
                                    signal_data_frame_pushed - signal_data_frame_pushed.shift(
                                1)).abs())

        reset_points = reset_points.cumsum()

        time_data_frame = pd.DataFrame(index=signal_data_frame.index,
                                       columns=signal_data_frame.columns,
                                       data=numpy.ones(
                                           [len(signal_data_frame.index),
                                            len(signal_data_frame.columns)]))

        # Make sure they have the same column names (otherwise issues around pd calc - assume same ordering for cols)
        old_cols = time_data_frame.columns
        time_data_frame.columns = signal_data_frame_pushed.columns

        for c in reset_points.columns:
            time_data_frame[c + 'cumperiods'] = reset_points[c]
            time_data_frame[c] = time_data_frame.groupby([c + 'cumperiods'])[
                c].cumsum()
            time_data_frame = time_data_frame.drop([c + 'cumperiods'], axis=1)

        time_data_frame.columns = old_cols

        return time_data_frame

    def calculate_risk_stop_signals(self, signal_data_frame, cum_rets_trades,
                                    stop_loss, take_profit):
        """

        Parameters
        ----------
        signal_data_frame : DataFrame
            Contains all the trade signals (typically mix of 0, +1 and +1

        cum_rets_trades : DataFrame
            Cumulative returns of strategy reset at every new trade

        stop_loss : float (or DataFrame)
            Stop loss level eg. -0.02

        take_profit : float (or DataFrame)
            Take profit level eg. +0.03

        Returns
        -------
        DataFrame containing amended signals that take into account stops and take profits

        """

        signal_data_frame_pushed = signal_data_frame  # signal_data_frame.shift(1)
        reset_points = ((
                                    signal_data_frame_pushed - signal_data_frame_pushed.shift(
                                1)).abs())

        ind = (cum_rets_trades > take_profit) | (cum_rets_trades < stop_loss)

        # to allow indexing, need to match column names
        ind.columns = signal_data_frame.columns
        signal_data_frame[ind] = 0

        reset_points[ind] = 1

        signal_data_frame[reset_points == 0] = numpy.nan
        signal_data_frame = signal_data_frame.ffill()
        # signal_data_frame = signal_data_frame.shift(-1)

        return signal_data_frame

    def calculate_risk_stop_dynamic_signals(self, signal_data_frame,
                                            asset_data_frame, stop_loss_df,
                                            take_profit_df):
        """

        Parameters
        ----------
        signal_data_frame : DataFrame
            Contains all the trade signals (typically mix of 0, +1 and +1

        stop_loss_df : DataFrame
            Continuous stop losses in the asset (in price amounts eg +2, +2.5, +2.6 USD - as opposed to percentages)

        take_profit_df : DataFrame
            Continuous take profits in the asset (in price amounts eg -2, -2.1, -2.5 USD - as opposed to percentages)

        Returns
        -------
        DataFrame containing amended signals that take into account stops and take profits

        """

        signal_data_frame_pushed = signal_data_frame  # signal_data_frame.shift(1)
        reset_points = ((
                                    signal_data_frame_pushed - signal_data_frame_pushed.shift(
                                1)).abs())

        # ensure all the inputs are pd DataFrames (rather than mixture of Series and DataFrames)
        asset_data_frame = pd.DataFrame(asset_data_frame)
        signal_data_frame = pd.DataFrame(signal_data_frame)
        stop_loss_df = pd.DataFrame(stop_loss_df)
        take_profit_df = pd.DataFrame(take_profit_df)

        non_trades = reset_points == 0

        # need to temporarily change column names to allow indexing (ASSUMES: columns in same order!)
        non_trades.columns = take_profit_df.columns

        # where we don't have a trade fill with NaNs
        take_profit_df[non_trades] = numpy.nan

        non_trades.columns = stop_loss_df.columns
        stop_loss_df[non_trades] = numpy.nan

        asset_df_copy = asset_data_frame.copy(deep=True)

        non_trades.columns = asset_df_copy.columns
        asset_df_copy[non_trades] = numpy.nan

        take_profit_df = take_profit_df.ffill()
        stop_loss_df = stop_loss_df.ffill()
        asset_df_copy = asset_df_copy.ffill()

        # take profit for buys
        ind1 = (asset_data_frame.values > (
                    asset_df_copy.values + take_profit_df.values)) & (
                       signal_data_frame.values > 0)

        # take profit for sells
        ind2 = (asset_data_frame.values < (
                    asset_df_copy.values - take_profit_df.values)) & (
                       signal_data_frame.values < 0)

        # stop loss for buys
        ind3 = (asset_data_frame.values < (
                    asset_df_copy.values + stop_loss_df.values)) & (
                           signal_data_frame.values > 0)

        # stop loss for sells
        ind4 = (asset_data_frame.values > (
                    asset_df_copy.values - stop_loss_df.values)) & (
                           signal_data_frame.values < 0)

        # when has there been a stop loss or take profit? assign those as being flat points
        ind = ind1 | ind2 | ind3 | ind4

        ind = pd.DataFrame(data=ind, columns=signal_data_frame.columns,
                           index=signal_data_frame.index)

        # for debugging
        # sum_ind = (ind == True).sum(); print(sum_ind)

        signal_data_frame[ind] = 0

        # those places where we have been stopped out/taken profit are additional trade "reset points", which we need to define
        # (already have ordinary buy/sell trades defined)
        reset_points[ind] = 1

        # where we don't have trade make these NaN and then fill down
        signal_data_frame[reset_points == 0] = numpy.nan
        signal_data_frame = signal_data_frame.ffill()

        return signal_data_frame

    # TODO
    def calculate_risk_stop_defined_signals(self, signal_data_frame,
                                            stops_data_frame):
        """

        Parameters
        ----------
        signal_data_frame : DataFrame
            Contains all the trade signals (typically mix of 0, +1 and +1

        stops_data_frame : DataFrame
            Contains 1/-1 to indicate where trades would be stopped out

        Returns
        -------
        DataFrame containing amended signals that take into account stops and take profits

        """

        signal_data_frame_pushed = signal_data_frame  # signal_data_frame.shift(1)
        reset_points = ((
                                    signal_data_frame_pushed - signal_data_frame_pushed.shift(
                                1)).abs())

        stops_data_frame = stops_data_frame.abs()
        ind = stops_data_frame >= 1

        ind.columns = signal_data_frame.columns
        signal_data_frame[ind] = 0

        reset_points[ind] = 1

        signal_data_frame[reset_points == 0] = numpy.nan
        signal_data_frame = signal_data_frame.ffill()

        return signal_data_frame

    def calculate_signal_returns_matrix(self, signal_data_frame,
                                        returns_data_frame, period_shift=1):
        """Calculates the trading strategy returns for given signal and asset
        as a matrix multiplication

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals

        returns_data_frame: DataFrame
            returns of asset to be traded

        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return pd.DataFrame(
            signal_data_frame.shift(
                period_shift).values * returns_data_frame.values,
            index=returns_data_frame.index)

    def calculate_signal_returns_with_tc(self, signal_data_frame,
                                         returns_data_frame, tc,
                                         period_shift=1):
        """Calculates the trading startegy returns for given signal and asset returns including
        transaction costs

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals

        returns_data_frame: DataFrame
            returns of asset to be traded

        tc : float
            transaction costs

        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """

        tc_costs = self.calculate_signal_tc(signal_data_frame, tc,
                                            period_shift)

        return signal_data_frame.shift(
            period_shift) * returns_data_frame - tc_costs

    def calculate_signal_returns_with_tc_from_prices(self, signal_data_frame,
                                                     prices_data_frame, tc,
                                                     period_shift=1):
        """Calculates the trading startegy returns for given signal and asset prices including
        transaction costs (and roll costs)

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals
        price_data_frame: DataFrame
            price of asset to be traded
        tc : float
            transaction costs
        period_shift : int
            number of periods to shift signal
        is_returns : bool (default: True)
            is the series of returns (as opposed to prices)

        Returns
        -------
        DataFrame
        """

        tc_costs = self.calculate_signal_tc(signal_data_frame, tc,
                                            period_shift)

        return signal_data_frame.shift(period_shift) * self.calculate_returns(
            prices_data_frame) - tc_costs

    def calculate_signal_returns_with_tc_matrix(self, signal_data_frame,
                                                returns_data_frame, tc,
                                                rc=None, period_shift=1):
        """Calculates the trading startegy returns for given signal and asset with transaction costs with matrix multiplication

        Parameters
        ----------
        signal_data_frame : DataFrame
            trading signals

        returns_data_frame: DataFrame
            returns of asset to be traded

        tc : float
            transaction costs

        rc : float
            roll costs

        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """

        # for transaction costs which vary by asset name

        # TODO add transaction costs which vary by size (maybe using a market impact model?)
        if isinstance(tc, dict):

            tc_ind = []

            for k in returns_data_frame.columns:
                try:
                    tc_ind.append(tc[k.split('.')[0]])
                except:
                    tc_ind.append(tc['default'])

            tc_ind = numpy.array(tc_ind)

            tc_costs = (numpy.abs(signal_data_frame.shift(
                period_shift).values - signal_data_frame.values) * tc_ind)
        elif isinstance(tc, pd.DataFrame):
            tc_ind = []

            # Get indices related to the returns
            for k in returns_data_frame.columns:
                try:
                    tc_ind.append(k.split('.')[0] + ".spread")
                except:
                    tc_ind.append('default.spread')

            # Don't include transaction costs at a portfolio level (TODO - weight it according to the assets)
            tc['Portfolio.spread'] = 0

            # Get associated transaction costs time series
            tc_costs = tc[tc_ind]

            # Make sure transaction costs are aligned to the signals
            signal_data_frame, tc_costs = signal_data_frame.align(tc_costs,
                                                                  join='left',
                                                                  axis='index')

            tc_costs = tc_costs.ffill()

            # Calculate the transaction costs by multiplying by trades
            tc_costs = (numpy.abs(signal_data_frame.shift(
                period_shift).values - signal_data_frame.values) * tc_costs.values)

        else:
            tc_costs = (numpy.abs(signal_data_frame.shift(
                period_shift).values - signal_data_frame.values) * tc)

        # Now handle roll costs
        if rc is not None:
            if isinstance(rc, dict):

                rc_ind = []

                for k in returns_data_frame.columns:
                    try:
                        rc_ind.append(rc[k.split('.')[0]])
                    except:
                        rc_ind.append(rc['default'])

                rc_ind = numpy.array(rc_ind)

                rc_costs = (numpy.abs(
                    signal_data_frame.shift(period_shift).values) * rc_ind)
            elif isinstance(rc, pd.DataFrame):
                rc_ind = []

                # Get indices related to the returns
                for k in returns_data_frame.columns:
                    try:
                        rc_ind.append(k.split('.')[0] + ".rc")
                    except:
                        rc_ind.append('default.rc')

                # Don't include roll costs at a portfolio level (TODO - weight it according to the assets)
                rc['Portfolio.rc'] = 0

                # Get associated roll costs time series
                rc_costs = rc[rc_ind]

                # Make sure transaction costs are aligned to the signals
                signal_data_frame, rc_costs = signal_data_frame.align(rc_costs,
                                                                      join='left',
                                                                      axis='index')
                
                # Given depreciated in later Pandas
                # rc_costs = rc_costs.fillna(0)
                rc_costs = rc_costs.replace(np.nan, 0)
                rc_costs = rc_costs.replace(None, 0)

                # Calculate the roll costs by multiplying by our position (eg. if position is zero, then no roll cost)
                rc_costs = (numpy.abs(signal_data_frame.shift(
                    period_shift).values) * rc_costs.values)

            else:
                rc_costs = (numpy.abs(
                    signal_data_frame.shift(period_shift).values) * rc)
        else:
            rc_costs = 0

        return pd.DataFrame(
            signal_data_frame.shift(
                period_shift).values * returns_data_frame.values - tc_costs - rc_costs,
            index=returns_data_frame.index)

    def calculate_returns(self, data_frame, period_shift=1):
        """Calculates the simple returns for an asset

        Parameters
        ----------
        data_frame : DataFrame
            asset price
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return data_frame / data_frame.shift(period_shift) - 1

    def calculate_diff_returns(self, data_frame, period_shift=1):
        """Calculates the differences for an asset

        Parameters
        ----------
        data_frame : DataFrame
            asset price
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return data_frame - data_frame.shift(period_shift)

    def calculate_log_returns(self, data_frame, period_shift=1):
        """Calculates the log returns for an asset

        Parameters
        ----------
        data_frame : DataFrame
            asset price
        period_shift : int
            number of periods to shift signal

        Returns
        -------
        DataFrame
        """
        return math.log(data_frame / data_frame.shift(period_shift))

    def create_mult_index(self, df_rets):
        """ Calculates a multiplicative index for a time series of returns starting from 100

        Parameters
        ----------
        df_rets : DataFrame
            asset price returns

        Returns
        -------
        DataFrame
        """

        df = 100.0 * (1.0 + df_rets).cumprod()

        # get the first non-nan values for rets and then start index
        # one before that (otherwise will ignore first rets point)
        # first_date_indices = df_rets.apply(lambda series: series.first_valid_index())
        # first_ord_indices = list()
        #
        # for i in first_date_indices:
        #     try:
        #         ind = df.index.searchsorted(i)
        #     except:
        #         ind = 0
        #
        #     if ind > 0: ind = ind - 1
        #
        #     first_ord_indices.append(ind)
        #
        # for i in range(0, len(df.columns)):
        #     df.iloc[first_ord_indices[i],i] = 100

        if isinstance(df, pd.Series):
            df = pd.DataFrame(df)

        # Probably a quicker way to do this, maybe using group by?
        for c in df.columns:
            df.loc[df[c].first_valid_index(), c] = 100

        return df

    def create_mult_index_from_prices(self, data_frame):
        """Calculates a multiplicative index for a time series of prices

        Parameters
        ----------
        df_rets : DataFrame
            asset price

        Returns
        -------
        DataFrame
        """
        return self.create_mult_index(self.calculate_returns(data_frame))

    def create_add_index(self, df_rets):
        """ Calculates a additive index for a time series of returns starting from 1

        Parameters
        ----------
        df_rets : DataFrame
            asset price returns

        Returns
        -------
        DataFrame
        """

        df = 1.0 + df_rets.cumsum()

        # get the first non-nan values for rets and then start index
        # one before that (otherwise will ignore first rets point)
        # first_date_indices = df_rets.apply(lambda series: series.first_valid_index())
        # first_ord_indices = list()
        #
        # for i in first_date_indices:
        #     try:
        #         ind = df.index.searchsorted(i)
        #     except:
        #         ind = 0
        #
        #     if ind > 0: ind = ind - 1
        #
        #     first_ord_indices.append(ind)
        #
        # for i in range(0, len(df.columns)):
        #     df.iloc[first_ord_indices[i],i] = 100

        if isinstance(df, pd.Series):
            df = pd.DataFrame(df)

        # Probably a quicker way to do this, maybe using group by?
        for c in df.columns:
            df.loc[df[c].first_valid_index(), c] = 1

        return df

    def create_add_index_from_prices(self, data_frame):
        """Calculates a additive index for a time series of prices

        Parameters
        ----------
        df_rets : DataFrame
            asset price

        Returns
        -------
        DataFrame
        """
        return self.create_add_index(self.calculate_returns(data_frame))

    def rolling_z_score(self, data_frame, periods):
        """Calculates the rolling z score for a time series

        Parameters
        ----------
        data_frame : DataFrame
            asset prices
        periods : int
            rolling window for z score computation

        Returns
        -------
        DataFrame
        """
        return (data_frame - data_frame.rolling(center=False,
                                                window=periods).mean()) / data_frame.rolling(
            center=False,
            window=periods).std()

    def expanding_z_score(self, data_frame, min_periods):
        """Calculates the exp z score for a time series

        Parameters
        ----------
        data_frame : DataFrame
            asset prices
        min_periods : int
            minimum window for z score computation

        Returns
        -------
        DataFrame
        """
        return (data_frame - data_frame.expanding(center=False,
                                                  min_periods=min_periods).mean()) / data_frame.expanding(
            center=False,
            min_periods=min_periods).std()

    def rolling_volatility(self, data_frame, periods, obs_in_year=252):
        """
        rolling_volatility - Calculates the annualised rolling volatility

        Parameters
        ----------
        data_frame : DataFrame
            contains returns time series
        obs_in_year : int
            number of observation in the year

        Returns
        -------
        DataFrame
        """

        # return pd.rolling_std(data_frame, periods) * math.sqrt(obs_in_year)
        return data_frame.rolling(window=periods,
                                  center=False).std() * math.sqrt(obs_in_year)

    def rolling_mean(self, data_frame, periods):
        return self.rolling_average(data_frame, periods)

    def rolling_average(self, data_frame, periods):
        """Calculates the rolling moving average

        Parameters
        ----------
        data_frame : DataFrame
            contains time series
        periods : int
            periods in the average

        Returns
        -------
        DataFrame
        """
        return data_frame.rolling(periods).mean()

    def rolling_sparse_average(self, data_frame, periods):
        """Calculates the rolling moving average of a sparse time series

        Parameters
        ----------
        data_frame : DataFrame
            contains time series
        periods : int
            number of periods in the rolling sparse average

        Returns
        -------
        DataFrame
        """

        # 1. calculate rolling sum (ignore NaNs)
        # 2. count number of non-NaNs
        # 3. average of non-NaNs
        foo = lambda z: z[pd.notnull(z)].sum()

        # rolling_sum = pd.rolling_apply(data_frame, periods, foo, min_periods=1)
        # rolling_non_nans = pd.stats.moments.rolling_count(data_frame, periods, freq=None, center=False, how=None) \
        rolling_sum = data_frame.rolling(center=False, window=periods,
                                         min_periods=1).apply(func=foo)
        rolling_non_nans = data_frame.rolling(window=periods,
                                              center=False).count()

        # For pd 0.18 onwards (TODO)
        # rolling_non_nans = data_frame.rolling(span=periods, freq=None, center=False, how=None).count()

        return rolling_sum / rolling_non_nans

    def rolling_sparse_sum(self, data_frame, periods):
        """Calculates the rolling moving sum of a sparse time series

        Parameters
        ----------
        data_frame : DataFrame
            contains time series
        periods : int
