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')