Overall Statistics
Total Trades
91
Average Win
0.31%
Average Loss
-0.63%
Compounding Annual Return
14.769%
Drawdown
8.700%
Expectancy
0.120
Net Profit
4.714%
Sharpe Ratio
0.972
Probabilistic Sharpe Ratio
47.770%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
0.49
Alpha
0.135
Beta
0.12
Annual Standard Deviation
0.167
Annual Variance
0.028
Information Ratio
-0.268
Tracking Error
0.232
Treynor Ratio
1.35
Total Fees
$91.00
#insights.append(Insight(symbol, self.insightsTimeDelta, InsightType.Price, symbolData.InsightDirection, None,None, None,0.1))
                #algorithm.Log(f"{symbol}\tMOM\t[{symbolData.fmom}]\t{round(symbolData.mom.Current.Value,2)}\tKAMA\t[{symbolData.fkama}]\t{round(symbolData.kama.Current.Value,2)}\
                #                \tPrice\t{symbolData.price}\tROC\t[{symbolData.froc}]\t{round(symbolData.roc.Current.Value,4)}\tEMA\t[{symbolData.fema}]\tEMA-13\t{round(symbolData.ema13.Current.Value,2)}\
                #                \tEMA-63\t{round(symbolData.ema63.Current.Value,2)}\tEMA-150\t{round(symbolData.ema150.Current.Value,2)}\taction\t{symbolData.InsightDirection}")

            
          #self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        #self.SetPortfolioConstruction(MeanVarianceOptimizationPortfolioConstructionModel(param.resolution,PortfolioBias.LongShort,1,63,param.resolution,0.02,MaximumSharpeRatioPortfolioOptimizer(0,1,0)))
        
        
            # self.rebalancingPeriod  = Expiry.EndOfMonth
            #pcm = InsightWeightingPortfolioConstructionModel(lambda time: param.rebalancingPeriod(time))
            
                        #self.InsightDirection   =   InsightDirection.Up
                                    #self.InsightDirection = InsightDirection.Flat # liqudates position - work around InsightDirection.Down which may sell and then short
import numpy as np
import pandas as pd
from datetime import datetime, date
from datetime import datetime, timedelta
from PortfolioOptimizerClass import PortfolioOptimizer

from clr import AddReference
AddReference("QuantConnect.Indicators")
from QuantConnect.Indicators import *

# TODO :
# fix buying daily
# Universe selection
# short selling model
# selling hourly
# rebalance weekly (weight based on RS?)
# look into small stocks large moves 35.65->33.51 which is 6% ; control via draw down?
# self.SetBrokerageModel(AlphaStreamsBrokerageModel()) # learn more about this
# fix if dt >9 and dt<18
# DONE: fix hourlyHouseKeeping
# 10 days daily STD for ROKU on 7 Jan 21 is 12.15529, mine (based on open) is 12.3907448
 

from System.Drawing import Color

