Overall Statistics Total Trades 712 Average Win 0.78% Average Loss -0.50% Compounding Annual Return 8.532% Drawdown 12.600% Expectancy 0.573 Net Profit 169.169% Sharpe Ratio 0.869 Probabilistic Sharpe Ratio 31.031% Loss Rate 38% Win Rate 62% Profit-Loss Ratio 1.56 Alpha 0.071 Beta -0.005 Annual Standard Deviation 0.081 Annual Variance 0.007 Information Ratio -0.077 Tracking Error 0.193 Treynor Ratio -12.988 Total Fees \$2182.34
from clr import AddReference

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 LongOnlyMomentumAlphaCreationModel(AlphaModel):

'''
Description:
- Every N days, this Alpha model calculates the momentum score of each risky asset in the Universe
The momentum score is a weighted average of cumulative returns: (1-month * 12) + (3-month * 4) + (6-month * 2) + (12-month * 1)
- This Alpha model then creates InsightDirection.Up (to go Long) for a duration of a trading bar, every day for the selected top momentum securities
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 constant long-only 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, riskyTickers, crashProtectionTicker, canaryTickers, topMomentum = 5, daysToRecalculate = 21):

self.riskyTickers = riskyTickers # risky tickers to use for momentum asset allocation
self.crashProtectionTicker = crashProtectionTicker # this ticker will also be part of momentum asset allocation,
# but it's a special asset that will get the crash protection allocation when needed
self.canaryTickers = canaryTickers # canary tickers to avoid in momentum calculations, but we need to subscribe to them
self.topMomentum = topMomentum # number of top momentum securities to keep

self.insightExpiry = Time.Multiply(Extensions.ToTimeSpan(Resolution.Daily), 0.25) # insight duration
self.insightDirection = InsightDirection.Up # insight direction

self.securities = [] # list to store securities to consider
self.topMomentumSecurities = {} # empty dictionary to store top momentum securities

self.daysToRecalculate = daysToRecalculate # number of trading days to recalculate momentum score
self.countDays = 0 # initialize count of days for recalculation

def Update(self, algorithm, data):

# add one day to the count
self.countDays += 1

### calculate momentum scores (every N number of trading days) --------------------------------------------------------

if self.countDays > self.daysToRecalculate:
algorithm.Log('(Alpha) time to calculate the momentum securities')

### get symbols ---------------------------------------------------------------------------------------------------

# risky symbols
riskySymbols = [x.Symbol for x in self.securities if x.Symbol.Value in self.riskyTickers]
algorithm.Log('(Alpha) number of risky assets: ' + str(len(riskySymbols)))
# crash protection symbol
crashProtectionSymbol = [x.Symbol for x in self.securities if x.Symbol.Value in self.crashProtectionTicker]
algorithm.Log('(Alpha) number of crash protection assets: ' + str(len(crashProtectionSymbol)))

# combine the two lists to get relevant symbols for momentum calculations
relevantSymbols = riskySymbols + crashProtectionSymbol
algorithm.Log('(Alpha) number of relevant assets for trading: ' + str(len(relevantSymbols)))

# canary symbols
canarySymbols = [x.Symbol for x in self.securities if x.Symbol.Value in self.canaryTickers]
algorithm.Log('(Alpha) number of canary assets: ' + str(len(canarySymbols)))

# combine all lists to get all symbols for calculations
allSymbols = relevantSymbols + canarySymbols
algorithm.Log('(Alpha) total number of assets considered for calculations: ' + str(len(allSymbols)))

### make momentum calculations ---------------------------------------------------------------------------------------

# create empty dictionary to store calculations
calculations = {}

if len(allSymbols) > 0:
# get historical prices for symbols
history = algorithm.History(allSymbols, 253, Resolution.Daily)

for symbol in allSymbols:
# if symbol has no historical data continue the loop
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('(Alpha) no historical data for: ' + str(symbol.Value))
continue
else:
calculations[symbol] = SymbolData(symbol)
try:
# get momentum score
calculations[symbol].CalculateMomentumScore(history)
except Exception:
algorithm.Log('(Alpha) removing from Alpha calculations due to CalculateMomentumScore failing')
calculations.pop(symbol)
continue

calculatedSymbols = [x for x in calculations]
algorithm.Log('(Alpha) checking the number of calculated symbols: ' + str(len(calculatedSymbols)))

### get the top momentum securities among risky assets (including crash protection asset) ---------------------------

# perform Absolute Momentum: get the securities with positive momentum
positiveMomentumSecurities = list(filter(lambda x: x.momentumScore > 0 and x.Symbol in relevantSymbols, calculations.values()))

# perform Relative Momentum: sort descending by momentum score and select the top n
self.topMomentumSecurities = sorted(positiveMomentumSecurities, key = lambda x: x.momentumScore, reverse = True)[:self.topMomentum]

### get percentage dedicated to risky assets ------------------------------------------------------------------------

pctAggressive = self.CalculatePctAggressive(calculations, canarySymbols)
algorithm.Log('(Alpha) pctAggressive: ' + str(pctAggressive))

### if percentage aggressive is less than 1 ------------------------------------------------------------
### we need to add the crashProtectionSecurity if it has positive absolute momentum

crashProtectionSecurity = [x for x in self.securities if x.Symbol.Value in self.crashProtectionTicker]
positiveMomentumSymbols = [x.Symbol for x in positiveMomentumSecurities]
topMomentumSymbols = [x.Symbol for x in self.topMomentumSecurities]

# if percentage aggressive is 0,
# we only generate insights for the crash protection asset if it has positive absolute momentum;
# if not, we don't send any insights
if pctAggressive == 0:
if crashProtectionSymbol in positiveMomentumSymbols:
algorithm.Log('(Alpha) pctAggressive is 0 but crashProtectionSymbol has positive momentum so we add it')
self.topMomentumSecurities = crashProtectionSecurity
else:
self.topMomentumSecurities = []

# if percentage aggressive is positive but less than 1,
# we need to make sure we are sending insights for the crash protection asset as well if it has positive absolute momentum
elif pctAggressive < 1:
if crashProtectionSymbol in positiveMomentumSymbols and crashProtectionSymbol not in topMomentumSymbols:
algorithm.Log('(Alpha) adding the crash protection asset to topMomentumSecurities')
self.topMomentumSecurities.append(crashProtectionSecurity)

# get top momentum tickers for logs
topMomentumTickers = [x.Symbol.Value for x in self.topMomentumSecurities]
algorithm.Log('(Alpha) top securities: ' + str(topMomentumTickers))

# restart count of days
self.countDays = 0

### end of rebalance calculations ---------------------------------------------------------------------------------------

### generate insights ---------------------------------------------------------------------------------------------------

insights = [] # list to store the new insights to be created

# loop through active securities and generate insights
for security in self.topMomentumSecurities:
# 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('(Portfolio) excluding 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
'''

