import time
import pandas as pd
import numpy as np
from scipy import stats
import string
import matplotlib.pyplot as plt
import cvxopt as cp
from cvxopt import blas, solvers
import scipy.optimize as sco
from scipy.optimize import Bounds, LinearConstraint

num_of_assets = 10
num_of_periods = 10

data = pd.DataFrame(np.random.uniform(0,.2,size=(num_of_periods,num_of_assets)),
                            columns=list(string.ascii_uppercase[0:num_of_assets])) 


def get_portfolio_sd(weights,covariance):
    #get standard deviation of portfolio weight combo given weights and covariance matrix
    Std_Dev = np.sqrt(np.dot(weights.T,np.dot(covariance,weights)))

    return Std_Dev

def ret_risk(W, exp_ret, cov):
    return -((W.T@exp_ret) / (W.T@cov@W)**0.5)

def optimize(func, W, exp_ret, cov, target_return):
    opt_bounds = Bounds(0,1)
    opt_constraints = ({'type': 'eq',
                       'fun': lambda W: 1.0- np.sum(W)},
                       {'type':'eq',
                        'fun': lambda W: target_return-W.T@exp_ret})
    optimal_weights = sco.minimize(func,W,
                        args=(exp_ret,cov),
                        method='SLSQP',
                        bounds = opt_bounds,
                        constraints = opt_constraints)

    return optimal_weights['x']

def get_frontier_weights(mean,cov,min_ret,max_ret):

    step = (max_ret-min_ret)/1000
    ret_array = np.arange(min_ret,max_ret+step,step)
    optimal_portfolios = []

    for ret_level in ret_array:
        W = np.ones(10)
        optimal_portfolios.append(optimize(ret_risk,W,mean,cov,target_return=ret_level))
    
    return optimal_portfolios

def build_portfolio_stats(return_data,weights_table):
    """
    Generate scatterplot of portfolios and relevant statistics corresponding to a given increment
    """
    weights_table = weights_table.div(100)

    cov = return_data.cov(ddof=0)

    mean = return_data.apply(stats.gmean,axis=0)
    portfolio_return = weights_table.multiply(np.array(mean), axis = 'columns')

    portfolios = pd.DataFrame()
    portfolios['Expected_Return'] = portfolio_return.sum(axis = 'columns')
    portfolios['Std_Dev'] = weights_table.apply(lambda x: get_portfolio_sd(x,cov), axis = 'columns')
    portfolios['Sharpe'] = portfolios['Expected_Return']/portfolios['Std_Dev']

    #get positional arguments for max sharpe ratio portfolio
    max_sr_ret = portfolios['Expected_Return'][portfolios['Sharpe'].argmax()]
    max_sr_vol = portfolios['Std_Dev'][portfolios['Sharpe'].argmax()]

    min_vol_ret = portfolios['Expected_Return'][portfolios['Std_Dev'].argmin()]
    max_ret_ret = portfolios['Expected_Return'][portfolios['Expected_Return'].argmax()]

    
    optimal_portfolios = pd.DataFrame.from_dict(get_frontier_weights(mean,cov,min_vol_ret,max_ret_ret))
    optimal_returns = optimal_portfolios.multiply(np.array(mean), axis = 'columns')
    optimal_portfolios['Std_Dev'] = optimal_portfolios.apply(lambda x: get_portfolio_sd(x,cov), axis = 'columns')
    optimal_portfolios['Expected_Return'] = optimal_returns.sum(axis = 'columns')
    optimal_portfolios['Sharpe'] = optimal_portfolios['Expected_Return']/optimal_portfolios['Std_Dev']

    #plot portfolios
    plt.figure(figsize=(12,12))
    plt.scatter(portfolios['Std_Dev'], portfolios['Expected_Return'], c=portfolios['Sharpe'], cmap='viridis')
    plt.colorbar(label='Sharpe Ratio')
    plt.xlabel('Volatility')
    plt.ylabel('Return')
    plt.plot(optimal_portfolios['Std_Dev'],optimal_portfolios['Expected_Return'])
    plt.scatter(max_sr_vol, max_sr_ret,c='red', s=50) # red dot
    plt.show()

def main():
    combos = pd.read_csv('combos10.csv',index_col=False)
    build_portfolio_stats(data,combos)
    
if __name__ == '__main__':
    main()