class ModelA(AlphaModel): 
    
    def __init__(self, param):

        self.param                  = param
        self.symbolDataBySymbol     = {}
        self.modelResolution        = param.resolution
        self.insightsTimeDelta      = param.timedelta
        self.objectiveFunction      = param.pcmObjectiveFunction
        self.lookbackOptimization   = param.pcmLookbackOptimization
        self.portOpt                = PortfolioOptimizer(minWeight = 0, maxWeight = 1)
        self.startingMaxHoldingLimit = param.startingMaxHoldingLimit 
        
    def OnSecuritiesChanged(self, algorithm, changes):
            for added in changes.AddedSecurities:
                symbolData = self.symbolDataBySymbol.get(added.Symbol)
                if symbolData is None:
                    symbolData = SymbolData(added.Symbol, algorithm, self.param)
                    self.symbolDataBySymbol[added.Symbol] = symbolData
 
    def Update(self, algorithm, data):
        
        insights=[]
        liquidate_now=[]

        invested = [ x.Symbol.Value for x in algorithm.Portfolio.Values if x.Invested ] # can we make this easier via key query?
       
        for symbol, symbolData in self.symbolDataBySymbol.items():
 
            isInvested= str(symbol) in invested  
            
            if symbol != self.param.benchmark:

                symbolData.getInsight(algorithm.Securities[symbol].Price, isInvested) # Latest known price; we are at 12:00 and the last trade at 10.57 
            
                if symbolData.trade:
                    
                    if symbolData.liquidate:
                        invested.remove(str(symbol))
                        liquidate_now.append(str(symbol))
                    else:
                        invested.append(str(symbol))
                    

        # calculate optimal weights
        if invested:

            weights = self.CalculateOptimalWeights(algorithm, invested, self.objectiveFunction, self.lookbackOptimization)

            for symbol in invested:

                weight = weights[str(symbol)]
                if weight>self.startingMaxHoldingLimit and len(invested)<1/self.startingMaxHoldingLimit: weight=self.startingMaxHoldingLimit
                insights.append(Insight.Price(symbol, self.insightsTimeDelta, InsightDirection.Up,
                                 None,  None, None, weight))
        if liquidate_now:
            for symbol in liquidate_now:
                insights.append(Insight.Price(symbol, self.insightsTimeDelta, InsightDirection.Flat,
                                 None, None, None, 1))
            
            
        return insights

    def CalculateOptimalWeights(self, algorithm, symbols, objectiveFunction, lookbackOptimization):
            
        # get historical close prices
        historyClosePrices = algorithm.History(symbols, lookbackOptimization, Resolution.Daily)['close'].unstack(level = 0)
        
        # calculate daily returns
        returnsDf = historyClosePrices.pct_change().dropna()
        # rename the columns in the dataframe in order to have tickers and not symbol strings
        columnsList = list(returnsDf.columns)
        returnsDf.rename(columns = {columnsList[i]: algorithm.ActiveSecurities[columnsList[i]].Symbol.Value for i in range(len(columnsList))}, inplace = True)
        
        # calculate optimal weights
        weights = self.portOpt.Optimize(objectiveFunction, returnsDf)
        # convert the weights to a pandas Series
        weights = pd.Series(weights, index = returnsDf.columns, name = 'weights')
        
        return weights
        
class FrameworkAlgorithm(QCAlgorithm):
    
    def Initialize(self):

        param=paramData()
        symbols             =   [Symbol.Create(x, SecurityType.Equity, Market.USA) for x in param.tickers]
        
        self.SetStartDate(param.dateFrom[0],param.dateFrom[1],param.dateFrom[2])   
        self.SetEndDate(param.dateTo[0],param.dateTo[1],param.dateTo[2])   
        self.SetCash(param.cash)           
        
        self.liquidationBarrier=param.cash*param.stopLoss*-1
        
        self.SetBenchmark(param.benchmarkTicker)
        param.setBenchmark(self.AddEquity(param.benchmarkTicker,param.resolution).Symbol)
        
        self.UniverseSettings.Resolution = param.resolution
        self.SetWarmUp(timedelta(param.warmup)) 
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        self.SetBrokerageModel(AlphaStreamsBrokerageModel()) # learn more about this
        self.SetAlpha(ModelA(param))
                                
        myPCM = InsightWeightingPortfolioConstructionModel(rebalance = timedelta(days=252), portfolioBias = PortfolioBias.Long)
        myPCM.RebalanceOnInsightChanges = False
        myPCM.RebalanceOnSecurityChanges = False 
        self.SetPortfolioConstruction(myPCM)
        
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(param.maxDrawDown)) # NullRiskManagementModel() or MaximumDrawdownPercentPerSecurity(param.maxDrawDown)  > drop in profit from the max >> done daily / TODO: redo hourly? or 
        self.SetExecution(ImmediateExecutionModel())
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(param.runEveryXminutes)), self.hourlyHouseKeeping)

    def hourlyHouseKeeping(self):
        
            # Fail Safe - If our strategy is losing than acceptable (something is wrong)
            # Strategy suddenly losing money or logic problem/bug we did't catch when testing
            pnl= sum([self.Portfolio[symbol].NetProfit for symbol in self.Portfolio.Keys])
            #if self.LiveMode:
            if pnl < self.liquidationBarrier: 
                self.Debug(f"Fallback event triggered, liquidating with total portfolio loss of {pnl}")
                self.Liquidate()
                self.Quit()
            
            dt=int(self.Time.hour)
            if dt >9 and dt<18: # if not set still prints out of hours for self.IsMarketOpen("SPY")
                if (self.IsMarketOpen("SPY") and self.Portfolio.Invested):
                     #self.Log("\n\nPortfolio")
                    summary = {}
    
                    invested = [ x.Symbol.Value for x in self.Portfolio.Values if x.Invested ]
                    for symbol in invested:
                    
                        hold_val    = round(self.Portfolio[symbol].HoldingsValue, 2)
                        abs_val     = round(self.Portfolio[symbol].AbsoluteHoldingsValue, 2)
                        pnl         = round(self.Portfolio[symbol].UnrealizedProfit, 2)
                        qty         = self.Portfolio[symbol].Quantity
                        price       = self.Portfolio[symbol].Price
                        
                        summary[symbol]=[hold_val,abs_val,pnl,qty,price]
                        
                    df=pd.DataFrame(summary)
                    df.index = ['hold_val', 'abs_val', 'pnl', 'qty','price']
                    df=df.T
                    hold_val_total= abs(df['hold_val']).sum()
                    df = df.assign(weight=abs(df['hold_val'])/hold_val_total)
                     #self.Log(df)
                     #self.Log("\n\n")

