Overall Statistics
Total Trades
19
Average Win
6.12%
Average Loss
-3.47%
Compounding Annual Return
22.603%
Drawdown
29.600%
Expectancy
1.490
Net Profit
177.484%
Sharpe Ratio
0.973
Probabilistic Sharpe Ratio
47.545%
Loss Rate
10%
Win Rate
90%
Profit-Loss Ratio
1.77
Alpha
0.086
Beta
1.215
Annual Standard Deviation
0.239
Annual Variance
0.057
Information Ratio
0.633
Tracking Error
0.177
Treynor Ratio
0.191
Total Fees
$24.74
### PRODUCT INFORMATION --------------------------------------------------------------------------------
# Copyright InnoQuantivity.com, granted to the public domain.
# Use entirely at your own risk.
# This algorithm contains open source code from other sources and no claim is being made to such code.
# Do not remove this copyright notice.
### ----------------------------------------------------------------------------------------------------

from BuyAndHoldAlphaCreation import BuyAndHoldAlphaCreationModel
from CustomOptimizationPortfolioConstruction import CustomOptimizationPortfolioConstructionModel

class BuyAndHoldFrameworkAlgorithm(QCAlgorithmFramework):
    
    '''
    Trading Logic:
        This algorithm buys and holds the provided tickers from the start date until the end date
    Modules:
        Universe: Manual input of tickers
        Alpha: Constant creation of Up Insights every trading bar
        Portfolio: A choice between Equal Weighting, Maximize Portfolio Return, Minimize Portfolio Standard Deviation or Maximize Portfolio Sharpe Ratio
            - If some of the tickers did not exist at the start date, it will buy them when they first appeared in the market,
                in which case it will sell part of the existing securities in order to buy the new ones keeping an equally weighted portfolio
            - To rebalance the portfolio periodically to ensure optimal allocation, change the rebalancingParam below
        Execution: Immediate Execution with Market Orders
        Risk: Null
    '''
    
    def Initialize(self):
        
        ### user-defined inputs --------------------------------------------------------------

        self.SetStartDate(2015, 1, 1)   # set start date
        #self.SetEndDate(2019, 2, 1)     # set end date
        self.SetCash(100000)            # set strategy cash
        
        # add tickers to the list
        tickers = ['FB', 'AMZN', 'NFLX', 'GOOG']
        
        # objective function for portfolio optimizer
        # options are: equal (Equal Weighting), return (Maximize Portfolio Return), std (Minimize Portfolio Standard Deviation),
        # and sharpe (Maximize Portfolio Sharpe Ratio)
        objectiveFunction = 'std'
        
        # rebalancing period (to enable rebalancing enter an integer for number of calendar days, e.g. 1, 7, 30, 365)
        rebalancingParam = 365
            
        ### -----------------------------------------------------------------------------------
        
        # set the brokerage model for slippage and fees
        self.SetSecurityInitializer(self.CustomSecurityInitializer)
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        
        # set requested data resolution and disable fill forward data
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.FillForward = False
        
        # initialize plot for optimal allocation
        allocationPlot = Chart('Optimal Allocation')
            
        symbols = []
        # loop through the tickers list and create symbols for the universe
        for i in range(len(tickers)):
            symbols.append(Symbol.Create(tickers[i], SecurityType.Equity, Market.USA))
            allocationPlot.AddSeries(Series(tickers[i], SeriesType.Line, ''))
        self.AddChart(allocationPlot)
        
        # select modules
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        self.SetAlpha(BuyAndHoldAlphaCreationModel())
        self.SetPortfolioConstruction(CustomOptimizationPortfolioConstructionModel(objectiveFunction = objectiveFunction, rebalancingParam = rebalancingParam))
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
    def CustomSecurityInitializer(self, security):
        
        '''
        Description:
            Initialize the security with adjusted prices
        Args:
            security: Security which characteristics we want to change
        '''
        
        security.SetDataNormalizationMode(DataNormalizationMode.Adjusted)
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")

from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import AlphaModel, Insight, InsightType, InsightDirection

