import pandas as pd
import scipy.stats
import numpy as np

def drawdown(returns_series: pd.Series):
    '''
    Takes a time series of asset returns
    Computes and returns a DataFrame, that contains:
            the wealth index
            the previous peaks
            percent drawdowns
    '''
    wealth_index = 1000 * (1 + returns_series).cumprod()
    previous_peaks = wealth_index.cummax()
    drawdowns = (wealth_index - previous_peaks)/previous_peaks
    return pd.DataFrame({
        'Wealth': wealth_index,
        'Peaks': previous_peaks,
        'Drawdown': drawdowns
    })


def get_ffme_returns(cap_a, cap_b, label_a, label_b):
    '''
    Load the Fama-French Datasetfor the returns of the
    # Top and Bottom deciles by MarketCap
    given two deciles by MarketCap
    '''
    me_m = pd.read_csv('data/Portfolios_Formed_on_ME_monthly_EW.csv', header=0,
                   index_col=0, parse_dates=True, na_values=-99.99)
    # rets = me_m[['Lo 10', 'Hi 10']]
    rets = me_m[[cap_a, cap_b]]
    rets.columns = [label_a, label_b]
    rets = rets/100
    rets.index = pd.to_datetime(rets.index, format='%Y%m').to_period('M')
    
    return rets

def get_hfi_returns():
    '''
    Load and format the EDHEC Hedge Fund Index Returns
    '''
    hfi = pd.read_csv('data/edhec-hedgefundindices.csv',
                     header=0, index_col=0, parse_dates=True)
    hfi = hfi/100
    hfi.index = hfi.index.to_period('M')
    
    return hfi


def get_ind_returns():
    '''
    load and format the Ken French 30 Industry Portfolios Value weighted monthly Returns
    '''
    ind = pd.read_csv('data/ind30_m_vw_rets.csv', header=0, index_col=0, parse_dates=True)/100
    ind.index = pd.to_datetime(ind.index, format="%Y%m").to_period('M')
    ind.columns = ind.columns.str.strip()
    return ind


def semideviation(returns):
    '''
    Returns the semideviation aka negative semideviation or returns
    returns must be a series or a dataframe
    '''
    is_negative = returns < 0
    return returns[is_negative].std(ddof=0)
    

def skewness(r):
    '''
    Alternative to scipy.stats.skew()
    Coumputes the skewness of supplied Series or Dataframe
    returns a float or a Series
    '''
    demeaned_r = r - r.mean()
    # use the population standard deviation, so set ddof = 0
    sigma_r = r.std(ddof=0)
    exp = (demeaned_r ** 3).mean()
    return exp/sigma_r**3


def kurtosis(r):
    '''
    Alternative to scipy.stats.kurtosis()
    Coumputes the kurtosis of supplied Series or Dataframe
    returns a float or a Series
    '''
    demeaned_r = r - r.mean()
    # use the population standard deviation, so set ddof = 0
    sigma_r = r.std(ddof=0)
    exp = (demeaned_r ** 4).mean()
    return exp/sigma_r**4


def is_normal(r, level=0.01):
    '''
    Applies the Jarque Bera Test to determine if a series is normal or not
    Test is applies at a 1% level by default
    Returns True if the hypothesis is accepted, False otherwise
    '''
    statistics, p_value = scipy.stats.jarque_bera(r)
    return p_value > level
    
    
def var_historic(r, level=5):
    '''
    returns the historic value at Risk at a specified level
    i.e. returns the number such that "level" percent of the returns
    fall below that number, and the (100-level) percent are above
    '''
    if isinstance(r, pd.DataFrame):
        return r.aggregate(var_historic, level=level)
    elif isinstance(r, pd.Series):
        return -np.percentile(r, level)
    else:
        raise TypeError('Expected r to be a pandas Series or a DataFrame')

from scipy.stats import norm

def var_gaussian(r, level=5, modified=False):
    '''
    Returns the Parametric Gaussian VaR of a Series or DataFrame
    If "modified" is True, then the modified VaR is returned,
    using the Cornish-Fischer modification
    '''
    # compute the Z score assuming it was Gaussian
    z = norm.ppf(level/100)
    if modified:
        # modify the Z score based on observed skewness and kurtosis
        s = skewness(r)
        k = kurtosis(r)
        z = (z + 
                (z**2 - 1) * s/6 +
                (z**3 - 3*z)*(k-3)/24 -
                (2*z**3 - 5*z) * (s**2)/36
            )
        
    return -(r.mean() + z * r.std(ddof=0))


