| Overall Statistics |
|
Total Trades
305
Average Win
0.34%
Average Loss
-0.02%
Compounding Annual Return
14.997%
Drawdown
68.800%
Expectancy
4.923
Net Profit
420.323%
Sharpe Ratio
0.515
Probabilistic Sharpe Ratio
1.659%
Loss Rate
74%
Win Rate
26%
Profit-Loss Ratio
21.47
Alpha
0.004
Beta
1.632
Annual Standard Deviation
0.289
Annual Variance
0.084
Information Ratio
0.319
Tracking Error
0.189
Treynor Ratio
0.091
Total Fees
$68.68
Estimated Strategy Capacity
$970000.00
Lowest Capacity Asset
LBTYB SZC2UFSQNK9X
|
# https://quantpedia.com/strategies/earnings-announcements-combined-with-stock-repurchases/
#
# The investment universe consists of stocks from NYSE/AMEX/Nasdaq (no ADRs, CEFs or REITs), bottom 25% of firms by market cap are dropped.
# Each quarter, the investor looks for companies that announce a stock repurchase program (with announced buyback for at least 5% of outstanding stocks)
# during days -30 to -15 before the earnings announcement date for each company.
# Investor goes long stocks with announced buybacks during days -10 to +15 around an earnings announcement.
# The portfolio is equally weighted and rebalanced daily.
#
# QC Implementation:
# - Universe consists of tickers, which have earnings annoucement.
#region imports
from AlgorithmImports import *
import numpy as np
#endregion
class EarningsAnnouncementsCombinedWithStockRepurchases(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2011, 1, 1) # Buyback data strats at 2011
self.SetCash(100000)
self.fine = {}
self.price = {}
self.managed_symbols = []
self.earnings_universe = []
self.earnings = {}
self.buybacks = {}
self.max_traded_stocks = 40 # maximum number of trading stocks
self.quantile = 4
self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
# load earnings dates
csv_data = self.Download('data.quantpedia.com/backtesting_data/economic/earning_dates.csv')
lines = csv_data.split('\r\n')
for line in lines:
line_split = line.split(';')
date = line_split[0]
if date == '' :
continue
date = datetime.strptime(date, "%Y-%m-%d").date()
self.earnings[date] = []
for ticker in line_split[1:]: # skip date in current line
self.earnings[date].append(ticker)
if ticker not in self.earnings_universe:
self.earnings_universe.append(ticker)
# load buyback dates
csv_data = self.Download('data.quantpedia.com/backtesting_data/equity/BUY_BACKS.csv')
lines = csv_data.split('\r\n')
for line in lines[1:]: # skip header
line_split = line.split(';')
date = line_split[0]
if date == '' :
continue
date = datetime.strptime(date, "%d.%m.%Y").date()
self.buybacks[date] = []
for ticker in line_split[1:]: # skip date in current line
self.buybacks[date].append(ticker)
self.months_counter = 0
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
def CoarseSelectionFunction(self, coarse):
# update stocks last prices
for stock in coarse:
ticker = stock.Symbol.Value
if ticker in self.earnings_universe:
# store stock's last price
self.price[ticker] = stock.AdjustedPrice
# rebalance quarterly
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
# select stocks, which had spin off
selected = [x.Symbol for x in coarse if x.Symbol.Value in self.earnings_universe]
return selected
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and
((x.SecurityReference.ExchangeId == "NYS") or
(x.SecurityReference.ExchangeId == "NAS") or
(x.SecurityReference.ExchangeId == "ASE"))]
if len(fine) < self.quantile:
return Universe.Unchanged
# exclude 25% stocks with lowest market capitalization
quantile = int(len(fine) / self.quantile)
sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap)
selected = sorted_by_market_cap[quantile:]
self.fine = {x.Symbol.Value : x.Symbol for x in selected}
return list(self.fine.values())
def OnData(self, data:Slice) -> None:
remove_managed_symbols = []
# maybe there should be BDay(15)
liquidate_date = self.Time.date() - timedelta(15)
# check if bought stocks have 15 days after earnings annoucemnet
for managed_symbol in self.managed_symbols:
if managed_symbol.earnings_date >= liquidate_date:
remove_managed_symbols.append(managed_symbol)
# liquidate stock by selling it's quantity
self.MarketOrder(managed_symbol.symbol, -managed_symbol.quantity)
# remove liquidated stocks from self.managed_symbols
for managed_symbol in remove_managed_symbols:
self.managed_symbols.remove(managed_symbol)
# maybe there should be BDay(10)
after_current = self.Time.date() + timedelta(10)
if after_current in self.earnings:
# this stocks has earnings annoucement after 10 days
stocks_with_earnings = self.earnings[after_current]
# 30 days before earnings annoucement
buyback_start = self.Time.date() - timedelta(20)
# 15 days before earnings annoucement
buyback_end = self.Time.date() - timedelta(5)
stocks_with_buyback = [] # storing stocks with buyback in period -30 to -15 days before earnings annoucement
for buyback_date, tickers in self.buybacks.items():
# check if buyback date is in period before earnings annoucement
if buyback_date >= buyback_start and buyback_date <= buyback_end:
# iterate through each stock ticker for buyback date
for ticker in tickers:
# add stock ticker if it isn't already added, it has earnings annoucement after 10 days and was selected in fine
if (ticker not in stocks_with_buyback) and (ticker in stocks_with_earnings) and (ticker in self.fine):
stocks_with_buyback.append(self.fine[ticker])
# buying stocks buyback in period -30 to -15 days before earnings annoucement
# and stocks, which have earnings date -10 days before current date
for symbol in stocks_with_buyback:
# check if there is a place in Portfolio for trading current stock
if not len(self.managed_symbols) < self.max_traded_stocks:
continue
# calculate stock quantity
weight = self.Portfolio.TotalPortfolioValue / self.max_traded_stocks
quantity = np.floor(weight / self.price[symbol.Value])
# go long stock
self.MarketOrder(symbol, quantity)
# store stock's ticker, earnings date and traded quantity
if symbol in data and data[symbol]:
self.managed_symbols.append(ManagedSymbol(symbol, after_current, quantity))
def Selection(self):
# quarterly selection
if self.months_counter % 3 == 0:
self.selection_flag = True
self.months_counter += 1
class ManagedSymbol():
def __init__(self, symbol, earnings_date, quantity):
self.symbol = symbol
self.earnings_date = earnings_date
self.quantity = quantity
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))