class BuyAndHoldAlphaCreationModel(AlphaModel):
    
    '''
    Description:
        This Alpha model creates InsightDirection.Up (to go Long) for a duration of 1 day, every day for all active securities in our Universe
    Details:
        The important thing to understand here is the concept of Insight:
            - A prediction about the future of the security, indicating an expected Up, Down or Flat move
            - This prediction has an expiration time/date, meaning we think the insight holds for some amount of time
            - In the case of a Buy and Hold strategy, we are just updating every day the Up prediction for another extra day
            - In other words, every day we are making the conscious decision of staying invested in the security one more day
    '''

    def __init__(self, resolution = Resolution.Daily):
        
        self.insightExpiry = Time.Multiply(Extensions.ToTimeSpan(resolution), 0.25) # insight duration
        self.insightDirection = InsightDirection.Up # insight direction
        self.securities = [] # list to store securities to consider
        
    def Update(self, algorithm, data):

        insights = [] # list to store the new insights to be created
        
        # loop through securities and generate insights
        for security in self.securities:
            # check if there's new data for the security or we're already invested
            # if there's no new data but we're invested, we keep updating the insight since we don't really need to place orders
            if data.ContainsKey(security.Symbol) or algorithm.Portfolio[security.Symbol].Invested:
                # append the insights list with the prediction for each symbol
                insights.append(Insight.Price(security.Symbol, self.insightExpiry, self.insightDirection))
            else:
                algorithm.Log('(Alpha) excluding this security due to missing data: ' + str(security.Symbol.Value))
            
        return insights
        
    def OnSecuritiesChanged(self, algorithm, changes):
        
        '''
        Description:
            Event fired each time the we add/remove securities from the data feed
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm
        '''
        
        # add new securities
        for added in changes.AddedSecurities:
            self.securities.append(added)

        # remove securities
        for removed in changes.RemovedSecurities:
            if removed in self.securities:
                self.securities.remove(removed)
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm.Framework")

from QuantConnect import Resolution, Extensions
from QuantConnect.Algorithm.Framework.Alphas import *
from QuantConnect.Algorithm.Framework.Portfolio import *
from itertools import groupby
from datetime import datetime, timedelta
from pytz import utc
UTCMIN = datetime.min.replace(tzinfo=utc)

import numpy as np
import pandas as pd
from scipy.optimize import minimize

