| 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