Earnings Announcements With Buybacks
Earnings Announcements With Buybacks 策略在公司财报前几天若存在股票回购计划则买入,并在财报后不久卖出。
细节
- 入场条件:若公司有回购,在财报前
DaysBefore天买入。 - 方向:仅多头。
- 出场条件:在财报后
DaysAfter天卖出。 - 止损:无。
- 默认值:
DaysBefore = 5DaysAfter = 1CandleType = 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>
/// Strategy that buys the primary instrument before synthetic earnings events when a synthetic buyback regime is active and exits after the event.
/// </summary>
public class EarningsAnnouncementsWithBuybacksStrategy : Strategy
{
private readonly StrategyParam<int> _daysBefore;
private readonly StrategyParam<int> _daysAfter;
private readonly StrategyParam<int> _eventCycleBars;
private readonly StrategyParam<int> _buybackLength;
private readonly StrategyParam<decimal> _buybackThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _buybackProxy = null!;
private int _barIndex;
private int _holdingRemaining;
private int _cooldownRemaining;
private decimal _latestBuybackValue;
/// <summary>
/// Number of bars before the synthetic earnings event to enter.
/// </summary>
public int DaysBefore
{
get => _daysBefore.Value;
set => _daysBefore.Value = value;
}
/// <summary>
/// Number of bars after the synthetic earnings event to exit.
/// </summary>
public int DaysAfter
{
get => _daysAfter.Value;
set => _daysAfter.Value = value;
}
/// <summary>
/// Distance between synthetic earnings events in bars.
/// </summary>
public int EventCycleBars
{
get => _eventCycleBars.Value;
set => _eventCycleBars.Value = value;
}
/// <summary>
/// Smoothing length for the synthetic buyback activity proxy.
/// </summary>
public int BuybackLength
{
get => _buybackLength.Value;
set => _buybackLength.Value = value;
}
/// <summary>
/// Minimum synthetic buyback score required to enter.
/// </summary>
public decimal BuybackThreshold
{
get => _buybackThreshold.Value;
set => _buybackThreshold.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 price data.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="EarningsAnnouncementsWithBuybacksStrategy"/>.
/// </summary>
public EarningsAnnouncementsWithBuybacksStrategy()
{
_daysBefore = Param(nameof(DaysBefore), 3)
.SetRange(1, 10)
.SetDisplay("Days Before", "Bars before the synthetic earnings event to enter", "Trading");
_daysAfter = Param(nameof(DaysAfter), 1)
.SetRange(1, 10)
.SetDisplay("Days After", "Bars after the synthetic earnings event to exit", "Trading");
_eventCycleBars = Param(nameof(EventCycleBars), 20)
.SetRange(8, 80)
.SetDisplay("Event Cycle Bars", "Distance between synthetic earnings events", "Trading");
_buybackLength = Param(nameof(BuybackLength), 8)
.SetRange(2, 40)
.SetDisplay("Buyback Length", "Smoothing length for the synthetic buyback proxy", "Indicators");
_buybackThreshold = Param(nameof(BuybackThreshold), 0.7m)
.SetRange(-5m, 5m)
.SetDisplay("Buyback Threshold", "Minimum synthetic buyback score required to enter", "Indicators");
_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();
_buybackProxy = null!;
_barIndex = 0;
_holdingRemaining = 0;
_cooldownRemaining = 0;
_latestBuybackValue = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security is not specified.");
_buybackProxy = new ExponentialMovingAverage { Length = BuybackLength };
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 buybackSignal = CalculateBuybackSignal(candle);
_latestBuybackValue = _buybackProxy.Process(buybackSignal, candle.OpenTime, true).ToDecimal();
if (!_buybackProxy.IsFormed || !IsFormedAndOnlineAndAllowTrading())
{
_barIndex++;
return;
}
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (_holdingRemaining > 0)
{
_holdingRemaining--;
if (_holdingRemaining == 0 && Position > 0)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
}
var barsToEvent = EventCycleBars - (_barIndex % EventCycleBars);
var inEntryWindow = barsToEvent <= DaysBefore && barsToEvent > 0;
var buybackActive = _latestBuybackValue >= BuybackThreshold;
if (_cooldownRemaining == 0 && Position == 0 && inEntryWindow && buybackActive)
{
BuyMarket();
_holdingRemaining = DaysAfter + 1;
_cooldownRemaining = CooldownBars;
}
_barIndex++;
}
private decimal CalculateBuybackSignal(ICandleMessage candle)
{
var priceBase = Math.Max(candle.OpenPrice, 1m);
var range = Math.Max(candle.HighPrice - candle.LowPrice, Security?.PriceStep ?? 1m);
var closeLocation = ((candle.ClosePrice - candle.LowPrice) - (candle.HighPrice - candle.ClosePrice)) / range;
var compression = 1m - Math.Min(0.2m, range / priceBase);
return (closeLocation * 2m) + compression;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class earnings_announcements_with_buybacks_strategy(Strategy):
"""Strategy that buys the primary instrument before synthetic earnings events
when a synthetic buyback regime is active and exits after the event."""
def __init__(self):
super(earnings_announcements_with_buybacks_strategy, self).__init__()
self._days_before = self.Param("DaysBefore", 3) \
.SetRange(1, 10) \
.SetDisplay("Days Before", "Bars before the synthetic earnings event to enter", "Trading")
self._days_after = self.Param("DaysAfter", 1) \
.SetRange(1, 10) \
.SetDisplay("Days After", "Bars after the synthetic earnings event to exit", "Trading")
self._event_cycle_bars = self.Param("EventCycleBars", 20) \
.SetRange(8, 80) \
.SetDisplay("Event Cycle Bars", "Distance between synthetic earnings events", "Trading")
self._buyback_length = self.Param("BuybackLength", 8) \
.SetRange(2, 40) \
.SetDisplay("Buyback Length", "Smoothing length for the synthetic buyback proxy", "Indicators")
self._buyback_threshold = self.Param("BuybackThreshold", 0.7) \
.SetRange(-5.0, 5.0) \
.SetDisplay("Buyback Threshold", "Minimum synthetic buyback score required to enter", "Indicators")
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._buyback_proxy = None
self._bar_index = 0
self._holding_remaining = 0
self._cooldown_remaining = 0
self._latest_buyback_value = 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_announcements_with_buybacks_strategy, self).OnReseted()
self._buyback_proxy = None
self._bar_index = 0
self._holding_remaining = 0
self._cooldown_remaining = 0
self._latest_buyback_value = 0.0
def OnStarted2(self, time):
super(earnings_announcements_with_buybacks_strategy, self).OnStarted2(time)
self._buyback_proxy = ExponentialMovingAverage()
self._buyback_proxy.Length = int(self._buyback_length.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
buyback_signal = self.CalculateBuybackSignal(candle)
result = process_float(self._buyback_proxy, buyback_signal, candle.OpenTime, True)
self._latest_buyback_value = float(result)
if not self._buyback_proxy.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
self._bar_index += 1
return
cooldown = int(self._cooldown_bars.Value)
days_before = int(self._days_before.Value)
days_after = int(self._days_after.Value)
event_cycle = int(self._event_cycle_bars.Value)
buyback_thresh = float(self._buyback_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:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
bars_to_event = event_cycle - (self._bar_index % event_cycle)
in_entry_window = bars_to_event <= days_before and bars_to_event > 0
buyback_active = self._latest_buyback_value >= buyback_thresh
if self._cooldown_remaining == 0 and self.Position == 0 and in_entry_window and buyback_active:
self.BuyMarket()
self._holding_remaining = days_after + 1
self._cooldown_remaining = cooldown
self._bar_index += 1
def CalculateBuybackSignal(self, candle):
price_base = max(float(candle.OpenPrice), 1.0)
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
range_val = max(float(candle.HighPrice) - float(candle.LowPrice), price_step)
close_location = ((float(candle.ClosePrice) - float(candle.LowPrice)) - (float(candle.HighPrice) - float(candle.ClosePrice))) / range_val
compression = 1.0 - min(0.2, range_val / price_base)
return (close_location * 2.0) + compression
def CreateClone(self):
return earnings_announcements_with_buybacks_strategy()