| Overall Statistics |
|
Total Trades 8298 Average Win 0.03% Average Loss -0.03% Compounding Annual Return -0.954% Drawdown 6.500% Expectancy -0.031 Net Profit -4.299% Sharpe Ratio -0.25 Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.07 Alpha -0.06 Beta 2.903 Annual Standard Deviation 0.032 Annual Variance 0.001 Information Ratio -0.805 Tracking Error 0.032 Treynor Ratio -0.003 Total Fees $8299.51 |
# https://quantpedia.com/Screener/Details/155
from QuantConnect.Data.UniverseSelection import *
import math
import numpy as np
import pandas as pd
import scipy as sp
from collections import deque
class MomentumReversalCombinedWithVolatility(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2014, 1, 1) # Set Start Date
self.SetEndDate(2018, 8, 1) # Set Start Date
self.SetCash(100000) # Set Strategy Cash
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.dataDict = {}
# 1/6 of the portfolio is rebalanced every month
self.portfolios = deque(maxlen=6)
self.AddEquity("SPY", Resolution.Daily)
self.Schedule.On(self.DateRules.MonthStart("SPY"),self.TimeRules.AfterMarketOpen("SPY"), self.Rebalance)
# the lookback period for volatility and return is six months
self.lookback = 20*6
self.filteredFine = None
self.monthly_rebalance = False
def CoarseSelectionFunction(self, coarse):
# update the price of stocks in universe everyday
for i in coarse:
if i.Symbol not in self.dataDict:
self.dataDict[i.Symbol] = SymbolData(i.Symbol, self.lookback)
self.dataDict[i.Symbol].Update(i.AdjustedPrice)
if self.monthly_rebalance:
# drop stocks which have no fundamental data or have too low prices
filteredCoarse = [x.Symbol for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)]
return filteredCoarse
else:
return []
def FineSelectionFunction(self, fine):
if self.monthly_rebalance:
sortedFine = sorted(fine, key = lambda x: x.EarningReports.BasicAverageShares.Value * self.dataDict[x.Symbol].Price, reverse=True)
# select stocks with large size
topFine = sortedFine[:int(0.5*len(sortedFine))]
self.filteredFine = [x.Symbol for x in topFine]
return self.filteredFine
else:
return []
def Rebalance(self):
self.monthly_rebalance = True
def OnData(self, data):
if self.monthly_rebalance and self.filteredFine:
filtered_data = {symbol: symbolData for (symbol, symbolData) in self.dataDict.items() if symbol in self.filteredFine and symbolData.IsReady()}
self.filteredFine = None
self.monthly_rebalance = False
# if the dictionary is empty, then return
if len(filtered_data) < 100: return
# sort the universe by volatility and select stocks in the top high volatility quintile
sortedByVol = sorted(filtered_data.items(), key=lambda x: x[1].Volatility(), reverse = True)[:int(0.2*len(filtered_data))]
sortedByVol = dict(sortedByVol)
# sort the stocks in top-quintile by realized return
sortedByReturn = sorted(sortedByVol, key = lambda x: sortedByVol[x].Return(), reverse = True)
long = sortedByReturn[:int(0.2*len(sortedByReturn))]
short = sortedByReturn[-int(0.2*len(sortedByReturn)):]
self.portfolios.append(short+long)
# 1/6 of the portfolio is rebalanced every month
if len(self.portfolios) == self.portfolios.maxlen:
for i in list(self.portfolios)[0]:
self.Liquidate(i)
# stocks are equally weighted and held for 6 months
short_weight = 1/len(short)
for i in short:
self.SetHoldings(i, -1/6*short_weight)
long_weight = 1/len(long)
for i in long:
self.SetHoldings(i, 1/6*long_weight)
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, lookback):
self.symbol = symbol
# self.History = RollingWindow[Decimal](lookback)
self.History = deque(maxlen=lookback)
self.Price = None
def Update(self, value):
# update yesterday's close price
self.Price = value
# update the history price series
self.History.append(float(value))
# self.History.Add(value)
def IsReady(self):
return len(self.History) == self.History.maxlen
def Volatility(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self.History)[:-5]
returns = (prices[1:]-prices[:-1])/prices[:-1]
# calculate the annualized realized volatility
return np.std(returns)*np.sqrt(250/len(returns))
def Return(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self.History)[:-5]
# calculate the annualized realized return
return (prices[-1]-prices[0])/prices[0]