
The strategy identifies NYSE, AMEX, and NASDAQ stocks with significant price and volume swings, selecting those with analyst revisions, holding equally weighted positions for one month, rebalancing monthly.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Large, Price, Analyst, Revisions
I. STRATEGY IN A NUTSHELL
The strategy invests in NYSE, AMEX, and NASDAQ stocks that experience a +5% one-day price surge with trading volume above 1.1 times the 45-day average. Within five days, analysts’ target price revisions are assessed—stocks with mostly upward revisions are bought, while those with downward revisions are sold. Positions are equally weighted, held for one month, and rebalanced monthly, exploiting analyst reactions to sharp price and volume movements.
II. ECONOMIC RATIONALE
Sharp price jumps may stem from either noise or fundamental news. Analyst revisions after such moves increase the likelihood that new information drives the change. The persistence of this effect is explained by limits-to-arbitrage theory, as it is not risk-free and arbitrageurs lack unlimited capital to fully eliminate the inefficiency.
III. SOURCE PAPER
Large Price Changes and Subsequent Returns [Click to Open PDF]
Govindaraj, Livnat, Savor, Zhao
<Abstract>
We investigate whether large stock price changes are associated with short-term reversals or momentum, conditional on the issuance of analyst price target or earnings forecast revisions immediately following these price changes. Our study provides evidence that when analyst revisions occur immediately after large price shocks, stock prices exhibit momentum, suggesting the initial price change was based on new information. In contrast, when price changes are not followed by immediate analyst revisions, we document short-term reversals, indicating that the initial price shocks were probably caused by liquidity or noise traders. A trading strategy that is based on the direction of the price change and the existence of immediate analyst revisions in the same direction earns significant abnormal monthly calendar-time returns.

IV. BACKTEST PERFORMANCE
| Annualised Return | 11.22% |
| Volatility | 11.23% |
| Beta | 1.016 |
| Sharpe Ratio | N/A |
| Sortino Ratio | 0.468 |
| Maximum Drawdown | N/A |
| Win Rate | 57% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from pandas.tseries.offsets import BDay
from typing import List, Dict
import data_tools
# endregion
class LargePriceChangesCombinedWithAnalystRevisions(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2010, 1, 1) # estimize dataset starts in 2011
self.SetCash(100_000)
self.years_period: int = 3
self.low_high_percentage: int = 30
self.min_values: int = 15
self.period: int = 45 # need n values for mean volumes calculation
self.volume_percentage: float = 1.1
self.return_increase: float = 0.05
self.days_for_revision: int = 5
self.leverage: int = 5
self.min_share_price: int = 5
self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']
self.data: Dict[Symbol, SymbolData] = {}
self.weights: Dict[Symbol, float] = {}
self.already_subscribed: Dict[Symbol] = []
self.estimates: Dict[str, Dict[datetime.date, List[str]]] = {}
self.analysts_data: Dict[str, Dict[datetime.date, float]] = {}
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.fundamental_count: int = 500
self.rebalance_flag: bool = False
self.selection_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
curr_date: datetime.date = self.Time.date()
# daily update of prices and volumes
for equity in fundamental:
symbol: Symbol = equity.Symbol
if symbol in self.data:
self.data[symbol].update(curr_date, equity.AdjustedPrice, equity.Volume)
# monthly selection
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
self.rebalance_flag = True
selected: List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.MarketCap != 0 and x.Market == 'usa' and x.Price > self.min_share_price and \
x.SecurityReference.ExchangeId in self.exchange_codes
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
selected_stocks: set = set()
for stock in selected:
symbol: Symbol = stock.Symbol
ticker: str = symbol.Value
# check if stock is already subscribed
if symbol not in self.data:
self.data[symbol] = data_tools.SymbolData(self.period)
self.AddData(EstimizeEstimate, symbol)
if not self.data[symbol].is_ready(self.min_values):
continue
if ticker not in self.estimates:
continue
large_swing_dates: List[datetime.date] = self.data[symbol].get_large_swing_dates(self.volume_percentage, self.return_increase)
# iterate through each large swing date and check if any analyst increased estimated EPS within self.days_for_revision days
for date in large_swing_dates:
for i in range(1, self.days_for_revision + 1, 1):
future_date: datetime.date = (date + BDay(i)).date()
if future_date not in self.estimates[ticker]:
continue
analyst_ids: List[str] = self.estimates[ticker][future_date]
for analyst_id in analyst_ids:
estimate_dates: List[datetime.date] = list(self.analysts_data[analyst_id].keys())
estimate_dates.reverse()
est_after_swing: float = self.analysts_data[analyst_id][future_date]
latest_date_before_swing: datetime.date = next((est_date for est_date in estimate_dates if est_date < date), None)
# check if analyst increased his/her EPS estimate value
if latest_date_before_swing != None and (est_after_swing > self.analysts_data[analyst_id][latest_date_before_swing]):
selected_stocks.add(symbol)
break
# stock were already selected, no need to check any more dates
if selected_stocks in selected_stocks:
break
# reset monthly data
for symbol, symbol_obj in self.data.items():
symbol_obj.reset_monthly_data()
long_length: int = len(selected_stocks)
for symbol in selected_stocks:
self.weights[symbol] = 1 / long_length
return list(selected_stocks)
def OnData(self, data: Slice) -> None:
estimate = data.Get(EstimizeEstimate)
for symbol, value in estimate.items():
ticker: str = symbol.Value
if ticker not in self.estimates:
self.estimates[ticker] = {}
created_at: datetime.date = value.CreatedAt.date()
if created_at not in self.estimates[ticker]:
self.estimates[ticker][created_at] = []
analyst_id: str = value.AnalystId
self.estimates[ticker][created_at].append(analyst_id)
if analyst_id not in self.analysts_data:
self.analysts_data[analyst_id] = {}
self.analysts_data[analyst_id][created_at] = value.Eps
# rebalance when selection was made
if not self.rebalance_flag:
return
self.rebalance_flag = False
# reset monthly data
for _, symbol_obj in self.data.items():
symbol_obj.reset_monthly_data()
# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()
def Selection(self) -> None:
self.selection_flag = True