VIX Trigger 戦略
VIX Triggerはボラティリティ指数の変化に反応します。上昇するVIXは恐怖と原資産における潜在的なリバーサルを示します。この戦略はVIXの方向を移動平均に対する価格と比較します。
テストでは年間平均リターン約148%が示されています。外国為替市場で最も優れたパフォーマンスを発揮します。
VIXが上昇し価格が移動平均を下回ると、回復を期待して買いを入れます。逆に、価格が平均を上回っている状態でVIXが上昇すると、ショートポジションを取ります。
VIXが下落するかストップロスの割合に達するとポジションをクローズします。
詳細
- エントリー条件: VIXが上昇し、MAに対する価格がロングまたはショートを引き起こす。
- ロング/ショート: 両方向。
- エグジット条件: VIXが下落するかストップ。
- ストップ: はい。
- デフォルト値:
MAPeriod= 20StopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: Contrarian
- 方向: 両方
- インジケーター: VIX, MA
- ストップ: はい
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using Ecng.ComponentModel;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades based on VIX (Volatility Index) movements.
/// It enters positions when VIX is rising (indicating increasing fear/volatility in the market)
/// and price is moving in an expected direction relative to its moving average.
/// </summary>
public class VixTriggerStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<Security> _vixSecurity;
private decimal? _previousVix;
private DateTime? _vixBarTime;
private int _vixDirection;
private bool _hasVixDirection;
private DateTime? _mainBarTime;
private decimal _mainPrice;
private decimal _mainAverage;
private DateTime? _processedPairTime;
private Order _pendingOrder;
/// <summary>
/// Period for Moving Average calculation (default: 20)
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Stop-loss as percentage from entry price (default: 2%)
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// VIX Security (required)
/// </summary>
public Security VixSecurity
{
get => _vixSecurity.Value;
set => _vixSecurity.Value = value;
}
/// <summary>
/// Initialize the VIX Trigger strategy
/// </summary>
public VixTriggerStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Technical Parameters")
.SetOptimize(10, 50, 5);
_stopLossPercent = Param(nameof(StopLossPercent), 2.0m).SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss as percentage from entry price", "Risk Management")
.SetOptimize(1.0m, 5.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Data");
_vixSecurity = Param<Security>(nameof(VixSecurity))
.SetDisplay("VIX Security", "VIX Security to use for signals", "Data")
.SetRequired();
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (VixSecurity, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ClearSignalState();
}
private void ClearSignalState()
{
_previousVix = null;
_vixBarTime = null;
_vixDirection = 0;
_hasVixDirection = 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 primary.
if (VixSecurity is null || Security is null || string.Equals(VixSecurity.Id, Security.Id, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("VixSecurity must explicitly identify a different external instrument.");
base.OnStarted2(time);
ClearSignalState();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
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();
}
// Create indicator
var sma = new SimpleMovingAverage { Length = MAPeriod };
// Create subscriptions
var mainSubscription = SubscribeCandles(CandleType);
var vixSubscription = SubscribeCandles(CandleType, security: VixSecurity);
// Bind indicator to main security candles
mainSubscription
.BindEx(sma, ProcessMainCandle, false)
.Start();
// Process VIX candles separately
vixSubscription
.Bind(ProcessVixCandle)
.Start();
// Configure chart
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, mainSubscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between signal pairs.
}
private void ProcessVixCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _vixBarTime is DateTime previousTime && candle.OpenTime <= previousTime)
return;
_vixBarTime = candle.OpenTime;
_hasVixDirection = _previousVix.HasValue;
_vixDirection = _previousVix is decimal previous ? Math.Sign(candle.ClosePrice - previous) : 0;
_previousVix = candle.ClosePrice;
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()
{
// Keep only the newest bar from each source; never reuse stale unmatched bars.
if (!_hasVixDirection || _mainBarTime is not DateTime time || _vixBarTime != 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 && _vixDirection < 0)
{
if (Position > 0m) SellMarket(Position);
else BuyMarket(Math.Abs(Position));
}
else if (Position == 0m && _vixDirection > 0)
{
if (_mainPrice < _mainAverage) BuyMarket(Volume);
else if (_mainPrice > _mainAverage) SellMarket(Volume);
}
// Equal index closes are not falling, and equal price/MA is not a short signal.
}
}
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 vix_trigger_strategy(Strategy):
"""
Strategy that trades based on VIX (Volatility Index) movements.
It enters positions when VIX is rising (indicating increasing fear/volatility in the market)
and price is moving in an expected direction relative to its moving average.
"""
def __init__(self):
super(vix_trigger_strategy, self).__init__()
self._clear_signal_state()
self.OrderRegistering += self._track_pending
# Initialize strategy parameters
self._maPeriod = self.Param("MAPeriod", 20).SetGreaterThanZero() \
.SetDisplay("MA Period", "Period for Moving Average calculation", "Technical Parameters")
self._stopLossPercent = self.Param("StopLossPercent", 2.0).SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop loss as percentage from entry price", "Risk Management")
self._candleType = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "Data")
self._vixSecurity = self.Param[Security]("VixSecurity", None) \
.SetDisplay("VIX Security", "VIX Security to use for signals", "Data").SetRequired()
@property
def MAPeriod(self):
return self._maPeriod.Value
@MAPeriod.setter
def MAPeriod(self, value):
self._maPeriod.Value = value
@property
def StopLossPercent(self):
return self._stopLossPercent.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stopLossPercent.Value = value
@property
def CandleType(self):
return self._candleType.Value
@CandleType.setter
def CandleType(self, value):
self._candleType.Value = value
@property
def VixSecurity(self):
return self._vixSecurity.Value
@VixSecurity.setter
def VixSecurity(self, value):
self._vixSecurity.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.VixSecurity, self.CandleType), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _clear_signal_state(self):
self._previous_vix = None
self._vix_time = None
self._vix_direction = 0
self._has_vix_direction = 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(vix_trigger_strategy, self).OnReseted()
self._clear_signal_state()
def OnStarted2(self, time):
# Reject before startup side effects: a missing security silently selects the primary.
if self.VixSecurity is None or self.Security is None or str(self.VixSecurity.Id).lower() == str(self.Security.Id).lower():
raise InvalidOperationException("VixSecurity must explicitly identify a different external instrument.")
super(vix_trigger_strategy, self).OnStarted2(time)
self._clear_signal_state()
self.StartProtection(Unit(), Unit(Decimal(self.StopLossPercent), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
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()
sma = SimpleMovingAverage()
sma.Length = self.MAPeriod
main_subscription = self.SubscribeCandles(self.CandleType)
# The second positional argument is isFinishedOnly, NOT the instrument.
index_subscription = self.SubscribeCandles(self.CandleType, security=self.VixSecurity)
main_subscription.BindEx(sma, self._process_main_candle, False).Start()
index_subscription.Bind(self._process_vix_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, main_subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal pairs.
pass
def _process_vix_candle(self, candle):
if candle.State != CandleStates.Finished or (self._vix_time is not None and candle.OpenTime <= self._vix_time):
return
self._vix_time = candle.OpenTime
self._has_vix_direction = self._previous_vix is not None
self._vix_direction = Math.Sign(candle.ClosePrice - self._previous_vix) if self._previous_vix is not None else 0
self._previous_vix = candle.ClosePrice
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):
# Keep only each source's latest bar; stale unmatched bars are not reused.
if not self._has_vix_direction or self._main_time is None or self._vix_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 and self._vix_direction < 0:
if self.Position > 0:
self.SellMarket(self.Position)
else:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and self._vix_direction > 0:
if self._main_price < self._main_average:
self.BuyMarket(self.Volume)
elif self._main_price > self._main_average:
self.SellMarket(self.Volume)
# Equal index is not falling; equal price/MA is not a short signal.
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return vix_trigger_strategy()