class CustomOptimizationPortfolioConstructionModel(PortfolioConstructionModel):
    
    '''
    Description:
        Allocate optimal weights to each security in order to optimize the portfolio objective function provided
    Details:
        - The target percent holdings of each security is 1/N where N is the number of securities with active Up/Down insights
        - For InsightDirection.Up, long targets are returned
        - For InsightDirection.Down, short targets are returned
        - For InsightDirection.Flat, closing position targets are returned
    '''

    def __init__(self, objectiveFunction = 'std', rebalancingParam = False):
        
        '''
        Description:
            Initialize a new instance of CustomOptimizationPortfolioConstructionModel
        Args:
            objectiveFunction: The function to optimize. If set to 'equal', it will just perform equal weighting
            rebalancingParam: Integer indicating the number of days for rebalancing (default set to False, no rebalance)
                - Independent of this parameter, the portfolio will be rebalanced when a security is added/removed/changed direction
        '''
        
        if objectiveFunction != 'equal':
            # minWeight set to 0 to ensure long only weights
            self.optimizer = CustomPortfolioOptimizer(minWeight = 0, maxWeight = 1, objFunction = objectiveFunction) # initialize the optimizer
        
        self.optWeights = None
        self.objectiveFunction = objectiveFunction
        self.insightCollection = InsightCollection()
        self.removedSymbols = []
        self.nextExpiryTime = UTCMIN
        self.rebalancingTime = UTCMIN
        
        # if the rebalancing parameter is not False but a positive integer
        # convert rebalancingParam to timedelta and create rebalancingFunc
        if rebalancingParam > 0:
            self.rebalancing = True
            rebalancingParam = timedelta(days = rebalancingParam)
            self.rebalancingFunc = lambda dt: dt + rebalancingParam
        else:
            self.rebalancing = rebalancingParam

    def CreateTargets(self, algorithm, insights):

        '''
        Description:
            Create portfolio targets from the specified insights
        Args:
            algorithm: The algorithm instance
            insights: The insights to create portfolio targets from
        Returns:
            An enumerable of portfolio targets to be sent to the execution model
        '''

        targets = []
        
        # check if we have new insights coming from the alpha model or if some existing insights have expired
        # or if we have removed symbols from the universe
        if (len(insights) == 0 and algorithm.UtcTime <= self.nextExpiryTime and self.removedSymbols is None):
            return targets
        
        # here we get the new insights and add them to our insight collection
        for insight in insights:
            self.insightCollection.Add(insight)
            
        # create flatten target for each security that was removed from the universe
        if self.removedSymbols is not None:
            universeDeselectionTargets = [ PortfolioTarget(symbol, 0) for symbol in self.removedSymbols ]
            targets.extend(universeDeselectionTargets)
            self.removedSymbols = None

        # get insight that haven't expired of each symbol that is still in the universe
        activeInsights = self.insightCollection.GetActiveInsights(algorithm.UtcTime)

        # get the last generated active insight for each symbol
        lastActiveInsights = []
        for symbol, g in groupby(activeInsights, lambda x: x.Symbol):
            lastActiveInsights.append(sorted(g, key = lambda x: x.GeneratedTimeUtc)[-1])

        # check if we actually want to create new targets for the securities (check function ShouldCreateTargets for details)
        if self.ShouldCreateTargets(algorithm, self.optWeights, lastActiveInsights):
            # symbols with active insights
            lastActiveSymbols = [x.Symbol for x in lastActiveInsights]
            
            # get historical data for all symbols for the last 253 trading days (to get 252 returns)
            history = algorithm.History(lastActiveSymbols, 253, Resolution.Daily)
            
            # empty dictionary for calculations
            calculations = {}
            
            # iterate over all symbols and perform calculations
            for symbol in lastActiveSymbols:
                if (str(symbol) not in history.index or history.loc[str(symbol)].get('close') is None
                or history.loc[str(symbol)].get('close').isna().any()):
                    algorithm.Log('(Portfolio) no historical data for: ' + str(symbol.Value))
                    continue
                else:
                    # add symbol to calculations
                    calculations[symbol] = SymbolData(symbol)
                    try:
                        # get series of log-returns
                        calculations[symbol].CalculateLogReturnSeries(history)
                    except Exception:
                        algorithm.Log('(Portfolio) removing from calculations due to CalculateLogReturnSeries failing: ' + str(symbol.Value))
                        calculations.pop(symbol)
                        continue
            
            # determine target percent for the given insights (check function DetermineTargetPercent for details)
            self.optWeights = self.DetermineTargetPercent(calculations, lastActiveInsights)
            
            if not self.optWeights.isnull().values.any():
                algorithm.Log('(Portfolio) optimal weights: ' + str(self.optWeights))
                
                errorSymbols = {}
                for symbol in lastActiveSymbols:
                    if str(symbol) in self.optWeights:
                        # avoid very small numbers and make them 0
                        if self.optWeights[str(symbol)] <= 1e-10:
                            self.optWeights[str(symbol)] = 0
                        algorithm.Plot('Optimal Allocation', symbol.Value, float(self.optWeights[str(symbol)]))
                        target = PortfolioTarget.Percent(algorithm, symbol, self.optWeights[str(symbol)])
                        if not target is None:
                            targets.append(target)
                        else:
                            errorSymbols[symbol] = symbol

            # update rebalancing time
            if self.rebalancing:
                self.rebalancingTime = self.rebalancingFunc(algorithm.UtcTime)

        # get expired insights and create flatten targets for each symbol
        expiredInsights = self.insightCollection.RemoveExpiredInsights(algorithm.UtcTime)

        expiredTargets = []
        for symbol, f in groupby(expiredInsights, lambda x: x.Symbol):
            if not self.insightCollection.HasActiveInsights(symbol, algorithm.UtcTime) and not symbol in errorSymbols:
                expiredTargets.append(PortfolioTarget(symbol, 0))
                continue

        targets.extend(expiredTargets)
        
        # here we update the next expiry date in the insight collection
        self.nextExpiryTime = self.insightCollection.GetNextExpiryTime()
        if self.nextExpiryTime is None:
            self.nextExpiryTime = UTCMIN

        return targets
        
    def ShouldCreateTargets(self, algorithm, optWeights, lastActiveInsights):
        
        '''
        Description:
            Determine whether we should rebalance the portfolio to keep equal weighting when:
                - It is time to rebalance regardless
                - We want to include some new security in the portfolio
                - We want to modify the direction of some existing security
        Args:
            optWeights: Series containing the current optimal weight for each security
            lastActiveInsights: The last active insights to check
        '''
        
        # it is time to rebalance
        if self.rebalancing and algorithm.UtcTime >= self.rebalancingTime:
            return True
        
        for insight in lastActiveInsights:
            # if there is an insight for a new security that's not invested and it has no existing optimal weight, then rebalance
            if (not algorithm.Portfolio[insight.Symbol].Invested
            and insight.Direction != InsightDirection.Flat
            and str(insight.Symbol) not in optWeights):
                return True
            # if there is an insight to close a long position, then rebalance
            elif algorithm.Portfolio[insight.Symbol].IsLong and insight.Direction != InsightDirection.Up:
                return True
            # if there is an insight to close a short position, then rebalance
            elif algorithm.Portfolio[insight.Symbol].IsShort and insight.Direction != InsightDirection.Down:
                return True
            else:
                continue
            
        return False
        
    def DetermineTargetPercent(self, calculations, lastActiveInsights):
        
        '''
        Description:
            Determine the target percent for each symbol provided
        Args:
            calculations: Dictionary with calculations for symbols
            lastActiveInsights: Dictionary with calculations for symbols
        '''
        
        if self.objectiveFunction == 'equal':
            # give equal weighting to each security
            count = sum(x.Direction != InsightDirection.Flat for x in lastActiveInsights)
            percent = 0 if count == 0 else 1.0 / count
        
            result = {}
            for insight in lastActiveInsights:
                result[str(insight.Symbol)] = insight.Direction * percent
            
            weights = pd.Series(result)
            
            return weights
        
        else:        
            # create a dictionary keyed by the symbols in calculations with a pandas.Series as value to create a dataframe of log-returns
            logReturnsDict = { str(symbol): symbolData.logReturnSeries for symbol, symbolData in calculations.items() }
            logReturnsDf = pd.DataFrame(logReturnsDict)
            
            # portfolio optimizer finds the optimal weights for the given data
            weights = self.optimizer.Optimize(historicalLogReturns = logReturnsDf)
            weights = pd.Series(weights, index = logReturnsDf.columns)
            
            return weights
        
    def OnSecuritiesChanged(self, algorithm, changes):
        
        '''
        Description:
            Event fired each time the we add/remove securities from the data feed
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm
        '''

        # get removed symbol and invalidate them in the insight collection
        self.removedSymbols = [x.Symbol for x in changes.RemovedSecurities]
        self.insightCollection.Clear(self.removedSymbols)
        