# remove securities
for removed in changes.RemovedSecurities:
if removed in self.securities:
self.securities.remove(removed)

def CalculatePctAggressive(self, calculations, canarySymbols):

'''
Description:
Calculate the percentage dedicated to risky assets
Args:
calculations: Dictionary with calculations for symbols
canarySymbols: Symbols for the canary assets
Returns:
Float with the percentage dedicated to risky assets
'''

# get a list with the canary assets that have positive absolute momentum
canaryPosMomList = list(filter(lambda x: x.momentumScore > 0 and x.Symbol in canarySymbols, calculations.values()))

# get the average positive (basically the options are 0, 0.5 or 1)
# this will be the allocation for risky assets
pctAggressive = len(canaryPosMomList) / len(canarySymbols)

return pctAggressive

class SymbolData:

''' Contain data specific to a symbol required by this model '''

def __init__(self, symbol):

self.Symbol = symbol

def CalculateMomentumScore(self, history):

''' Calculate the weighted average momentum value for each security '''

returnSeries = history.loc[str(self.Symbol)]['close'].pct_change(periods = 1).dropna() # 1-day returns for last year

cumRet1 = (returnSeries.tail(21).add(1).prod()) - 1 # 1-month momentum
cumRet3 = (returnSeries.tail(63).add(1).prod()) - 1 # 3-month momentum
cumRet6 = (returnSeries.tail(126).add(1).prod()) - 1 # 6-month momentum
cumRet12 = (returnSeries.tail(252).add(1).prod()) - 1 # 12-month momentum

self.momentumScore = (cumRet1 * 12 + cumRet3 * 4 + cumRet6 * 2 + cumRet12) # weighted average momentum
from clr import AddReference

