Overall Statistics |
Total Trades
41208
Average Win
0.37%
Average Loss
-0.36%
Compounding Annual Return
22.559%
Drawdown
61.800%
Expectancy
0.043
Net Profit
1486.724%
Sharpe Ratio
0.805
Probabilistic Sharpe Ratio
11.862%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.03
Alpha
0.232
Beta
-0.023
Annual Standard Deviation
0.285
Annual Variance
0.081
Information Ratio
0.365
Tracking Error
0.342
Treynor Ratio
-10.14
Total Fees
$68398.13
Estimated Strategy Capacity
$25000.00
Lowest Capacity Asset
STZB RY8WNNRUWIN9
|
# https://quantpedia.com/strategies/reversal-during-earnings-announcements/ # # The investment universe consists of stocks listed at NYSE, AMEX, and NASDAQ, whose daily price data are available at the CRSP database. # Earnings-announcement dates are collected from Compustat. Firstly, the investor sorts stocks into quintiles based on firm size. Then he # further sorts the stocks in the top quintile (the biggest) into quintiles based on their average returns in the 3-day window between # t-4 and t-2, where t is the day of the earnings announcement. The investor goes long on the bottom quintile (past losers) and short on # the top quintile (past winners) and holds the stocks during the 3-day window between t-1, t, and t+1. Stocks in the portfolios are # weighted equally. # # QC Impelmentation: # - Universe consits of stocks, which have earnings dates in Quantpedia data. # - Maximum of 20 long and 20 short stock are held at the same time. import data_tools import numpy as np from collections import deque class ReversalDuringEarningsAnnouncements(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) # earnings dates start at 2008 self.SetCash(100000) self.ear_period = 4 self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol # Daily price data. self.data = {} # Import earnigns data. self.earnings_data = {} # Available symbols from earning_dates.csv. self.symbols = set() self.first_date = None csv_string_file = self.Download('data.quantpedia.com/backtesting_data/economic/earning_dates.csv') lines = csv_string_file.split('\r\n') for line in lines: if line == '': continue line_split = line.split(';') date = datetime.strptime(line_split[0], "%Y-%m-%d").date() if not self.first_date: self.first_date = date self.earnings_data[date] = [] for i in range(1, len(line_split)): symbol = line_split[i] self.earnings_data[date].append(symbol) self.symbols.add(symbol) # EAR history for previous quarter used for statistics. self.ear_previous_quarter = [] self.ear_actual_quarter = [] # 5 equally weighted brackets for traded symbols. - 20 symbols long , 20 for short, 3 days of holding. self.trade_manager = data_tools.TradeManager(self, 20, 20, 3) self.month = 12 self.selection_flag = False self.rebalance_flag = False self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFunction) self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: security.SetFeeModel(data_tools.CustomFeeModel(self)) security.SetLeverage(5) def CoarseSelectionFunction(self, coarse): # update daily prices for stock in coarse: symbol = stock.Symbol if symbol in self.data: self.data[symbol].Add(stock.AdjustedPrice) if not self.selection_flag: return Universe.Unchanged self.selection_flag = False selected = [x.Symbol for x in coarse if x.Symbol.Value in self.symbols] for symbol in selected: if symbol in self.data: continue self.data[symbol] = RollingWindow[float](self.ear_period) history = self.History(symbol, self.ear_period, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {symbol} yet") continue closes = history.loc[symbol].close for time, close in closes.iteritems(): self.data[symbol].Add(close) return selected def OnData(self, data): date_to_lookup = (self.Time + timedelta(days=1)).date() # Liquidate opened symbols after three days. self.trade_manager.TryLiquidate() ret_t4_t2 = {} for symbol in self.data: # Data is ready. if self.data[symbol].IsReady: # Earnings is in next two day for the symbol. if date_to_lookup in self.earnings_data and symbol.Value in self.earnings_data[date_to_lookup]: closes = [x for x in self.data[symbol]] # Calculate t-4 to t-2 return. ret = (closes[0] - closes[-1]) / closes[-1] ret_t4_t2[symbol] = ret # Store return in this month's history. self.ear_actual_quarter.append(ret) # Wait until we have history data for previous three months. if len(self.ear_previous_quarter) != 0: # Sort by EAR. ear_values = self.ear_previous_quarter top_ear_quintile = np.percentile(ear_values, 80) bottom_ear_quintile = np.percentile(ear_values, 20) # Store symbol to set. long = [x[0] for x in ret_t4_t2.items() if x[1] <= bottom_ear_quintile] short = [x[0] for x in ret_t4_t2.items() if x[1] >= top_ear_quintile] # Open new trades. for symbol in long: if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable: self.trade_manager.Add(symbol, True) for symbol in short: if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable: self.trade_manager.Add(symbol, False) def Selection(self): # There is no earnings data yet. if self.Time.date() < self.first_date: return self.selection_flag = True # Every three months. if self.month % 3 == 0: # Save quarter history. self.ear_previous_quarter = [x for x in self.ear_actual_quarter] self.ear_actual_quarter.clear() self.month += 1 if self.month > 12: self.month = 1
import numpy as np import statsmodels.api as sm # Custom fee model class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD")) # NOTE: Manager for new trades. It's represented by certain count of equally weighted brackets for long and short positions. # If there's a place for new trade, it will be managed for time of holding period. class TradeManager(): def __init__(self, algorithm, long_size, short_size, holding_period): self.algorithm = algorithm # algorithm to execute orders in. self.long_size = long_size self.short_size = short_size self.long_len = 0 self.short_len = 0 # Arrays of ManagedSymbols self.symbols = [] self.holding_period = holding_period # Days of holding. # Add stock symbol object def Add(self, symbol, long_flag): # Open new long trade. managed_symbol = ManagedSymbol(symbol, self.holding_period, long_flag) if long_flag: # If there's a place for it. if self.long_len < self.long_size: self.symbols.append(managed_symbol) self.algorithm.SetHoldings(symbol, 1 / self.long_size) self.long_len += 1 else: self.algorithm.Log("There's not place for additional trade.") # Open new short trade. else: # If there's a place for it. if self.short_len < self.short_size: self.symbols.append(managed_symbol) self.algorithm.SetHoldings(symbol, - 1 / self.short_size) self.short_len += 1 else: self.algorithm.Log("There's not place for additional trade.") # Decrement holding period and liquidate symbols. def TryLiquidate(self): symbols_to_delete = [] for managed_symbol in self.symbols: managed_symbol.days_to_liquidate -= 1 # Liquidate. if managed_symbol.days_to_liquidate == 0: symbols_to_delete.append(managed_symbol) self.algorithm.Liquidate(managed_symbol.symbol) if managed_symbol.long_flag: self.long_len -= 1 else: self.short_len -= 1 # Remove symbols from management. for managed_symbol in symbols_to_delete: self.symbols.remove(managed_symbol) def LiquidateTicker(self, ticker): symbol_to_delete = None for managed_symbol in self.symbols: if managed_symbol.symbol.Value == ticker: self.algorithm.Liquidate(managed_symbol.symbol) symbol_to_delete = managed_symbol if managed_symbol.long_flag: self.long_len -= 1 else: self.short_len -= 1 break if symbol_to_delete: self.symbols.remove(symbol_to_delete) else: self.algorithm.Debug("Ticker is not held in portfolio!") class ManagedSymbol(): def __init__(self, symbol, days_to_liquidate, long_flag): self.symbol = symbol self.days_to_liquidate = days_to_liquidate self.long_flag = long_flag