class SymbolData:
    
    ''' Contain data specific to a symbol required by this model '''
    
    def __init__(self, symbol):
        
        self.Symbol = symbol
        self.logReturnSeries = None
    
    def CalculateLogReturnSeries(self, history):
        
        ''' Calculate the log-returns series for each security '''
        
        self.logReturnSeries = np.log(1 + history.loc[str(self.Symbol)]['close'].pct_change(periods = 1).dropna()) # 1-day log-returns
        
### class containing the CustomPortfolioOptimizer -----------------------------------------------------------------------------------------

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

    def Optimize(self, historicalLogReturns, covariance = None):
        
        '''
        Description:
            Perform portfolio optimization using a provided matrix of historical returns and covariance (optional)
        Args:
            historicalLogReturns: Matrix of historical log-returns where each column represents a security and each row log-returns for the given date/time (size: K x N)
            covariance: Multi-dimensional array of double with the portfolio covariance of returns (size: K x K)
        Returns:
            Array of double with the portfolio weights (size: K x 1)
        '''
        
        # if no covariance is provided, calculate it using the historicalLogReturns
        if covariance is None:
            covariance = historicalLogReturns.cov()

        size = historicalLogReturns.columns.size # K x 1
        x0 = np.array(size * [1. / size])
        
        # apply equality constraints
        constraints = ({'type': 'eq', 'fun': lambda weights: self.GetBudgetConstraint(weights)})

        opt = minimize(lambda weights: self.ObjectiveFunction(weights, historicalLogReturns, covariance),   # Objective function
                        x0,                                                                                 # Initial guess
                        bounds = self.GetBoundaryConditions(size),                                          # Bounds for variables
                        constraints = constraints,                                                          # Constraints definition
                        method = 'SLSQP')                                                                   # Optimization method: Sequential Least Squares Programming
                        
        return opt['x']

    def ObjectiveFunction(self, weights, historicalLogReturns, covariance):
        
        '''
        Description:
            Compute the objective function
        Args:
            weights: Portfolio weights
            historicalLogReturns: Matrix of historical log-returns
            covariance: Covariance matrix of historical log-returns
        '''
        
        # calculate the annual return of portfolio
        annualizedPortfolioReturns = np.sum(historicalLogReturns.mean() * 252 * weights)

        # calculate the annual standard deviation of portfolio
        annualizedPortfolioStd = np.sqrt( np.dot(weights.T, np.dot(covariance * 252, weights)) )
        
        if annualizedPortfolioStd == 0:
            raise ValueError(f'CustomPortfolioOptimizer.ObjectiveFunction: annualizedPortfolioStd cannot be zero. Weights: {weights}')
        
        # calculate annual sharpe ratio of portfolio
        annualizedPortfolioSharpeRatio = (annualizedPortfolioReturns / annualizedPortfolioStd)
            
        if self.objFunction == 'sharpe':
            return -annualizedPortfolioSharpeRatio # convert to negative to be minimized
        elif self.objFunction == 'return':
            return -annualizedPortfolioReturns # convert to negative to be minimized
        elif self.objFunction == 'std':
            return annualizedPortfolioStd
        else:
            raise ValueError(f'CustomPortfolioOptimizer.ObjectiveFunction: objFunction input has to be one of sharpe, return or std')

    def GetBoundaryConditions(self, size):
        
        ''' Create the boundary condition for the portfolio weights '''
        
        return tuple((self.minWeight, self.maxWeight) for x in range(size))

    def GetBudgetConstraint(self, weights):
        
        ''' Define a budget constraint: the sum of the weights equal to 1 '''
        
        return np.sum(weights) - 1