class paramData:
    def __init__(self):
        self.dateFrom                   = (2020,9,1)    
        self.dateTo                     = (2021,1,1)   
        self.cash                       = 50000                     # how to top this up after going live?
        self.warmup                     = 28                        # starts from self.dateFrom
        self.resolution                 = Resolution.Hour           # 10-11, etc Daily data is midnight to mifnight, 12AM EST 
        self.tickers                    = ["MSFT","ROKU","ANET","FSLY"]                  # how do I change this on request?
        #self.resolution                 = Resolution.Daily           # 10-11, etc Daily data is midnight to mifnight, 12AM EST 
        #self.tickers                    = ["MSFT","MRNA","MELI","ROKU","ANET","XRX","SHOP","EBAY","CSCO","ORCL","NOW","THO","BIDU","SPOT","DOCU","DDOG","SQ","FSLY","TMO","PFE","IOVA","EXEL","ACLS","BNTX","IBM"]
        self.tickers_len                = len(self.tickers)
        self.timedelta                  = timedelta(hours=240)
        self.maxDrawDown                = 0.05
        self.runEveryXminutes           = 60                        # Schedule frequency
        self.benchmarkTicker            = 'SPY'                     # can be ticker as a part of the dictionary ["MSFT:SPY"]
        self.pcmObjectiveFunction       = 'equalWeighting'          #'equalWeighting' 'maxReturn' 'riskParity' 
        self.pcmLookbackOptimization    = 63
        self.stopLoss                   = 0.15                      # 15% of the total cash invested
        self.startingMaxHoldingLimit    = 0.17                      # we do not allocate more than this % for each security 

        
    def setBenchmark(self, symbol):
        self.benchmark          =  symbol
        
