| Overall Statistics |
|
Total Orders 552 Average Win 1.36% Average Loss -0.71% Compounding Annual Return 1.855% Drawdown 33.200% Expectancy 0.027 Start Equity 100000 End Equity 109632.12 Net Profit 9.632% Sharpe Ratio -0.159 Sortino Ratio -0.181 Probabilistic Sharpe Ratio 1.247% Loss Rate 65% Win Rate 35% Profit-Loss Ratio 1.90 Alpha -0.051 Beta 0.442 Annual Standard Deviation 0.114 Annual Variance 0.013 Information Ratio -0.754 Tracking Error 0.123 Treynor Ratio -0.041 Total Fees $639.78 Estimated Strategy Capacity $4500000.00 Lowest Capacity Asset WDAY VAP9FER7V5GL Portfolio Turnover 2.95% Drawdown Recovery 1108 |
# region imports
from AlgorithmImports import *
# endregion
class EarningsReversal(QCAlgorithm):
def Initialize(self):
self.set_start_date(self.end_date - timedelta(5*365))
self.set_cash(100000)
self.settings.seed_initial_prices = True
# ------------- Paramters --------------
# Minimum percentage decline before entry
self._entry_move = 0.02
# Max number of positions allowed at once (equal weighting)
self._max_positions = 10
# Size of first universe selection liquidity filter
self._liquidity_filter_size = 6*self._max_positions
# Number of days past since earnings
self._days_since_earnings = 1
# Percentage offset of trailing stop loss
self._stop_loss = 0.10
# --------------------------------------
# Add a universe of US Equities.
self._universe = self.add_universe(self._select_assets)
# Add a Scheduled Event to open new positions.
self.schedule.on(
self.date_rules.every_day('SPY'),
self.time_rules.at(8, 0),
self._trade
)
# Add a custom bar chart.
chart = Chart('Positions')
chart.add_series(Series('Longs', SeriesType.BAR, 0))
self.add_chart(chart)
def _select_assets(self, fundamentals):
# Select the most liquid stocks trading above $5.
fundamentals = [f for f in fundamentals if f.price > 5 and f.has_fundamental_data]
fundamentals = sorted(fundamentals, key=lambda f: f.dollar_volume, reverse=True)[:self._liquidity_filter_size]
# Select the subset of stocks with recent earnings.
fundamentals = [
f for f in fundamentals
if self.time == f.earning_reports.file_date.value + timedelta(self._days_since_earnings)
]
# Select the subset of stocks that have fallen in price since the earnings release.
prices_around_earnings = self.history([f.symbol for f in fundamentals], self._days_since_earnings+3, Resolution.DAILY)
universe = []
for f in fundamentals:
# Find tradeable date closest to specified number of days before earnings
date = min(
prices_around_earnings.loc[f.symbol]["close"].index,
key=lambda x:abs(x-(f.earning_reports.file_date.value - timedelta(1)))
)
price_on_earnings = prices_around_earnings.loc[f.symbol]["close"][date]
# Check if stock fell far enough.
if price_on_earnings * (1-self._entry_move) > f.price:
universe.append(f.symbol)
return universe
# Open equal weighted positions with trailing stop losses.
def _trade(self):
# Plot number of existing positions
positions = [symbol for symbol, holding in self.portfolio.items() if holding.invested]
self.plot("Positions", "Longs", len(positions))
# Enter new trades if there are assets in the universe.
if not self._universe.selected:
return
available_trades = self._max_positions - len(positions)
for symbol in [x for x in self._universe.selected if x not in positions][:available_trades]:
# Buy the stock.
self.set_holdings(symbol, 1 / self._max_positions)
def on_order_event(self, order_event: OrderEvent) -> None:
if (order_event.status != OrderStatus.FILLED or
order_event.ticket.order_type != OrderType.MARKET_ON_OPEN):
return
# Add a trailing stop order.
self.trailing_stop_order(order_event.symbol, -order_event.quantity, self._stop_loss, True)