| Overall Statistics |
|
Total Trades 597 Average Win 0.07% Average Loss -0.10% Compounding Annual Return 22.002% Drawdown 17.500% Expectancy 0.064 Net Profit 23.004% Sharpe Ratio 0.93 Probabilistic Sharpe Ratio 43.417% Loss Rate 38% Win Rate 62% Profit-Loss Ratio 0.73 Alpha 0.071 Beta 0.56 Annual Standard Deviation 0.179 Annual Variance 0.032 Information Ratio -0.03 Tracking Error 0.152 Treynor Ratio 0.297 Total Fees $599.26 Estimated Strategy Capacity $32000000.00 Lowest Capacity Asset SNPS R735QTJ8XC9X |
from datetime import date, timedelta, datetime
from decimal import Decimal
import numpy as np
import pandas as pd
from scipy.stats import linregress
import decimal as d
class MomentumandStateofMarketFiltersAlgorithm(QCAlgorithm):
def Initialize(self):
# QC setup
self.SetStartDate(2020, 1, 1)
# Cash setup
self.SetCash(100000)
# Initialize SPY
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
# Add universe selection
self.AddUniverse(self.Coarse, self.Fine)
# Set universe resolution to daily
self.UniverseSettings.Resolution = Resolution.Daily
# Max stocks filtered from coarse
self.num_coarse = 250
# User input list of stocks
self.manual_list = ['AAPL', 'ABBV', 'ABT', 'ACN', 'ADBE', 'AIG', 'AMGN', 'AMT', 'AMZN', 'AVGO',
'AXP', 'BA', 'BAC', 'BIIB', 'BK', 'BKNG', 'BLK', 'BMY', 'BRK.B', 'C', 'CAT',
'CHTR', 'CL', 'CMCSA', 'COF', 'COP', 'COST', 'CRM', 'CSCO', 'CVS', 'CVX', 'DD',
'DHR', 'DIS', 'DOW', 'DUK', 'EMR', 'EXC', 'F', 'FB', 'FDX', 'GD', 'GE', 'GILD',
'GM', 'GOOG', 'GOOGL', 'GS', 'HD', 'HON', 'IBM', 'INTC', 'JNJ', 'JPM', 'KHC', 'KO',
'LIN', 'LLY', 'LMT', 'LOW', 'MA', 'MCD', 'MDLZ', 'MDT', 'MET', 'MMM', 'MO', 'MRK', 'MS',
'MSFT', 'NEE', 'NFLX', 'NKE', 'NVDA', 'ORCL', 'PEP', 'PFE', 'PG', 'PM', 'PYPL', 'QCOM', 'RTX',
'SBUX', 'SO', 'SPG', 'T', 'TGT', 'TMO', 'TMUS', 'TSLA', 'TXN', 'UNH', 'UNP', 'UPS', 'USB', 'V',
'VZ', 'WBA', 'WFC', 'WMT', 'XOM']
# Length of history call for position sizing
self.back_period = 21 * 3 + 1
# Volume period for position sizing
self.vol_period = 21
# Target volume for position sizing
self.target_vol = 0.2
# Leverage
self.lev = 1.5
# Delta
self.delta = 0.05
# Create an array out of volume period
self.x = np.asarray(range(self.vol_period))
# Lookback period for momentum percent
self.lookback = 20*6
# MOM dictionary
self.mom = {}
# Set automatic warmup
self.AutomaticIndicatorWarmup = True
# Coarse selection
def Coarse(self, coarse):
# Filter stocks by conditions:
filtered = [x for x in coarse
# If ticker has fundamental data
if x.HasFundamentalData
# And if ticker dollar volume of ticker greater than 1000000
and x.DollarVolume > 1000000
# And if ticker price greater than 5
and x.Price > 5
# Or ticker symbol in user input list
or x.Symbol.Value in self.manual_list]
# Sort by dollar volume
sortedStocks = sorted(filtered, key = lambda x: x.DollarVolume, reverse = True)
# Return number of stocks that user set
return [x.Symbol for x in sortedStocks][:self.num_coarse]
# Fine selection
def Fine(self, fine):
# Return symbol if:
return [x.Symbol for x in fine
# Stock is listed in NASDAQ
if x.CompanyReference.PrimaryExchangeID == "NAS"
# And Stock is listed in USA
and x.CompanyReference.CountryId == "USA"
# Or stock's symbol in user input list
or x.Symbol.Value in self.manual_list]
# On securities changed
def OnSecuritiesChanged(self, changes):
# Loop through stocks removed from algorithm
for stock in changes.RemovedSecurities:
# Remove from indicators
self.mom.pop(stock.Symbol, None)
# Liquidate
self.Liquidate(stock.Symbol)
# Loop through added stocks
for stock in changes.AddedSecurities:
# Initialize momentum percent indicator for stock and put in self.mom dictionary
self.mom[stock.Symbol] = self.MOMP(stock.Symbol, self.lookback)
# Daily bars are received here
def OnData(self, data):
# Initialize temporary dictionary to store momp values
temp_dict = {}
# Loop through mom dictionary
for i in self.mom:
# Get current MOMP value
current_MOMP = self.mom[i].Current.Value
# Add to temporary dictionary
temp_dict[i] = current_MOMP
# Sort dictionary
sorted_mom = sorted(temp_dict, key = temp_dict.get, reverse=True)
# Long list contains top 20 stocks with highest momentum percent (getting stocks that have been having an upward trend)
long_list = sorted_mom[:20]
# Short list contains top 20 stocks with lowest momentum percent (getting stocks that have been having a downward trend)
short_list = sorted_mom[-20:]
# Check whether current SPY has positive momentum percent
if self.mom[self.spy].Current.Value > 0:
# If length of long list is greater than 0
if len(long_list) > 0:
# Initialize rebalancing with greater weight put on long
self.rebalance(long_list, 1, 0.7)
# If length of short list is greater than 0
if len(short_list) > 0:
# Initialize rebalancing with less weight put on short
self.rebalance(short_list, -1, 0.3)
# If SPY has negative momentum percent
elif self.mom[self.spy].Current.Value < 0:
# If length of long list is greater than 0
if len(long_list) > 0:
# Initialize rebalancing with less weight put on long
self.rebalance(long_list, 1, 0.3)
# If length of short list is greater than 0
if len(short_list) > 0:
# Initialize rebalancing with greater weight put on short
self.rebalance(short_list, -1, 0.7)
# Rebalance formula
def rebalance(self, lists, sign, portfolio_weight):
self.w = 1 / len(lists)
try:
pos_sizing = self.pos_sizing(lists, sign)
except Exception as e:
msg = f'Exception: {e}'
self.Log(msg)
return
tot_port = self.Portfolio.TotalPortfolioValue
for symbol, info in pos_sizing.items():
new_weight = info[0]
yesterdayClose = info[1]
security = self.Securities[symbol]
quantity = security.Holdings.Quantity
price = security.Price
if price == 0: price = yesterdayClose
curr_weight = quantity * price / tot_port
shall_trade = abs(new_weight - curr_weight) > self.delta
if shall_trade:
delta_shares = (sign * (int(new_weight * (tot_port * portfolio_weight) / price))) - quantity
if delta_shares != 0:
self.MarketOnOpenOrder(symbol, delta_shares)
msg = f"{symbol} -- weight: {new_weight:.2f} (old weight was: {curr_weight:.2f}) -- last price: {price}"
# Position sizing formula
def pos_sizing(self, lists, sign):
allPrices = self.History(lists, self.back_period, Resolution.Daily).close.unstack(level=0)
pos = {}
for symbol in lists:
try:
prices = allPrices[symbol]
change = prices.pct_change().dropna()
last = np.float(prices[-1])
rsq = self.rsquared(self.x, prices[-self.vol_period:])
alpha = min(0.5, np.exp(-10. * (1. - rsq)))
vol = change.ewm(alpha=alpha).std()
ann_vol = np.float(vol.tail(1)) * np.sqrt(252)
weight = (self.target_vol / ann_vol).clip(0.0, self.lev) * self.w
pos[symbol] = (weight, last)
msg = f"{symbol}: {pos[symbol][0]}, rsqr: {rsq}, alpha: {alpha}, ann_vol = {ann_vol}"
except KeyError:
pass
return pos
# Rsquared formula
def rsquared(self, x, y):
_, _, r_value, _, _ = linregress(x, y)
return r_value**2