Earnings Announcement Reversal
Earnings Announcement Reversal 策略在财报公布日做空近期赢家并买入近期输家。
细节
- 入场条件:在财报日,做空最近收益为正的股票,买入收益为负的股票。
- 方向:双向。
- 出场条件:信号后调整仓位,没有明确持有规则。
- 止损:无。
- 默认值:
LookbackDays = 5HoldingDays = 3CandleType = TimeSpan.FromMinutes(5).TimeFrame()
- 筛选:
- 类别:事件驱动
- 方向:双向
- 指标:收益
- 止损:无
- 复杂度:中等
- 时间框架:日线
- 季节性:否
- 神经网络:否
- 背离:否
- 风险等级:中等
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trades short-term reversals around synthetic earnings announcement dates for the primary instrument.
/// </summary>
public class EarningsAnnouncementReversalStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackDays;
private readonly StrategyParam<int> _holdingDays;
private readonly StrategyParam<int> _eventCycleBars;
private readonly StrategyParam<decimal> _reversalThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private RateOfChange _momentum = null!;
private int _barIndex;
private int _holdingRemaining;
private int _cooldownRemaining;
private decimal _latestMomentum;
/// <summary>
/// Lookback period in bars used to determine winners and losers.
/// </summary>
public int LookbackDays
{
get => _lookbackDays.Value;
set => _lookbackDays.Value = value;
}
/// <summary>
/// Number of bars to hold the position.
/// </summary>
public int HoldingDays
{
get => _holdingDays.Value;
set => _holdingDays.Value = value;
}
/// <summary>
/// Distance between synthetic earnings events in bars.
/// </summary>
public int EventCycleBars
{
get => _eventCycleBars.Value;
set => _eventCycleBars.Value = value;
}
/// <summary>
/// Absolute momentum threshold used to classify recent winners and losers.
/// </summary>
public decimal ReversalThreshold
{
get => _reversalThreshold.Value;
set => _reversalThreshold.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="EarningsAnnouncementReversalStrategy"/>.
/// </summary>
public EarningsAnnouncementReversalStrategy()
{
_lookbackDays = Param(nameof(LookbackDays), 6)
.SetRange(2, 30)
.SetDisplay("Lookback Days", "Number of bars used to calculate recent return", "Parameters");
_holdingDays = Param(nameof(HoldingDays), 3)
.SetRange(1, 20)
.SetDisplay("Holding Days", "Bars to hold the position after the event", "Parameters");
_eventCycleBars = Param(nameof(EventCycleBars), 20)
.SetRange(8, 80)
.SetDisplay("Event Cycle Bars", "Distance between synthetic earnings events", "Parameters");
_reversalThreshold = Param(nameof(ReversalThreshold), 1.2m)
.SetRange(0.1m, 20m)
.SetDisplay("Reversal Threshold", "Absolute momentum threshold used to classify winners and losers", "Parameters");
_cooldownBars = Param(nameof(CooldownBars), 2)
.SetRange(0, 20)
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk");
_stopLoss = Param(nameof(StopLoss), 2.5m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_momentum = null!;
_barIndex = 0;
_holdingRemaining = 0;
_cooldownRemaining = 0;
_latestMomentum = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security is not specified.");
_momentum = new RateOfChange { Length = LookbackDays };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent));
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var momentumValue = _momentum.Process(candle);
if (momentumValue.IsEmpty || !_momentum.IsFormed || !IsFormedAndOnlineAndAllowTrading())
{
_barIndex++;
return;
}
_latestMomentum = momentumValue.ToDecimal();
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (_holdingRemaining > 0)
{
_holdingRemaining--;
if (_holdingRemaining == 0 && Position != 0)
{
if (Position > 0)
SellMarket(Position);
else
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
}
var isEventBar = _barIndex > 0 && _barIndex % EventCycleBars == 0;
if (_cooldownRemaining == 0 && Position == 0 && isEventBar)
{
if (_latestMomentum >= ReversalThreshold)
{
SellMarket();
_holdingRemaining = HoldingDays;
_cooldownRemaining = CooldownBars;
}
else if (_latestMomentum <= -ReversalThreshold)
{
BuyMarket();
_holdingRemaining = HoldingDays;
_cooldownRemaining = CooldownBars;
}
}
_barIndex++;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RateOfChange, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class earnings_announcement_reversal_strategy(Strategy):
"""Trades short-term reversals around synthetic earnings announcement dates
for the primary instrument."""
def __init__(self):
super(earnings_announcement_reversal_strategy, self).__init__()
self._lookback_days = self.Param("LookbackDays", 6) \
.SetRange(2, 30) \
.SetDisplay("Lookback Days", "Number of bars used to calculate recent return", "Parameters")
self._holding_days = self.Param("HoldingDays", 3) \
.SetRange(1, 20) \
.SetDisplay("Holding Days", "Bars to hold the position after the event", "Parameters")
self._event_cycle_bars = self.Param("EventCycleBars", 20) \
.SetRange(8, 80) \
.SetDisplay("Event Cycle Bars", "Distance between synthetic earnings events", "Parameters")
self._reversal_threshold = self.Param("ReversalThreshold", 1.2) \
.SetRange(0.1, 20.0) \
.SetDisplay("Reversal Threshold", "Absolute momentum threshold used to classify winners and losers", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 2) \
.SetRange(0, 20) \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk")
self._stop_loss = self.Param("StopLoss", 2.5) \
.SetRange(0.5, 10.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._momentum = None
self._bar_index = 0
self._holding_remaining = 0
self._cooldown_remaining = 0
self._latest_momentum = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
result = []
if self.Security is not None:
result.append((self.Security, self.candle_type))
return result
def OnReseted(self):
super(earnings_announcement_reversal_strategy, self).OnReseted()
self._momentum = None
self._bar_index = 0
self._holding_remaining = 0
self._cooldown_remaining = 0
self._latest_momentum = 0.0
def OnStarted2(self, time):
super(earnings_announcement_reversal_strategy, self).OnStarted2(time)
self._momentum = RateOfChange()
self._momentum.Length = int(self._lookback_days.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._momentum, candle)
civ.IsFinal = True
momentum_value = self._momentum.Process(civ)
if momentum_value.IsEmpty or not self._momentum.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
self._bar_index += 1
return
self._latest_momentum = float(momentum_value)
cooldown = int(self._cooldown_bars.Value)
holding_days = int(self._holding_days.Value)
event_cycle = int(self._event_cycle_bars.Value)
reversal_thresh = float(self._reversal_threshold.Value)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if self._holding_remaining > 0:
self._holding_remaining -= 1
if self._holding_remaining == 0 and self.Position != 0:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
is_event_bar = self._bar_index > 0 and self._bar_index % event_cycle == 0
if self._cooldown_remaining == 0 and self.Position == 0 and is_event_bar:
if self._latest_momentum >= reversal_thresh:
self.SellMarket()
self._holding_remaining = holding_days
self._cooldown_remaining = cooldown
elif self._latest_momentum <= -reversal_thresh:
self.BuyMarket()
self._holding_remaining = holding_days
self._cooldown_remaining = cooldown
self._bar_index += 1
def CreateClone(self):
return earnings_announcement_reversal_strategy()