PEAD Strategy
This strategy trades post-earnings announcement drift after a positive EPS surprise and gap-up. It enters long on the day after earnings when price gaps up and recent performance is positive, using an EMA trail, fixed stop/breakeven, and max holding period.
Details
- Entry Criteria: Positive EPS surprise, gap-up after earnings, and prior positive performance.
- Long/Short: Long only.
- Exit Criteria: Daily EMA cross-under, fixed stop/breakeven, or max holding bars.
- Stops: Fixed stop with breakeven.
- Default Values:
GapThreshold= 1EpsSurpriseThreshold= 5PerfDays= 20StopPct= 8EmaLen= 50MaxHoldBars= 50CandleType= TimeSpan.FromDays(1)
- Filters:
- Category: Earnings
- Direction: Long
- Indicators: EMA
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Daily
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Post-earnings announcement drift strategy with gap and EMA exit.
/// </summary>
public class PeadStrategy : Strategy
{
private readonly StrategyParam<decimal> _gapThreshold;
private readonly StrategyParam<decimal> _epsSurpriseThreshold;
private readonly StrategyParam<int> _perfDays;
private readonly StrategyParam<decimal> _stopPct;
private readonly StrategyParam<int> _emaLen;
private readonly StrategyParam<int> _maxHoldBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevEma;
private decimal _prevRoc;
/// <summary>
/// Gap-up threshold (%).
/// </summary>
public decimal GapThreshold
{
get => _gapThreshold.Value;
set => _gapThreshold.Value = value;
}
/// <summary>
/// EPS surprise threshold (%).
/// </summary>
public decimal EpsSurpriseThreshold
{
get => _epsSurpriseThreshold.Value;
set => _epsSurpriseThreshold.Value = value;
}
/// <summary>
/// Positive-performance look-back.
/// </summary>
public int PerfDays
{
get => _perfDays.Value;
set => _perfDays.Value = value;
}
/// <summary>
/// Initial fixed stop-loss (%).
/// </summary>
public decimal StopPct
{
get => _stopPct.Value;
set => _stopPct.Value = value;
}
/// <summary>
/// Daily EMA length for trail stop.
/// </summary>
public int EmaLen
{
get => _emaLen.Value;
set => _emaLen.Value = value;
}
/// <summary>
/// Exit after N bars from entry.
/// </summary>
public int MaxHoldBars
{
get => _maxHoldBars.Value;
set => _maxHoldBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PeadStrategy()
{
_gapThreshold = Param(nameof(GapThreshold), 1m)
.SetDisplay("Gap Threshold", "Gap-up threshold (%)", "General")
;
_epsSurpriseThreshold = Param(nameof(EpsSurpriseThreshold), 5m)
.SetDisplay("EPS Surprise", "EPS surprise threshold (%)", "General")
;
_perfDays = Param(nameof(PerfDays), 20)
.SetDisplay("Performance Days", "Positive-performance look-back", "General")
;
_stopPct = Param(nameof(StopPct), 8m)
.SetDisplay("Stop Percent", "Initial fixed stop-loss (%)", "Risk")
;
_emaLen = Param(nameof(EmaLen), 50)
.SetDisplay("EMA Length", "Daily EMA length for trail stop", "Indicators")
;
_maxHoldBars = Param(nameof(MaxHoldBars), 50)
.SetDisplay("Max Hold Bars", "Exit after N bars from entry", "General")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevClose = 0;
_prevEma = 0;
_prevRoc = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaLen };
var roc = new RateOfChange { Length = PerfDays + 1 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, roc, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(3, UnitTypes.Percent),
stopLoss: new Unit(StopPct, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawIndicator(area, roc);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal rocValue)
{
if (candle.State != CandleStates.Finished)
return;
// Use strong candle body instead of gap (gaps don't exist in continuous 5min data)
var bodyPct = _prevClose != 0 ? Math.Abs(candle.ClosePrice - candle.OpenPrice) / _prevClose * 100m : 0m;
var strongMove = bodyPct >= GapThreshold;
var perfPos = _prevRoc > 0;
var entryCond = perfPos && strongMove && Position == 0;
if (entryCond)
{
BuyMarket();
}
_prevClose = candle.ClosePrice;
_prevEma = emaValue;
_prevRoc = rocValue;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, RateOfChange
from StockSharp.Algo.Strategies import Strategy
class pead_strategy(Strategy):
def __init__(self):
super(pead_strategy, self).__init__()
self._gap_threshold = self.Param("GapThreshold", 1.0)
self._perf_days = self.Param("PerfDays", 20)
self._stop_pct = self.Param("StopPct", 8.0)
self._ema_len = self.Param("EmaLen", 50)
self._max_hold_bars = self.Param("MaxHoldBars", 50)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._prev_close = 0.0
self._prev_roc = 0.0
self._entry_price = 0.0
self._bars_in_trade = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(pead_strategy, self).OnReseted()
self._prev_close = 0.0
self._prev_roc = 0.0
self._entry_price = 0.0
self._bars_in_trade = 0
def OnStarted2(self, time):
super(pead_strategy, self).OnStarted2(time)
self._prev_close = 0.0
self._prev_roc = 0.0
self._entry_price = 0.0
self._bars_in_trade = 0
self._ema = ExponentialMovingAverage()
self._ema.Length = self._ema_len.Value
self._roc = RateOfChange()
self._roc.Length = self._perf_days.Value + 1
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ema, self._roc, self.OnProcess).Start()
self.StartProtection(
Unit(3, UnitTypes.Percent),
Unit(self._stop_pct.Value, UnitTypes.Percent)
)
def OnProcess(self, candle, ema_val, roc_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
opn = float(candle.OpenPrice)
ev = float(ema_val)
rv = float(roc_val)
body_pct = abs(close - opn) / self._prev_close * 100.0 if self._prev_close != 0 else 0.0
gt = float(self._gap_threshold.Value)
strong_move = body_pct >= gt
perf_pos = self._prev_roc > 0
if perf_pos and strong_move and self.Position == 0:
self.BuyMarket()
self._prev_close = close
self._prev_roc = rv
def CreateClone(self):
return pead_strategy()