from System import *
from QuantConnect import *
from QuantConnect.Orders import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Execution import *
from QuantConnect.Algorithm.Framework.Portfolio import *

import numpy as np

class ImmediateExecutionWithLogsModel(ExecutionModel):

'''
Description:
Custom implementation of IExecutionModel that immediately submits market orders to achieve the desired portfolio targets
Details:
This custom implementation includes logs with information about number of shares traded, prices, profit and profit percent
for both long and short positions.
'''

def __init__(self):

''' Initializes a new instance of the ImmediateExecutionModel class '''

self.targetsCollection = PortfolioTargetCollection()

def Execute(self, algorithm, targets):

'''
Description:
Immediately submits orders for the specified portfolio targets
Args:
algorithm: The algorithm instance
targets: The portfolio targets to be ordered
'''

if self.targetsCollection.Count > 0:
for target in self.targetsCollection.OrderByMarginImpact(algorithm):
# calculate remaining quantity to be ordered (this could be positive or negative)
unorderedQuantity = OrderSizing.GetUnorderedQuantity(algorithm, target)
# calculate the lot size for the security
lotSize = algorithm.ActiveSecurities[target.Symbol].SymbolProperties.LotSize

# this is the size of the order in terms of absolute number of shares
orderSize = abs(unorderedQuantity)

remainder = orderSize % lotSize
missingForLotSize = lotSize - remainder
# if the amount we are missing for +1 lot size is 1M part of a lot size
# we suppose its due to floating point error and round up
# Note: this is required to avoid a diff with C# equivalent
if missingForLotSize < (lotSize / 1000000):
remainder -= lotSize

# round down to even lot size
orderSize -= remainder
quantity = np.sign(unorderedQuantity) * orderSize

# check if quantity is greater than 1 share (in absolute value to account for shorts)
if quantity != 0:
# get the current holdings quantity, average price and cost
beforeHoldingsQuantity = algorithm.ActiveSecurities[target.Symbol].Holdings.Quantity
beforeHoldingsAvgPrice = algorithm.ActiveSecurities[target.Symbol].Holdings.AveragePrice
beforeHoldingsCost = algorithm.ActiveSecurities[target.Symbol].Holdings.HoldingsCost

# place market order
algorithm.MarketOrder(target.Symbol, quantity)

# get the new holdings quantity, average price and cost
newHoldingsQuantity = beforeHoldingsQuantity + quantity
newHoldingsAvgPrice = algorithm.ActiveSecurities[target.Symbol].Holdings.AveragePrice
newHoldingsCost = algorithm.ActiveSecurities[target.Symbol].Holdings.HoldingsCost

# this is just for market on open orders because the avg price and cost won't update until order gets filled
# so to avoid getting previous values we just make them zero
if newHoldingsAvgPrice == beforeHoldingsAvgPrice and newHoldingsCost == beforeHoldingsCost:
newHoldingsAvgPrice = 0
newHoldingsCost = 0

# calculate the profit percent and dollar profit when closing positions
lastPrice = algorithm.ActiveSecurities[target.Symbol].Price
if beforeHoldingsAvgPrice != 0 and lastPrice != 0:
# profit/loss percent for the trade
tradeProfitPercent = (((lastPrice / beforeHoldingsAvgPrice) - 1) * np.sign(beforeHoldingsQuantity)) * 100
# dollar profit/loss for the trade (when partially or entirely closing a position)
tradeDollarProfit = (lastPrice - beforeHoldingsAvgPrice) * (abs(quantity) * np.sign(beforeHoldingsQuantity))
# dollar profit/loss for the trade (when reversing a position from long/short to short/long)
tradeDollarProfitReverse = (lastPrice - beforeHoldingsAvgPrice) * beforeHoldingsQuantity
else:

