Estratégia Implied Volatility Spike
Esta estratégia monitora a volatilidade implícita em busca de saltos repentinos em relação ao valor anterior. Um forte spike combinado com o preço operando contra a média móvel pode sinalizar uma reversão de curto prazo.
Os testes indicam um retorno anual médio de aproximadamente 163%. Funciona melhor no mercado de ações.
Quando a volatilidade implícita aumenta acima do limiar configurado, o sistema entra na direção oposta ao movimento do preço, esperando que a volatilidade reverta.
As posições são fechadas assim que a volatilidade começa a cair ou ocorre um stop-loss.
Detalhes
- Critérios de entrada: IV spike acima de
IVSpikeThresholde preço relativo à MA. - Comprado/Vendido: Ambas as direções.
- Critérios de saída: IV diminui ou stop.
- Stops: Sim.
- Valores padrão:
MAPeriod= 20IVPeriod= 20IVSpikeThreshold= 1.5mCandleType= TimeSpan.FromMinutes(5)
- Filtros:
- Categoria: Volatilidade
- Direção: Ambos
- Indicadores: IV, MA
- Stops: Sim
- Complexidade: Intermediário
- Período: Intradiário
- Sazonalidade: Não
- Redes neurais: Não
- Divergência: Não
- Nível de risco: Médio
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Implied volatility spike strategy.
/// The implied volatility readings are the candle closes of a separate instrument on the same timeframe.
/// A spike is a rise to at least <see cref="IVSpikeThreshold"/> times the previous reading that also leaves the reading
/// above the average of the last <see cref="IVPeriod"/> readings. On a spike the strategy enters against the price move:
/// long when the close is below the moving average, short when it is above.
/// The position is closed when a reading falls below the previous one or the stop-loss is hit.
/// </summary>
public class IvSpikeStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _ivPeriod;
private readonly StrategyParam<decimal> _ivSpikeThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<Security> _ivSecurity;
private decimal? _previousIv;
private DateTime? _ivBarTime;
private bool _hasIvChange;
private bool _isIvSpike;
private bool _isIvDecline;
private DateTime? _mainBarTime;
private decimal _mainPrice;
private decimal _mainAverage;
private DateTime? _processedPairTime;
private Order _pendingOrder;
/// <summary>
/// Period of the moving average the close is compared with (default: 20).
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Number of implied volatility readings in the average a spike reading must exceed (default: 20).
/// </summary>
public int IVPeriod
{
get => _ivPeriod.Value;
set => _ivPeriod.Value = value;
}
/// <summary>
/// Multiple of the previous implied volatility reading that a rise must reach to count as a spike (default: 1.5).
/// </summary>
public decimal IVSpikeThreshold
{
get => _ivSpikeThreshold.Value;
set => _ivSpikeThreshold.Value = value;
}
/// <summary>
/// Stop-loss as a percentage from the entry price (default: 2%). Zero disables it.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Type of candles used for both the traded instrument and the implied volatility instrument.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Instrument whose candle closes are the implied volatility readings (required).
/// </summary>
public Security IVSecurity
{
get => _ivSecurity.Value;
set => _ivSecurity.Value = value;
}
/// <summary>
/// Initialize the IV Spike strategy.
/// </summary>
public IvSpikeStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period of the moving average the close is compared with", "Indicators")
.SetOptimize(10, 50, 10);
_ivPeriod = Param(nameof(IVPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("IV Period", "Implied volatility readings in the average a spike reading must exceed", "Indicators")
.SetOptimize(10, 30, 5);
_ivSpikeThreshold = Param(nameof(IVSpikeThreshold), 1.5m)
.SetGreaterThanZero()
.SetDisplay("IV Spike Threshold", "Multiple of the previous implied volatility reading a spike must reach", "Entry")
.SetOptimize(1.2m, 2.0m, 0.1m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss as percentage from entry price", "Risk Management")
.SetOptimize(1m, 5m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for the traded and the implied volatility instruments", "General");
_ivSecurity = Param<Security>(nameof(IVSecurity))
.SetDisplay("IV Security", "Instrument whose candle closes are the implied volatility readings", "Data")
.SetRequired();
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (IVSecurity, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ClearSignalState();
}
private void ClearSignalState()
{
_previousIv = null;
_ivBarTime = null;
_hasIvChange = false;
_isIvSpike = false;
_isIvDecline = false;
_mainBarTime = null;
_mainPrice = 0m;
_mainAverage = 0m;
_processedPairTime = null;
_pendingOrder = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
// Reject before any startup side effects: SubscribeCandles(null) falls back to the traded instrument.
if (IVSecurity is null || Security is null || string.Equals(IVSecurity.Id, Security.Id, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("IVSecurity must explicitly identify a different external instrument.");
base.OnStarted2(time);
ClearSignalState();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// Bid and ask updates let the native stop react between finished candles.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var priceAverage = new SimpleMovingAverage { Length = MAPeriod };
var ivAverage = new SimpleMovingAverage { Length = IVPeriod };
var mainSubscription = SubscribeCandles(CandleType);
var ivSubscription = SubscribeCandles(CandleType, security: IVSecurity);
mainSubscription
.BindEx(priceAverage, ProcessMainCandle)
.Start();
ivSubscription
.BindEx(ivAverage, ProcessIvCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, mainSubscription);
DrawIndicator(area, priceAverage);
DrawOwnTrades(area);
var ivArea = CreateChartArea();
DrawCandles(ivArea, ivSubscription);
DrawIndicator(ivArea, ivAverage);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between signal pairs.
}
private void ProcessIvCandle(ICandleMessage candle, IIndicatorValue average)
{
if (candle.State != CandleStates.Finished || _ivBarTime is DateTime previousTime && candle.OpenTime <= previousTime)
return;
var reading = candle.ClosePrice;
_ivBarTime = candle.OpenTime;
_hasIvChange = _previousIv.HasValue;
_isIvDecline = _previousIv is decimal previous && reading < previous;
// The jump is measured from the previous reading; the average only confirms the level is raised.
_isIvSpike = _previousIv is decimal prior && prior > 0m && reading > prior && reading >= IVSpikeThreshold * prior
&& average.Indicator.IsFormed && reading > average.GetValue<decimal>();
_previousIv = reading;
ProcessMatchedPair();
}
private void ProcessMainCandle(ICandleMessage candle, IIndicatorValue average)
{
if (candle.State != CandleStates.Finished || !average.Indicator.IsFormed
|| _mainBarTime is DateTime previousTime && candle.OpenTime <= previousTime)
return;
_mainBarTime = candle.OpenTime;
_mainPrice = candle.ClosePrice;
_mainAverage = average.GetValue<decimal>();
ProcessMatchedPair();
}
private void ProcessMatchedPair()
{
// Act once per bar time and only when both streams finished it; an unmatched bar is never reused.
if (!_hasIvChange || _mainBarTime is not DateTime time || _ivBarTime != time || _processedPairTime == time)
return;
_processedPairTime = time;
if (!IsFormedAndOnlineAndAllowTrading()
|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position != 0m)
{
if (!_isIvDecline)
return;
if (Position > 0m)
SellMarket(Position);
else
BuyMarket(Math.Abs(Position));
}
else if (_isIvSpike)
{
// A close exactly on the average shows no price move to fade.
if (_mainPrice < _mainAverage)
BuyMarket(Volume);
else if (_mainPrice > _mainAverage)
SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal, InvalidOperationException
from StockSharp.Messages import UnitTypes, Unit, DataType, CandleStates, OrderStates, Level1Fields
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security, Subscription
class iv_spike_strategy(Strategy):
"""
Implied volatility spike strategy.
The implied volatility readings are the candle closes of a separate instrument on the same timeframe.
A spike is a rise to at least IVSpikeThreshold times the previous reading that also leaves the reading
above the average of the last IVPeriod readings. On a spike the strategy enters against the price move:
long when the close is below the moving average, short when it is above.
The position is closed when a reading falls below the previous one or the stop-loss is hit.
"""
def __init__(self):
super(iv_spike_strategy, self).__init__()
self._clear_signal_state()
self.OrderRegistering += self._track_pending
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero() \
.SetDisplay("MA Period", "Period of the moving average the close is compared with", "Indicators")
self._iv_period = self.Param("IVPeriod", 20).SetGreaterThanZero() \
.SetDisplay("IV Period", "Implied volatility readings in the average a spike reading must exceed", "Indicators")
self._iv_spike_threshold = self.Param("IVSpikeThreshold", 1.5).SetGreaterThanZero() \
.SetDisplay("IV Spike Threshold", "Multiple of the previous implied volatility reading a spike must reach", "Entry")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop loss as percentage from entry price", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles for the traded and the implied volatility instruments", "General")
self._iv_security = self.Param[Security]("IVSecurity", None) \
.SetDisplay("IV Security", "Instrument whose candle closes are the implied volatility readings", "Data").SetRequired()
@property
def MAPeriod(self):
return self._ma_period.Value
@MAPeriod.setter
def MAPeriod(self, value):
self._ma_period.Value = value
@property
def IVPeriod(self):
return self._iv_period.Value
@IVPeriod.setter
def IVPeriod(self, value):
self._iv_period.Value = value
@property
def IVSpikeThreshold(self):
return self._iv_spike_threshold.Value
@IVSpikeThreshold.setter
def IVSpikeThreshold(self, value):
self._iv_spike_threshold.Value = value
@property
def StopLossPercent(self):
return self._stop_loss_percent.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stop_loss_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def IVSecurity(self):
return self._iv_security.Value
@IVSecurity.setter
def IVSecurity(self, value):
self._iv_security.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.IVSecurity, self.CandleType), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _clear_signal_state(self):
self._previous_iv = None
self._iv_time = None
self._has_iv_change = False
self._is_iv_spike = False
self._is_iv_decline = False
self._main_time = None
self._main_price = Decimal.Zero
self._main_average = Decimal.Zero
self._processed_pair_time = None
self._pending_order = None
def OnReseted(self):
super(iv_spike_strategy, self).OnReseted()
self._clear_signal_state()
def OnStarted2(self, time):
# Reject before startup side effects: a missing security silently selects the traded instrument.
if self.IVSecurity is None or self.Security is None or str(self.IVSecurity.Id).lower() == str(self.Security.Id).lower():
raise InvalidOperationException("IVSecurity must explicitly identify a different external instrument.")
super(iv_spike_strategy, self).OnStarted2(time)
self._clear_signal_state()
self.StartProtection(Unit(), Unit(Decimal(self.StopLossPercent), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# Bid and ask updates let the native stop react between finished candles.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
price_average = SimpleMovingAverage()
price_average.Length = self.MAPeriod
iv_average = SimpleMovingAverage()
iv_average.Length = self.IVPeriod
main_subscription = self.SubscribeCandles(self.CandleType)
# The second positional argument is isFinishedOnly, NOT the instrument.
iv_subscription = self.SubscribeCandles(self.CandleType, security=self.IVSecurity)
main_subscription.BindEx(price_average, self._process_main_candle, False).Start()
iv_subscription.BindEx(iv_average, self._process_iv_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, main_subscription)
self.DrawIndicator(area, price_average)
self.DrawOwnTrades(area)
iv_area = self.CreateChartArea()
self.DrawCandles(iv_area, iv_subscription)
self.DrawIndicator(iv_area, iv_average)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal pairs.
pass
def _process_iv_candle(self, candle, average):
if candle.State != CandleStates.Finished or (self._iv_time is not None and candle.OpenTime <= self._iv_time):
return
reading = candle.ClosePrice
previous = self._previous_iv
self._iv_time = candle.OpenTime
self._has_iv_change = previous is not None
self._is_iv_decline = previous is not None and reading < previous
# The jump is measured from the previous reading; the average only confirms the level is raised.
self._is_iv_spike = previous is not None and previous > Decimal.Zero and reading > previous \
and reading >= Decimal(self.IVSpikeThreshold) * previous \
and average.Indicator.IsFormed and reading > average.GetValue[Decimal](None)
self._previous_iv = reading
self._process_matched_pair()
def _process_main_candle(self, candle, average):
if candle.State != CandleStates.Finished or not average.Indicator.IsFormed or (self._main_time is not None and candle.OpenTime <= self._main_time):
return
self._main_time = candle.OpenTime
self._main_price = candle.ClosePrice
self._main_average = average.GetValue[Decimal](None)
self._process_matched_pair()
def _process_matched_pair(self):
# Act once per bar time and only when both streams finished it; an unmatched bar is never reused.
if not self._has_iv_change or self._main_time is None or self._iv_time != self._main_time or self._processed_pair_time == self._main_time:
return
self._processed_pair_time = self._main_time
if not self.IsFormedAndOnlineAndAllowTrading() or (self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position != 0:
if not self._is_iv_decline:
return
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(Math.Abs(self.Position))
elif self._is_iv_spike:
# A close exactly on the average shows no price move to fade.
if self._main_price < self._main_average:
self.BuyMarket(self.Volume)
elif self._main_price > self._main_average:
self.SellMarket(self.Volume)
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return iv_spike_strategy()