import numpy as np
import scipy as sp
import pandas as pd
import pandas.io.data as web
import matplotlib.pyplot as plt
import scipy.optimize as opt
from pandas.io.pytables import HDFStore
from datetime import datetime, timedelta
from Selector import Selector
from optimizers import *
import rs

# from scipy import optimize
# from scipy.optimize import basinhopping


class Portfolio:  

  def __init__(self, assets, data, weights=None, date=None, window=365, maxAssets=5, investment=100000.0, transaction_cost=5.0, selector=None):    
    self.portfolio = pd.DataFrame(index=assets, columns=['weights'])     
    self.assets = pd.Series(assets.tolist(), index=assets)        
    self.data = data
    self.maxAssets = maxAssets
    self.selector = selector
    self.setup(date, window)    

    self.investment = investment
    self.transaction_cost = transaction_cost    
    # select assets
    # print self.portfolio.loc["^VIX"]
    # vix = self.portfolio.loc["^VIX"]
    # self.portfolio = self.portfolio.sort("sortino", ascending=False)[:maxAssets] # [(self.portfolio.sharpe > 0)]
    # if not "^VIX" in self.portfolio.index:
    #     self.portfolio = self.portfolio.append(vix)
    # self.assets = pd.Series(self.portfolio.index.tolist(), index=self.portfolio.index)        

    # if weights is None:
    #   weights = pd.Series(1.0/ len(self.portfolio.index) , index = self.portfolio.index)    # 1.0/ assets.count()
    # self.portfolio.weights = weights    

  
  def setup(self, date=None, window=None):
    """ Set date and window of portfolio to be optimized """    

    self.date = date or self.date or self.data.assets["^GSPC"].index[-1]     
    self.window = window or self.window
    self.returns = pd.DataFrame(index=self.data.assets["^GSPC"].index.to_series()[self.date-timedelta(days=self.window):self.date])    
    # self.portfolio.weights = 1.0/ len(self.portfolio.index) 

    for symbol in self.assets:    
        self.returns[symbol] = self.data.assets[symbol]["excess_daily_ret"][self.date-timedelta(days=self.window):self.date].dropna()            
    

    if self.selector is None:
        # check if data are valid - from self.date - window to self date
        # drop assets that have more than one NaN in window
        self.returns.dropna(axis=1, inplace=True)             

        #update portfolio assets in index   
        self.portfolio = pd.DataFrame(index=list(self.returns.columns.values), columns=['weights'])   

        # perform selection for simulation iteration purposes
        self.portfolio['sortino'] = self.portfolio.index.map(lambda symbol : self.sortino(self.returns[symbol])) 
        self.portfolio = self.portfolio.sort("sortino", ascending=False)[:self.maxAssets] 
    else:
        selection = Selector(self.data, date=self.date, window=self.window, maxAssets=self.maxAssets, mode=self.selector).select()        
        self.portfolio = pd.DataFrame({'sortino':selection['sortino_c']} ,index=selection.index, columns=['weights', 'sortino'])                 
        print "Portfolio set up with selector:"
        print self.portfolio

    self.returns = self.returns[self.portfolio.index]   

    # calculate other assets statistics
    self.portfolio['mean'] = self.portfolio.index.map(lambda symbol : self.returns[symbol].mean()) 
    self.portfolio['std'] = self.portfolio.index.map(lambda symbol : self.returns[symbol].std()) 
    self.portfolio['sharpe'] = self.portfolio.index.map(lambda symbol : np.sqrt(252) * self.portfolio["mean"][symbol] / self.portfolio["std"][symbol])             

    # equally distributed allocation
    self.setWeights()
    # self.portfolio.weights = pd.Series(1.0/ len(self.portfolio.index) , index = self.portfolio.index)    # 1.0/ assets.count()     


    # create correlations matrix - does not change by changing weights
    # optimize not to calculate rolling correlations, only one at date - window    
    # self.corellationMatrix = pd.rolling_corr_pairwise(self.returns, self.returns.iloc[0].count())[self.returns.index[-1]]          
    # self.onesMatrix = self.corellationMatrix * 0 + 1    
    # self.riskMatrix = self.onesMatrix.mul(self.portfolio['std']).mul(self.portfolio['std'], 'index') 


  def __str__(self):        
    return  "".join([self.portfolio.to_string(), "\nPortfolio sortino: ", str(self.sortino()), "\nWeights sum: ", str(self.portfolio.weights.sum()), " count: ", str(self.portfolio.weights.count()), "\nDate: ", str(self.date) ])

  def setWeights(self, weights=None):
    """ transform weights to comply with constraints and set up """
    if weights is None:
        self.portfolio.weights = pd.Series(1.0/ len(self.portfolio.index), index = self.portfolio.index)
    else:
        self.portfolio.weights = pd.Series(weights, index=self.portfolio.index)        

    self.portfolio.weights[weights<0.001] = 0.0
    self.portfolio.weights *= 1/self.portfolio.weights.sum()


  def utilityFunction(self, weights=None, *params):
    if weights is None:
        weights = self.portfolio.weights
    self.setWeights(weights)
    
    diversificationPenalty = self.portfolio.weights[weights<0.001].count()*(3/self.portfolio.weights.count())

    period_returns = self.getReturns()
    
    if period_returns.count()>0:
        # calculate cost of buying and selling of assets in portfolio and spread costs on periodic return as percentage of investment
        #calculate units of cost - low weights have linearly scaled cost up to 1/n (equal state) then 1
        n = self.portfolio.weights.count() # assets count
        cost_units = self.portfolio.weights[weights>=1.0/n].count() #whole cost units
        cost_units += (self.portfolio.weights[weights<1.0/n] / (1.0/n)).sum() #partial cost units        
        
        period_costs = 2.0*self.transaction_cost*cost_units
        period_returns =  period_returns - period_costs / self.investment / period_returns.count() # sub percentual loss over period        

        
    
    result = - self.sortino(period_returns) + diversificationPenalty
    if self.u and self.u[-1]>result:
        self.u.append(result)
    self.c += 1
    return result
    # return - rs.sortino(self.getReturns(), 0) + diversificationPenalty

  def optimize(self, type="anneal", opts={}, date=None):
    print "Optimizing portfolio : ", type    
    self.u = [0]
    self.c = 0
    
    # set returns for specified time frame (time, window)
    # self.setup(date, self.window)    
    print "Date :", self.date, ", window :", self.window, "days"

    optimizers = {
      'anneal' : anneal.optimize,
      'basinhopping' : basinhopping.optimize,
      'pso' : pso.optimize
    }
    
    return optimizers[type](self, opts=opts)       

  def sharpe(self):    
    """ Calculate sharpe ratio of portfolio from precalculated matrices and variable asset weights """    
    weightsMatrix = self.onesMatrix.mul(self.portfolio.weights).mul(self.portfolio.weights, 'index')
    finalMatrix = self.corellationMatrix * self.riskMatrix * weightsMatrix      
    
    risk =  finalMatrix.sum().sum() ** 0.5
    meanDailyRerutn = (self.portfolio["mean"] * self.portfolio["weights"]).sum()
    sharpe = np.sqrt(252) * meanDailyRerutn / risk
    return sharpe

  def sortino(self, rets=None ):    
      """
      @summary Returns the daily Sortino ratio of the returns.     
      @return Sortino Ratio, computed off daily returns
      """
      # print('.')
      # if returns to calculate not set, use whole portfolio window with weights
      if rets is None:
        rets = self.getReturns()

      return rs.sortino_d(rets, 0)
      # return rs.sortino_c(rets, 0)
        
      # self.print_full(rets)
      # f_mean = rets.mean()
      # negative_rets = rets[rets < 0]      
      # f_dev = negative_rets.std()
      # f_sortino = (f_mean*252 - risk_free) / (f_dev * np.sqrt(252))
      # return f_sortino
  
  # @staticmethod      
  # def staticSortino(rets):
  #   """ Returns simplified sortino ratio using discrete values for calculating downside_risk, target return = 0  """
  #   f_mean = rets.mean()
  #   days = rets.count()
  #   negative_rets = rets[rets < 0]          
  #   downside_risk = np.sqrt((negative_rets**2).mean())
  #   f_sortino = f_mean * np.sqrt(days) / downside_risk
  #   return f_sortino    

  # @staticmethod      
  # def staticSortino2(rets=None, risk_free=0.00 ):
  #   """ Returns simplified sortino ratio using discrete values for calculating downside_risk, target return = 0  """
  #    # self.print_full(rets)
  #   f_mean = rets.mean()
  #   days = rets.count()
  #   negative_rets = rets[rets < 0]      
  #   f_dev = negative_rets.std()
  #   f_sortino = f_mean*np.sqrt(days) / f_dev
  #   return f_sortino   

  def getNormalizedReturns(self, days=30, endDate=None):
    """ Returns normalized future returns of portfolio starting self.date (= 1.0), ending self.date+days (or endDate) """
    endDate = endDate or self.date+timedelta(days=days)  

    # get first date in index - some days are not business days, we need one that is and is <= self.date
    dividerDate = self.data.assets["^GSPC"][:self.date].index[-1]    

    # normalized returns = price_series / price_series[0] (1.0, 1.01, 0.98, ...)
    returns = self.portfolio.index.map(lambda symbol : self.data.assets[symbol]["Adj Close"][self.date:endDate] / self.data.assets[symbol]["Adj Close"][dividerDate]) 
    return (returns * self.portfolio.weights).sum(axis=1).dropna()

  def getReturns(self):
    """Returns sumarized returns for current portfolio window"""
    return (self.returns * self.portfolio.weights).sum(axis=1)

  def getPortfolioFutureReturns(self, days=365):
    futureReturns = pd.DataFrame()
    for symbol in self.portfolio.index:
          futureReturns[symbol] = self.data.assets[symbol]["excess_daily_ret"][self.date:self.date+timedelta(days=days)].dropna()            
    return (futureReturns * self.portfolio.weights).sum(axis=1)      


  def shift(self, days=30):
    self.setup(self.date+timedelta(days=days))

  def print_full(self, x):
    pd.set_option('display.max_rows', len(x))
    print(x)
    pd.reset_option('display.max_rows')