def cvar_historic(r, level=5):
    '''
    Computes the Conditional VaR of Series or a DataFrame
    '''
    if isinstance(r, pd.Series):
        is_beyond = r<= -var_historic(r, level=level)
        return -r[is_beyond].mean()
    elif isinstance(r, pd.DataFrame):
        return r.aggregate(cvar_historic, level=level)
    else:
        raise TypeError('Expected r to be a Series or a DataFrame')
    
def annualize_rets(r, periods_per_year):
    '''
    Annualizes a set of returns
    We should infer the periods per year
    but that is currently left as an exercise
    '''
    compunded_growth = (1+r).prod()
    n_periods = r.shape[0]
    return compunded_growth ** (periods_per_year/n_periods) - 1

def annualize_vol(r, periods_per_year):
    '''
    Annualizes the volatility of a set of returns
    We should infer the periods per year
    but that is currently left as an exercise
    to the reader
    '''
    return r.std()*(periods_per_year ** 0.5)
    

def sharpe_ratio(r, riskfree_rate, periods_per_year):
    '''
    Computes the annualized sharpe ratio of a set of returns
    '''
    # convert the annual riskfree rate to per period
    rf_per_period = (1 + riskfree_rate) ** (1/periods_per_year) - 1
    excess_ret = r - rf_per_period
    ann_ex_ret = annualize_rets(excess_ret, periods_per_year)
    ann_vol = annualize_vol(r, periods_per_year)
    return ann_ex_ret/ann_vol
    

def portfolio_return(weights, returns):
    '''
    Weights -> Returns
    '''
    return weights.T @ returns

def portfolio_vol(weights, covmat):
    '''
    Weights -> Vol
    '''
    return (weights.T @ covmat @ weights) ** 0.5


def plot_ef2(n_points, er, cov, style='.-'):
    '''
    plots the two-asset Eifficient Frontier
    '''
    if er.shape[0] != 2:
        raise ValueError('Plot ef2 can only plot two assets')
    weights = [np.array([w, 1-w]) for w in np.linspace(0, 1, n_points)]
    rets = [portfolio_return(w, er) for w in weights]
    vols = [portfolio_vol(w, cov) for w in weights]
    ef = pd.DataFrame({'Returns': rets, 'Volatility': vols})
    return ef.plot.line(x='Volatility', y='Returns', style=style)
    
from scipy.optimize import minimize    

def minimze_vol(target_return, er, cov):
    '''
    target_return -> W
    '''
    n = er.shape[0]
    init_guess = np.repeat(1/n, n)
    bounds = ((0.0, 1.0), ) * n
    return_is_target = {
        'type': 'eq',
        'args': (er,), 
        'fun': lambda weights, er: target_return - portfolio_return(weights, er) # target_is_met
    }
    weights_sum_to_1 = {
        'type': 'eq',
        'fun': lambda weights: np.sum(weights) - 1
    }
    results = minimize(portfolio_vol, init_guess, args=(cov, ), method='SLSQP',
                      options={'disp':False},
                      constraints=(return_is_target, weights_sum_to_1), bounds=bounds)
    return results.x


def optimal_weights(n_points, er, cov):
    '''
    --> list of weights to run the optimizer on to minimize the vol
    '''
    target_rs = np.linspace(er.min(), er.max(), n_points)
    weights = [minimze_vol(target_return, er, cov) for target_return in target_rs]
    return weights


def plot_efn(n_points, er, cov, style='.-'):
    '''
    plots the n-asset Eifficient Frontier
    '''
    weights = optimal_weights(n_points, er, cov)
    rets = [portfolio_return(w, er) for w in weights]
    vols = [portfolio_vol(w, cov) for w in weights]
    ef = pd.DataFrame({'Returns': rets, 'Volatility': vols})
    return ef.plot.line(x='Volatility', y='Returns', style=style)
