| Overall Statistics |
|
Total Orders 2391 Average Win 0.59% Average Loss -0.63% Compounding Annual Return -3.539% Drawdown 44.500% Expectancy -0.025 Start Equity 100000 End Equity 76078.46 Net Profit -23.922% Sharpe Ratio -0.211 Sortino Ratio -0.23 Probabilistic Sharpe Ratio 0.010% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 0.93 Alpha -0.065 Beta 0.41 Annual Standard Deviation 0.135 Annual Variance 0.018 Information Ratio -0.825 Tracking Error 0.143 Treynor Ratio -0.07 Total Fees $4265.60 Estimated Strategy Capacity $66000000.00 Lowest Capacity Asset TWOU VP9395D0KIUD Portfolio Turnover 7.55% |
#region imports
from AlgorithmImports import *
#endregion
class SeasonalitySignalAlgorithm(QCAlgorithm):
'''
A strategy that takes long and short positions based on historical same-calendar month returns
Paper: https://www.nber.org/papers/w20815.pdf
'''
def initialize(self):
self.set_start_date(2012, 1, 1) # Set Start Date
self.set_end_date(2019, 8, 1) # Set End Date
self.set_cash(100000) # Set Strategy Cash
self._num_coarse = 100 # Number of equities for coarse selection
self._num_long = 5 # Number of equities to long
self._num_short = 5 # Number of equities to short
self._long_symbols = [] # Contain the equities we'd like to long
self._short_symbols = [] # Contain the equities we'd like to short
self.universe_settings.resolution = Resolution.DAILY # Resolution of universe selection
self._universe = self.add_universe(self._same_month_return_selection) # Universe selection based on historical same-calendar month returns
self._next_rebalance = self.time # Next rebalance time
def _same_month_return_selection(self, coarse):
'''
Universe selection based on historical same-calendar month returns
'''
# Before next rebalance time, just remain the current universe
if self.time < self._next_rebalance:
return Universe.UNCHANGED
# Sort the equities with prices > 5 in DollarVolume decendingly
selected = sorted([x for x in coarse if x.price > 5],
key=lambda x: x.dollar_volume, reverse=True)
# Get equities after coarse selection
symbols = [x.symbol for x in selected[:self._num_coarse]]
# Get historical close data for coarse-selected symbols of the same calendar month
start = self.time.replace(day = 1, year = self.time.year-1)
end = Expiry.end_of_month(start) - timedelta(1)
history = self.history(symbols, start, end, Resolution.DAILY).close.unstack(level=0)
# Get the same calendar month returns for the symbols
monthly_return = {ticker: prices.iloc[-1]/prices.iloc[0] for ticker, prices in history.items()}
# Sorted the values of monthly return
sorted_return = sorted(monthly_return.items(), key=lambda x: x[1], reverse=True)
# Get the symbols to long / short
self._long_symbols = [x[0] for x in sorted_return[:self._num_long]]
self._short_symbols = [x[0] for x in sorted_return[-self._num_short:]]
# Note that self._long_symbols/self._short_symbols contains strings instead of symbols
return [x for x in symbols if str(x) in self._long_symbols + self._short_symbols]
def _get_tradable_assets(self, symbols):
return [s for s in symbols if self.securities[s].is_tradable]
def on_data(self, data):
'''
Rebalance every month based on same-calendar month returns effect
'''
# Before next rebalance, do nothing
if self.time < self._next_rebalance:
return
long_symbols = self._get_tradable_assets(self._long_symbols)
short_symbols = self._get_tradable_assets(self._short_symbols)
# Open long positions
for symbol in long_symbols:
self.set_holdings(symbol, 0.5/len(long_symbols))
# Open short positions
for symbol in short_symbols:
self.set_holdings(symbol, -0.5/len(short_symbols))
# Rebalance at the end of every month
self._next_rebalance = Expiry.end_of_month(self.time) - timedelta(1)
def on_securities_changed(self, changes):
'''
Liquidate the stocks that are not in the universe
'''
for security in changes.removed_securities:
if security.invested:
self.liquidate(security.symbol, 'Removed from Universe')