### if we are not invested already the options are: ----------------------------------------------------------
# new holdings > 0 => going long
# new holdings < 0 => going short
if beforeHoldingsQuantity == 0:
if newHoldingsQuantity > 0:
algorithm.Log(str(target.Symbol.Value) + ': going long!'
+ ' current total holdings: ' + str(round(quantity, 0))
+ '; current average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))
else:
algorithm.Log(str(target.Symbol.Value) + ': going short!'
+ ' current total holdings: ' + str(round(quantity, 0))
+ '; average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))
### -----------------------------------------------------------------------------------------------------------

### if we are already long the security the options are: ------------------------------------------------------
# new quantity > 0 => adding to long position
# new quantity < 0 and new holdings < before holdings => partially selling long position
# new quantity < 0 and new holdings = 0 => closing entire long position
# new quantity < 0 and new holdings < 0 => closing entire long position and going short
elif beforeHoldingsQuantity > 0:
if quantity > 0:
algorithm.Log(str(target.Symbol.Value) + ': adding to current long position!'
+ ' additional shares: ' + str(round(quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; current average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))

elif newHoldingsQuantity > 0 and newHoldingsQuantity < beforeHoldingsQuantity:
algorithm.Log(str(target.Symbol.Value) + ': selling part of current long position!'
+ ' selling shares: ' + str(round(-quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; buying average price was: ' + str(round(beforeHoldingsAvgPrice, 4))
+ '; approx. selling average price is: ' + str(round(lastPrice, 4))
+ '; profit percent: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit: ' + str(round(tradeDollarProfit, 2)))

elif newHoldingsQuantity == 0:
algorithm.Log(str(target.Symbol.Value) + ': closing down entire current long position!'
+ ' selling shares: ' + str(round(-quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; buying average price was: ' + str(round(beforeHoldingsAvgPrice, 4))
+ '; approx. selling average price is: ' + str(round(lastPrice, 4))
+ '; profit percent: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit: ' + str(round(tradeDollarProfit, 2)))

elif newHoldingsQuantity < 0:
algorithm.Log(str(target.Symbol.Value) + ': closing down entire current long position and going short!'
+ ' selling shares to close long: ' + str(round(beforeHoldingsQuantity, 0))
+ '; profit percent on long position: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit on long position: ' + str(round(tradeDollarProfitReverse, 2))
+ '; selling shares to go short: ' + str(round(-newHoldingsQuantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; current average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))
### --------------------------------------------------------------------------------------------------------------

### if we are already short the security the options are: --------------------------------------------------------
# new quantity < 0 => adding to short position
# new quantity > 0 and new holdings > before holdings => partially buying back short position
# new quantity > 0 and new holdings = 0 => closing entire short position
# new quantity > 0 and new holdings > 0 => closing entire short position and going long
elif beforeHoldingsQuantity < 0:
if quantity < 0:
algorithm.Log(str(target.Symbol.Value) + ': adding to current short position!'
+ ' additional shares: ' + str(round(quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; current average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))

elif newHoldingsQuantity < 0 and newHoldingsQuantity > beforeHoldingsQuantity:
algorithm.Log(str(target.Symbol.Value) + ': buying back part of current short position!'
+ ' buying back shares: ' + str(round(quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; shorting average price was: ' + str(round(beforeHoldingsAvgPrice, 4))
+ '; approx. buying back average price is: ' + str(round(lastPrice, 4))
+ '; profit percent: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit: ' + str(round(tradeDollarProfit, 2)))

elif newHoldingsQuantity == 0:
algorithm.Log(str(target.Symbol.Value) + ': closing down entire current short position!'
+ ' buying back shares: ' + str(round(quantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; shorting average price was: ' + str(round(beforeHoldingsAvgPrice, 4))
+ '; approx. buying back average price is: ' + str(round(lastPrice, 4))
+ '; profit percent: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit: ' + str(round(tradeDollarProfit, 2)))

elif newHoldingsQuantity > 0:
algorithm.Log(str(target.Symbol.Value) + ': closing down entire current short position and going long!'
+ ' buying back shares to close short: ' + str(round(-beforeHoldingsQuantity, 0))
+ '; profit percent on short position: ' + str(round(tradeProfitPercent, 4))
+ '; dollar profit on short position: ' + str(round(tradeDollarProfitReverse, 2))
+ '; buying shares to go long: ' + str(round(newHoldingsQuantity, 0))
+ '; current total holdings: ' + str(round(newHoldingsQuantity, 0))
+ '; current average price: ' + str(round(newHoldingsAvgPrice, 4))
+ '; current total holdings cost: ' + str(round(newHoldingsCost, 2)))
### ---------------------------------------------------------------------------------------------------------------

self.targetsCollection.ClearFulfilled(algorithm)
import numpy as np
from scipy.optimize import minimize

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
from clr import AddReference

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)

from optimizer import CustomPortfolioOptimizer
import numpy as np
import pandas as pd

class OptimizationPortfolioConstructionModel(PortfolioConstructionModel):

'''
Description:
Allocate optimal weights to each security in order to optimize the portfolio objective function provided
Details:
- Two Canary Assets determine how much to invest in Risky Assets:
If both assets have positive absolute momentum => 100%
If only one has positive absolute momentum => 50%
If none have positive absolute momentum => 0%
* The remaining % from the above calculation will go to the Crash Protection Ticker, only if it has positive absolute momentum
- To calculate the weights for risky assets, we perform portfolio optimization with the following particularity:
We construct the correlation matrix using a 1-3-6-12 momentum weighting:
( last month correlation * 12 + last 3-month correlation * 4 + last 6-month correlation * 2 + last 12-month correlation ) / 19
'''

def __init__(self, crashProtectionTicker, canaryTickers, topMomentum, objectiveFunction = 'std', daysToRebalance = 21):

self.crashProtectionTicker = crashProtectionTicker # this ticker will also be part of momentum asset allocation,
# but it's a special asset that will get the crash protection allocation when needed
self.canaryTickers = canaryTickers # canary tickers to avoid in momentum calculations, but we need to subscribe to them
self.topMomentum = topMomentum # number of top momentum securities to keep

self.optimizer = CustomPortfolioOptimizer(minWeight = 0.001, maxWeight = 1, objFunction = objectiveFunction) # initialize the optimizer
self.insightCollection = InsightCollection()
self.nextExpiryTime = UTCMIN

self.daysToRecalculate = daysToRebalance # number of trading days to rebalance portfolio
self.countDays = 0 # initialize count of days for rebalancing

def CreateTargets(self, algorithm, insights):

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

# add one day to the count
self.countDays += 1

targets = []

# check if we have new insights coming from the alpha model or if some existing insights have expired
if (len(insights) == 0 and algorithm.UtcTime <= self.nextExpiryTime):
return targets

# here we get the new insights and add them to our insight collection
for insight in insights:

# 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])

# symbols with active insights
lastActiveSymbols = [x.Symbol for x in lastActiveInsights]

### calculate targets -------------------------------------------------------------------------------------

if self.ShouldCreateTargets(lastActiveSymbols):
algorithm.Log('(Portfolio) time to calculate the targets')
algorithm.Log('(Portfolio) number of active insights: ' + str(len(lastActiveSymbols)))

### get symbols ---------------------------------------------------------------------------------------

# top momentum symbols
topMomentumSymbols = [x.Symbol for x in lastActiveInsights]

# crash protection symbol
crashProtectionSymbol = [x.Symbol for x in algorithm.ActiveSecurities.Values if x.Symbol.Value in self.crashProtectionTicker]

# if active symbols are more than topMomentum, we need to remove the crashProtectionSymbol from topMomentumSymbols
if len(lastActiveSymbols) > self.topMomentum:
if crashProtectionSymbol in topMomentumSymbols:
topMomentumSymbols.remove(crashProtectionSymbol)
else:
algorithm.Log('(Portfolio) lastActiveSymbols is bigger than topMomentum, but crashProtectionSymbol is not in topMomentumSymbols!')

# canary symbols
canarySymbols = [x.Symbol for x in algorithm.ActiveSecurities.Values if x.Symbol.Value in self.canaryTickers]

# combine the two lists to get all symbols for calculations
allSymbols = topMomentumSymbols + canarySymbols

### ----------------------------------------------------------------------------------------------------

# get historical data for all symbols
history = algorithm.History(allSymbols, 253, Resolution.Daily)

# empty dictionary for calculations
calculations = {}

# iterate over all symbols and perform calculations
for symbol in allSymbols:
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))
if symbol in lastActiveSymbols:
lastActiveSymbols.remove(symbol)
continue
else:
calculations[symbol] = SymbolData(symbol)
try:
# get momentum score
calculations[symbol].CalculateMomentumScore(history)
except Exception:
algorithm.Log('(Portfolio) removing from Portfolio calculations due to CalculateMomentumScore failing')
calculations.pop(symbol)
if symbol in lastActiveSymbols:
lastActiveSymbols.remove(symbol)
continue

# calculate the percentage of aggressive allocation for risky assets
pctAggressive = self.CalculatePctAggressive(calculations, canarySymbols)
algorithm.Log('(Portfolio) pctAggressive: ' + str(pctAggressive))

# calculate optimal weights
optWeights = self.DetermineTargetPercent(calculations, topMomentumSymbols, crashProtectionSymbol)

if not optWeights.isnull().values.any():
algorithm.Log('(Portfolio) optimal weights: ' + str(optWeights))

# apply percentage aggressive to the weights to get final weights
finalWeights = optWeights * pctAggressive
algorithm.Log('(Portfolio) final weights: ' + str(finalWeights))

# iterate over active symbols and create targets
for symbol in lastActiveSymbols:
# we allocate the rest to the crash protection asset
if symbol in crashProtectionSymbol and pctAggressive < 1:
finalWeights[str(symbol)] = finalWeights[str(symbol)] + (1 - pctAggressive)

algorithm.Log('(Portfolio) adding ' + str(1 - pctAggressive) + ' extra weight for ' + str(symbol.Value)
+ '; final weight: ' + str(finalWeights[str(symbol)]))

weight = finalWeights[str(symbol)]
target = PortfolioTarget.Percent(algorithm, symbol, weight)

if not target is None:
targets.append(target)

### end of calculations --------------------------------------------------------------------------------

# 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):
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, lastActiveSymbols):

'''
Description:
Determine whether we should create new portfolio targets when:
- It's time to rebalance and there are active insights
Args:
lastActiveSymbols: Symbols for the last active securities
'''

if self.countDays > self.daysToRecalculate and len(lastActiveSymbols) > 0:
# restart count of days
self.countDays = 0
return True
else:
return False

def CalculatePctAggressive(self, calculations, canarySymbols):

'''
Description:
Calculate the percentage dedicated to risky assets
Args:
calculations: Dictionary with calculations for symbols
canarySymbols: Symbols for the canary assets
Returns:
Float with the percentage dedicated to risky assets
'''

# get a list with the canary assets that have positive absolute momentum
canaryPosMomList = list(filter(lambda x: x.momentumScore > 0 and x.Symbol in canarySymbols, calculations.values()))

# get the average positive (basically the options are 0, 0.5 or 1)
# this will be the allocation for risky assets
pctAggressive = len(canaryPosMomList) / len(canarySymbols)

return pctAggressive

def DetermineTargetPercent(self, calculations, topMomentumSymbols, crashProtectionSymbol):

'''
Description:
Determine the target percent for each symbol provided
Args:
calculations: Dictionary with calculations for symbols
topMomentumSymbols: Symbols for the top momentum assets
crashProtectionSymbol: Symbol for the crash protection asset
Returns:
Pandas series with the optimal weights for each symbol
'''

# 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): np.log(1 + symbolData.returnSeries) for symbol, symbolData in calculations.items() if symbol in topMomentumSymbols }
logReturnsDf = pd.DataFrame(logReturnsDict)

# create correlation matrix with 1-3-6-12 momentum weighting
corrMatrix = ( logReturnsDf.tail(21).corr() * 12 + logReturnsDf.tail(63).corr() * 4 + logReturnsDf.tail(126).corr() * 2 + logReturnsDf.tail(252).corr() ) / 19

# create standard deviation matrix using the 1-month standard deviation of returns
stdMatrix = pd.DataFrame(logReturnsDf.tail(21).std()) # column vector (one row per symbol and one single column with the standard deviation)
# get its transpose
stdMatrixTranspose = stdMatrix.T # row vector (one single row with the standard deviation and one column per symbol)

# compute the dot product between stdMatrix and its transpose to get the volatility matrix
volMatrix = stdMatrix.dot(stdMatrixTranspose) # square NxN matrix with the variances of each symbol on the diagonal and the product of stds on the off diagonal

# calculate the covariance matrix by doing element-wise multiplication of correlation matrix and volatility matrix
covMatrix = corrMatrix.multiply(volMatrix)

# portfolio optimizer finds the optimal weights for the given data
weights = self.optimizer.Optimize(historicalLogReturns = logReturnsDf, covariance = covMatrix)
weights = pd.Series(weights, index = logReturnsDf.columns)

# add crashProtectionSymbol to the finalWeights series with 0 if not already there
if str(crashProtectionSymbol) not in weights:
weights[str(crashProtectionSymbol)] = 0

return weights

class SymbolData:

''' Contain data specific to a symbol required by this model '''

def __init__(self, symbol):

self.Symbol = symbol
self.returnSeries = None
self.momentumScore = None

def CalculateMomentumScore(self, history):

''' Calculate the weighted average momentum score for each security '''

self.returnSeries = history.loc[str(self.Symbol)]['close'].pct_change(periods = 1).dropna() # 1-day returns for last year

cumRet1 = (self.returnSeries.tail(21).add(1).prod()) - 1 # 1-month momentum
cumRet3 = (self.returnSeries.tail(63).add(1).prod()) - 1 # 3-month momentum
cumRet6 = (self.returnSeries.tail(126).add(1).prod()) - 1 # 6-month momentum
cumRet12 = (self.returnSeries.tail(252).add(1).prod()) - 1 # 12-month momentum

self.momentumScore = (cumRet1 * 12 + cumRet3 * 4 + cumRet6 * 2 + cumRet12) # weighted average momentum
### 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 LongOnlyMomentumAlphaCreation import LongOnlyMomentumAlphaCreationModel
from OptimizationPortfolioConstruction import OptimizationPortfolioConstructionModel
from ImmediateExecutionWithLogs import ImmediateExecutionWithLogsModel

class KDAAssetAllocationFrameworkAlgorithm(QCAlgorithmFramework):

'''
- This algorithm is a long-only dual momentum asset class strategy as described in the link above
Modules:
Universe: Manual input of tickers
Alpha:
- Calculates momentum score for each security at the end of every period (see Alpha module for details)
- Constant creation of Up Insights every trading bar during the period for the top securities
Portfolio: Minimum Variance (optimal weights to minimize portfolio variance)
- See Portfolio module for details
Execution: Immediate Execution with Market Orders
Risk: Null
'''

def Initialize(self):

### user-defined inputs --------------------------------------------------------------

# set timeframe for backtest and starting cash
self.SetStartDate(2008, 1, 1)   # set start date
#self.SetEndDate(2016, 1, 1)    # set end date
self.SetCash(100000)            # set strategy cash

# add tickers for risky assets for momentum asset allocation
riskyTickers = ['SPY',  # US equities
'VGK',  # European equities
'EWJ',  # Japanese equities
'EEM',  # Emerging market equities
'VNQ',  # US REITs
'RWX',  # International REITs
'TLT',  # US 30-year Treasuries
'DBC',  # Commodities
'GLD',  # Gold
]

# this ticker will also be part of risky assets for momentum asset allocation,
# but it's a special asset that will get the crash protection allocation when needed
crashProtectionTicker = ['IEF'] # US 10-year Treasuries

# add tickers for canary assets
canaryTickers = ["VWO", # Vanguard FTSE Emerging Markets ETF
"BND"   # Vanguard Total Bond Market ETF
]

# number of top momentum securities to keep
topMomentum = 5

# number of trading days between recalculations and rebalancing (note it's trading days and not calendar days)
daysToRecalculate = 21

# objective function for portfolio optimizer
# options are: return (maximize portfolio return), std (minimize portfolio Standard Deviation) and sharpe (maximize portfolio sharpe ratio)
objectiveFunction = 'std'

### -----------------------------------------------------------------------------------

# 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

# combine all lists of tickers
allTickers = riskyTickers + crashProtectionTicker + canaryTickers

symbols = []
# loop through the tickers list and create symbols for the universe
for i in range(len(allTickers)):
symbols.append(Symbol.Create(allTickers[i], SecurityType.Equity, Market.USA))

# select modules
self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
self.SetAlpha(LongOnlyMomentumAlphaCreationModel(riskyTickers = riskyTickers,
crashProtectionTicker = crashProtectionTicker,
canaryTickers = canaryTickers,
topMomentum = topMomentum,
daysToRecalculate = daysToRecalculate))
self.SetPortfolioConstruction(OptimizationPortfolioConstructionModel(crashProtectionTicker = crashProtectionTicker,
canaryTickers = canaryTickers,
topMomentum = topMomentum,
objectiveFunction = objectiveFunction,
daysToRebalance = daysToRecalculate))
self.SetExecution(ImmediateExecutionWithLogsModel())
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)