class SymbolData:
    
    def __init__(self, symbol, algorithm, param):
        
        self.symbol             = symbol
        self.algorithm          = algorithm
        self.param              = param
        self.resolution         = param.resolution
        self.price              = 0.00 # last trading price
        self.lastPricePaidRef   = 0.00 # last purchase price reference; update with an actual price
        self.kama               = algorithm.KAMA(symbol, 10,2,30, self.resolution)
        self.variationRate      = 0.95 # tolerance level to avoid buy and immediate sell scenario
        self.mom                = algorithm.MOM(symbol, 14, self.resolution)
        self.roc                = algorithm.ROC(symbol, 9, self.resolution) 
        self.ema13              = algorithm.EMA(symbol, 13, self.resolution)
        self.ema63              = algorithm.EMA(symbol, 63, self.resolution)
        self.ema150             = algorithm.EMA(symbol, 150, self.resolution)
        self.fkama              = False
        self.fmom               = False
        self.froc               = False
        self.fema               = False
        self.rsStock            = False
        self.rsIdx              = False
        self.fbenchmark         = False
        self.lookback           = 10
        self.std                = algorithm.STD(symbol, self.lookback,self.resolution) 
        self.magnitude          = 0.025#algorithm.IndicatorExtensions.SMA(RateOfChangePercent(1),self.lookback).Current.Value
        self.lastDateTraded     = self.algorithm.Time.date()
        
        
        # Chart Plotting 
        self.kama.Updated       += self.getRSL
        self.kama.Updated       += self.OnSymbolDataUpdate
        self.dataPlot = Chart('Detail'+str(self.symbol))
        self.dataPlot.AddSeries(Series('Price', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('Kama', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('MOM', SeriesType.Line, ''))
        self.dataPlot.AddSeries(Series('EMA13', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('EMA63', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('EMA150', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('ROC', SeriesType.Line, ''))
        self.dataPlot.AddSeries(Series('RS-idx', SeriesType.Line, ''))
        self.dataPlot.AddSeries(Series('Std', SeriesType.Line, '$'))
        self.dataPlot.AddSeries(Series('Buy', SeriesType.Scatter, '$', Color.Green,ScatterMarkerSymbol.Circle))
        self.dataPlot.AddSeries(Series('Sell', SeriesType.Scatter, '$', Color.Red,ScatterMarkerSymbol.Circle))
        self.algorithm.AddChart(self.dataPlot)
        
    def getInsight(self, price, isInvested):    
        
        self.price              = price
        self.fkama_buy          = self.price>self.kama.Current.Value
        self.fkama_sell         = self.price<self.kama.Current.Value*self.variationRate
        self.fmom               = self.mom.Current.Value>0
        self.froc               = self.roc.Current.Value>0
        self.fema               = self.ema13.Current.Value>self.ema63.Current.Value>self.ema150.Current.Value

        self.trade              = False
        self.liquidate          = False
        
        self.fbenchmark         = self.rsStock>self.rsIdx 
        self.dateTradedDelta    = (self.algorithm.Time.date()-self.lastDateTraded).days
        #  and self.froc self.fmom and 
        
        self.algorithm.Log(f"{str(self.symbol)}\t{str(self.algorithm.Time.date())}\tTraded\t{str(self.lastDateTraded)}\tDt\t{str(self.dateTradedDelta)}\tstd\t{self.std}\tclose\t{self.price}")    
    
        if not isInvested and self.fkama_buy and self.fema and self.fbenchmark:

            self.trade              =   True
            self.lastDateTraded     =   self.algorithm.Time.date() 
            self.algorithm.Plot('Detail'+str(self.symbol),'Buy', self.price)
        
            self.algorithm.Log(f"\n>>>>>> Buy\t{str(self.symbol)}\tPrice\t{self.price}[{self.lastPricePaidRef}]\tMOM:{self.fmom}\trKAMA\t{self.price}\t \
                                    \nKAMA:{self.kama.Current.Value}\tFEMA:{self.fema}\tRS:{self.fbenchmark}\tSTD\t{self.std}")    
            
            self.lastPricePaidRef   =   self.price
            
                                     
        #  or not self.froc not self.fmom or 
        if isInvested and (self.fkama_sell or not self.fema or not self.fbenchmark \
                    or (self.dateTradedDelta<3 and self.price<self.lastPricePaidRef-float(str(self.std)))): # we avoid selling on the same/next day if move less than x std

            self.trade              =   True
            self.liquidate          =   True
            self.algorithm.Plot('Detail'+str(self.symbol),'Sell',self.price)
            
            self.algorithm.Log(f"\n<<<<<<< Sell\t{str(self.symbol)}\tMOM\t{self.fmom}\tPrice\t{self.price}[{self.lastPricePaidRef}]\trKAMA\t{self.price}\t \
                                    \nKAMA\t{self.kama.Current.Value}\tFEMA\t{self.fema}\tStock\t{self.rsStock}\tIdx\t{self.rsIdx}\tSTD\t{self.std}\tPriceDrop{str(self.lastPricePaidRef-float(str(self.std)))}")
    

    def OnSymbolDataUpdate(self, sender, updated):
        
        self.algorithm.Plot('Detail'+str(self.symbol),'Price', self.price)
        self.algorithm.Plot('Detail'+str(self.symbol),'Kama', self.kama.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'ROC', self.roc.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'MOM', self.mom.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'EMA13', self.ema13.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'EMA63', self.ema63.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'EMA150', self.ema150.Current.Value)
        self.algorithm.Plot('Detail'+str(self.symbol),'Std', self.std.Current.Value)
        
    def getRSL(self, sender, updated):

        # lookback days : algo weight 
        days = {40:0.5,80:0.25,160:0.25}

        rs = {}

        for symbol in [self.symbol,self.param.benchmark]:
 
            result =[]
            
            df=pd.DataFrame(self.algorithm.History(symbol, 300, Resolution.Daily)) 
            df=df.iloc[::-1]
            df=df.reset_index(level=0, drop=True)
            
            symbol = str(symbol)
            for x in days:
                result.append([symbol, x, df.iloc[0]['close'], df.iloc[x-1]['close'],days[x]])

            df = pd.DataFrame(result,columns=['Symbol','Days','Ref_Price','Close_Price','Weight'],dtype=float)
            df = df.assign(Rsl=(df['Ref_Price'])/df['Close_Price']*df['Weight'])
            
            rs[symbol] = (abs(df['Rsl']).sum()*1000)-1000
        
        self.rsStock    = rs[str(self.symbol)]
        self.rsIdx      = rs[str(self.param.benchmark)]

        self.algorithm.Plot('Detail'+str(self.symbol),'RS-idx', self.rsStock/self.rsIdx)
from clr import AddReference
AddReference("QuantConnect.Research")
#clr.AddReference('QuantConnect.Research')
from QuantConnect.Research import QuantBook

class RelativeStrengthLineCalc():

    def getRSL(self, ref_date, symbols):

        self.rsl_target_days     = [40,80,160] 
        self.rsl_target_weights  = [0.5,0.25,0.25]

        qb = QuantBook()
    
        date_end = datetime(ref_date)
        date_start = date_end - timedelta(days=300)
        
        for symbol in symbols:
            
            smbl = qb.AddEquity(symbol) # add equity data
            result =[]
            
            history = qb.History(smbl.Symbol, date_start, date_end, Resolution.Daily)
            df=pd.DataFrame(history)
            df=df.iloc[::-1]
            df=df.reset_index(level=0, drop=True)
            
            i=0
            for x in rsl_target_days:
                result.append([symbol, x, df.iloc[0]['close'], df.iloc[x-1]['close'],rsl_target_weights[i]])
                i=i+1
            df = pd.DataFrame(result,columns=['Symbol','Days','Ref_Price','Close_Price','Weight'],dtype=float)
            df = df.assign(Rsl=(df['Ref_Price'])/df['Close_Price']*df['Weight'])
            rsl=(abs(df['Rsl']).sum()*1000)-1000
            
        return  rsl
class RelativeStrengthLineCalc():

    def getRSL():

        rsl_target_days     = [40,80,160] 
        rsl_target_weights  = [0.5,0.25,0.25]
        
        return  1
import pandas as pd
import numpy as np
from scipy.optimize import minimize

class PortfolioOptimizer:
    
    '''
    Description:
        Implementation of a custom optimizer that calculates the weights for each asset to optimize a given objective function
    Details:
        Optimization can be:
            - Equal Weighting
            - Maximize Portfolio Return
            - Minimize Portfolio Standard Deviation
            - Mean-Variance (minimize Standard Deviation given a target return)
            - Maximize Portfolio Sharpe Ratio
            - Maximize Portfolio Sortino Ratio
            - Risk Parity Portfolio
        Constraints:
            - Weights must be between some given boundaries
            - Weights must sum to 1
    '''
    
    def __init__(self, 
                 minWeight = 0,
                 maxWeight = 1):
                     
        '''
        Description:
            Initialize the CustomPortfolioOptimizer
        Args:
            minWeight(float): The lower bound on portfolio weights
            maxWeight(float): The upper bound on portfolio weights
        '''
        
        self.minWeight = minWeight
        self.maxWeight = maxWeight

    def Optimize(self, objFunction, dailyReturnsDf, targetReturn = None):
        
        '''
        Description:
            Perform portfolio optimization given a series of returns
        Args:
            objFunction: The objective function to optimize (equalWeighting, maxReturn, minVariance, meanVariance, maxSharpe, maxSortino, riskParity)
            dailyReturnsDf: DataFrame of historical daily arithmetic returns
        Returns:
            Array of double with the portfolio weights (size: K x 1)
        '''
        
        # initial weights: equally weighted
        size = dailyReturnsDf.columns.size # K x 1
        self.initWeights = np.array(size * [1. / size])

        # get sample covariance matrix
        covariance = dailyReturnsDf.cov()
        # get the sample covariance matrix of only negative returns for sortino ratio
        negativeReturnsDf = dailyReturnsDf[dailyReturnsDf < 0]
        covarianceNegativeReturns = negativeReturnsDf.cov()
        
        if objFunction == 'equalWeighting':
            return self.initWeights
        
        bounds = tuple((self.minWeight, self.maxWeight) for x in range(size))
        constraints = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0}]
        
        if objFunction == 'meanVariance':
            # if no target return is provided, use the resulting from equal weighting
            if targetReturn is None:
                targetReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, self.initWeights)
            constraints.append( {'type': 'eq', 'fun': lambda weights:
                                self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights) - targetReturn} )

        opt = minimize(lambda weights: self.ObjectiveFunction(objFunction, dailyReturnsDf,
                                                                covariance, covarianceNegativeReturns,
                                                                weights),
                        x0 = self.initWeights,
                        bounds = bounds,
                        constraints = constraints,
                        method = 'SLSQP')

        return opt['x']

    def ObjectiveFunction(self, objFunction, dailyReturnsDf, covariance, covarianceNegativeReturns, weights):
        
        '''
        Description:
            Compute the objective function
        Args:
            objFunction: The objective function to optimize (equalWeighting, maxReturn, minVariance, meanVariance,
                                                                maxSharpe, maxSortino, riskParity)
            dailyReturnsDf: DataFrame of historical daily returns
            covariance: Sample covariance
            covarianceNegativeReturns: Sample covariance matrix of only negative returns
            weights: Portfolio weights
        '''
    
        if objFunction == 'maxReturn':
            f = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights)
            return -f # convert to negative to be minimized
        elif objFunction == 'minVariance':
            f = self.CalculateAnnualizedPortfolioStd(covariance, weights)
            return f
        elif objFunction == 'meanVariance':
            f = self.CalculateAnnualizedPortfolioStd(covariance, weights)
            return f
        elif objFunction == 'maxSharpe':
            f = self.CalculateAnnualizedPortfolioSharpeRatio(dailyReturnsDf, covariance, weights)
            return -f # convert to negative to be minimized
        elif objFunction == 'maxSortino':
            f = self.CalculateAnnualizedPortfolioSortinoRatio(dailyReturnsDf, covarianceNegativeReturns, weights)
            return -f # convert to negative to be minimized
        elif objFunction == 'riskParity':
            f = self.CalculateRiskParityFunction(covariance, weights)
            return f
        else:
            raise ValueError(f'PortfolioOptimizer.ObjectiveFunction: objFunction input has to be one of equalWeighting,'
             + ' maxReturn, minVariance, meanVariance, maxSharpe, maxSortino or riskParity')
        
    def CalculateAnnualizedPortfolioReturn(self, dailyReturnsDf, weights):
        
        annualizedPortfolioReturns = np.sum( ((1 + dailyReturnsDf.mean())**252 - 1) * weights )
        
        return annualizedPortfolioReturns
            
    def CalculateAnnualizedPortfolioStd(self, covariance, weights):
        
        annualizedPortfolioStd = np.sqrt( np.dot(weights.T, np.dot(covariance * 252, weights)) )
        
        if annualizedPortfolioStd == 0:
            raise ValueError(f'PortfolioOptimizer.CalculateAnnualizedPortfolioStd: annualizedPortfolioStd cannot be zero. Weights: {weights}')
            
        return annualizedPortfolioStd
    
    def CalculateAnnualizedPortfolioNegativeStd(self, covarianceNegativeReturns, weights):
    
        annualizedPortfolioNegativeStd = np.sqrt( np.dot(weights.T, np.dot(covarianceNegativeReturns * 252, weights)) )        
    
        if annualizedPortfolioNegativeStd == 0:
            raise ValueError(f'PortfolioOptimizer.CalculateAnnualizedPortfolioNegativeStd: annualizedPortfolioNegativeStd cannot be zero. Weights: {weights}')
        
        return annualizedPortfolioNegativeStd
        
    def CalculateAnnualizedPortfolioSharpeRatio(self, dailyReturnsDf, covariance, weights):
        
        annualizedPortfolioReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights)
        annualizedPortfolioStd = self.CalculateAnnualizedPortfolioStd(covariance, weights)
        annualizedPortfolioSharpeRatio = annualizedPortfolioReturn / annualizedPortfolioStd
            
        return annualizedPortfolioSharpeRatio
    
    def CalculateAnnualizedPortfolioSortinoRatio(self, dailyReturnsDf, covarianceNegativeReturns, weights):
        
        annualizedPortfolioReturn = self.CalculateAnnualizedPortfolioReturn(dailyReturnsDf, weights)
        annualizedPortfolioNegativeStd = self.CalculateAnnualizedPortfolioNegativeStd(covarianceNegativeReturns, weights)
        annualizedPortfolioSortinoRatio = annualizedPortfolioReturn / annualizedPortfolioNegativeStd
            
        return annualizedPortfolioSortinoRatio
    
    def CalculateRiskParityFunction(self, covariance, weights):
        
        ''' Spinu formulation for risk parity portfolio '''
        
        assetsRiskBudget = self.initWeights
        portfolioVolatility = self.CalculateAnnualizedPortfolioStd(covariance, weights)
        
        x = weights / portfolioVolatility
        riskParity = (np.dot(x.T, np.dot(covariance, x)) / 2) - np.dot(assetsRiskBudget.T, np.log(x))
            
        return riskParity