| Overall Statistics |
|
Total Orders 1085 Average Win 1.49% Average Loss -1.26% Compounding Annual Return 5.261% Drawdown 40.300% Expectancy 0.068 Start Equity 100000 End Equity 129232.43 Net Profit 29.232% Sharpe Ratio 0.141 Sortino Ratio 0.157 Probabilistic Sharpe Ratio 2.346% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.18 Alpha 0.019 Beta 0.334 Annual Standard Deviation 0.282 Annual Variance 0.079 Information Ratio -0.079 Tracking Error 0.293 Treynor Ratio 0.118 Total Fees $1539.19 Estimated Strategy Capacity $100000000.00 Lowest Capacity Asset CORT SXTX9AOGG2G5 Portfolio Turnover 5.88% Drawdown Recovery 671 |
#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(self.end_date - timedelta(5*365))
self.set_cash(100_000)
self.settings.seed_initial_prices = True
# Define some parameters.
self._liquidity_filter_size = 100
self._position_per_size = 5
# Add a universe of US Equities based on historical
# same-calendar month returns.
self._date_rule = self.date_rules.month_start('SPY')
self.universe_settings.schedule.on(self._date_rule)
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(self._select_assets)
# Add a warm-up period so that algorithm trades on deployment.
self.set_warm_up(timedelta(45))
def _select_assets(self, fundamentals):
'''
Universe selection based on historical same-calendar month returns
'''
# Select the most liquid US Equities above $5.
filtered = [f for f in fundamentals if f.price > 5]
filtered = sorted(filtered, key=lambda x: x.dollar_volume)[-self._liquidity_filter_size:]
# Get price data of all the filtered Equities for the same
# calendar month in the previous year.
start = self.time.replace(day=1, year=self.time.year-1)
end = Expiry.END_OF_MONTH(start) - timedelta(1)
history = self.history([f.symbol for f in filtered], start, end, Resolution.DAILY).close.unstack(level=0)
# Calculate the monthly return of each Equity during that period.
monthly_return_by_symbol = {
symbol: prices.iloc[-1] / prices.iloc[0] - 1
for symbol, prices in history.items()
}
# Split the Equities into long and short buckets.
sorted_by_return = sorted(monthly_return_by_symbol, key=lambda symbol: monthly_return_by_symbol[symbol])
self._longs = sorted_by_return[-self._position_per_size:]
self._shorts = sorted_by_return[:self._position_per_size]
return self._longs + self._shorts
def on_warmup_finished(self):
# Add a Scheduled Event to rebalance the portfolio each month.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self._date_rule, time_rule, self._rebalance)
# Rebalance today too.
if self.live_mode:
self._rebalance()
else:
self.schedule.on(self.date_rules.today, time_rule, self._rebalance)
def _rebalance(self):
'''
Rebalance every month based on same-calendar month returns effect
'''
# Form a long-short portfolio.
targets = [PortfolioTarget(symbol, 0.5/len(self._longs)) for symbol in self._longs]
targets += [PortfolioTarget(symbol, -0.5/len(self._shorts)) for symbol in self._shorts]
self.set_holdings(targets, True)