VoVix DEVMA Strategy
This strategy analyzes volatility behaviour using Deviation Moving Averages (DEVMA) built on the standard deviation of ATR. It trades transitions between contraction and expansion regimes and uses ATR-based exits.
Details
- Entry Criteria:
- Long: Fast DEVMA crosses above Slow DEVMA.
- Short: Fast DEVMA crosses below Slow DEVMA.
- Long/Short: Both.
- Exit Criteria:
- ATR stop-loss and take-profit.
- Stops: Yes, ATR multiples.
- Default Values:
DeviationLookback= 59FastLength= 20SlowLength= 60ATR SL Mult= 2ATR TP Mult= 3
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Multiple
- Stops: Yes
- Complexity: Complex
- Timeframe: Medium-term
- 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>
/// VoVix DEVMA strategy.
/// Uses fast/slow StdDev deviation crossover as volatility regime shift signal.
/// Enters on deviation crossover, exits on percent TP/SL.
/// </summary>
public class VoVixDevmaStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<decimal> _stopPct;
private readonly StrategyParam<decimal> _tpMult;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private static readonly object _sync = new();
private decimal _prevFastStd;
private decimal _prevSlowStd;
private decimal _entryPrice;
private decimal _stopDist;
private int _cooldownRemaining;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public decimal StopPct { get => _stopPct.Value; set => _stopPct.Value = value; }
public decimal TpMult { get => _tpMult.Value; set => _tpMult.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public VoVixDevmaStrategy()
{
_fastLength = Param(nameof(FastLength), 10)
.SetGreaterThanZero()
.SetDisplay("Fast Length", "Fast StdDev period", "DEVMA");
_slowLength = Param(nameof(SlowLength), 20)
.SetGreaterThanZero()
.SetDisplay("Slow Length", "Slow StdDev period", "DEVMA");
_stopPct = Param(nameof(StopPct), 1m)
.SetGreaterThanZero()
.SetDisplay("Stop %", "Stop loss percent", "Risk");
_tpMult = Param(nameof(TpMult), 2m)
.SetGreaterThanZero()
.SetDisplay("TP Mult", "Take profit as multiple of stop", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 3)
.SetNotNegative()
.SetDisplay("Signal Cooldown", "Closed candles to wait before a new entry", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_prevFastStd = 0;
_prevSlowStd = 0;
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastStd = new StandardDeviation { Length = FastLength };
var slowStd = new StandardDeviation { Length = SlowLength };
var ema = new ExponentialMovingAverage { Length = FastLength };
_prevFastStd = 0;
_prevSlowStd = 0;
_entryPrice = 0;
_stopDist = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(candle => ProcessCandle(candle, fastStd, slowStd, ema)).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, StandardDeviation fastStd, StandardDeviation slowStd, ExponentialMovingAverage ema)
{
if (candle.State != CandleStates.Finished)
return;
lock (_sync)
{
var time = candle.OpenTime;
var priceValue = new DecimalIndicatorValue(fastStd, candle.ClosePrice, time) { IsFinal = true };
var fastStdValue = fastStd.Process(priceValue);
var slowStdValue = slowStd.Process(new DecimalIndicatorValue(slowStd, candle.ClosePrice, time) { IsFinal = true });
var emaValue = ema.Process(new DecimalIndicatorValue(ema, candle.ClosePrice, time) { IsFinal = true });
if (!fastStdValue.IsFinal || !slowStdValue.IsFinal || !emaValue.IsFinal || !fastStd.IsFormed || !slowStd.IsFormed || !ema.IsFormed)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var fastStdVal = fastStdValue.ToDecimal();
var slowStdVal = slowStdValue.ToDecimal();
var emaVal = emaValue.ToDecimal();
if (Position > 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice <= _entryPrice - _stopDist || candle.ClosePrice >= _entryPrice + _stopDist * TpMult)
{
SellMarket(Position);
_entryPrice = 0m;
_stopDist = 0m;
_cooldownRemaining = SignalCooldownBars;
}
}
else if (Position < 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice >= _entryPrice + _stopDist || candle.ClosePrice <= _entryPrice - _stopDist * TpMult)
{
BuyMarket(-Position);
_entryPrice = 0m;
_stopDist = 0m;
_cooldownRemaining = SignalCooldownBars;
}
}
if (_prevFastStd == 0m || _prevSlowStd == 0m || fastStdVal <= 0m || slowStdVal <= 0m)
{
_prevFastStd = fastStdVal;
_prevSlowStd = slowStdVal;
return;
}
var volExpanding = fastStdVal > slowStdVal;
var wasContracting = _prevFastStd <= _prevSlowStd;
var bullCross = _cooldownRemaining == 0 && wasContracting && volExpanding && candle.ClosePrice > emaVal;
var bearCross = _cooldownRemaining == 0 && wasContracting && volExpanding && candle.ClosePrice < emaVal;
if (bullCross && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
else if (bearCross && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
_prevFastStd = fastStdVal;
_prevSlowStd = slowStdVal;
}
}
}
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
from StockSharp.Algo.Indicators import ExponentialMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class vo_vix_devma_strategy(Strategy):
def __init__(self):
super(vo_vix_devma_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 10) \
.SetDisplay("Fast Length", "Fast StdDev period", "DEVMA")
self._slow_length = self.Param("SlowLength", 20) \
.SetDisplay("Slow Length", "Slow StdDev period", "DEVMA")
self._stop_pct = self.Param("StopPct", 1.0) \
.SetDisplay("Stop %", "Stop loss percent", "Risk")
self._tp_mult = self.Param("TpMult", 2.0) \
.SetDisplay("TP Mult", "Take profit as multiple of stop", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 3) \
.SetDisplay("Signal Cooldown", "Closed candles to wait before a new entry", "General")
self._prev_fast_std = 0.0
self._prev_slow_std = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._cooldown_remaining = 0
self._fast_std = None
self._slow_std = None
self._ema = None
@property
def fast_length(self):
return self._fast_length.Value
@property
def slow_length(self):
return self._slow_length.Value
@property
def stop_pct(self):
return self._stop_pct.Value
@property
def tp_mult(self):
return self._tp_mult.Value
@property
def candle_type(self):
return self._candle_type.Value
@property
def signal_cooldown_bars(self):
return self._signal_cooldown_bars.Value
def OnReseted(self):
super(vo_vix_devma_strategy, self).OnReseted()
self._prev_fast_std = 0.0
self._prev_slow_std = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(vo_vix_devma_strategy, self).OnStarted2(time)
self._fast_std = StandardDeviation()
self._fast_std.Length = self.fast_length
self._slow_std = StandardDeviation()
self._slow_std.Length = self.slow_length
self._ema = ExponentialMovingAverage()
self._ema.Length = self.fast_length
self._prev_fast_std = 0.0
self._prev_slow_std = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._cooldown_remaining = 0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ema)
self.DrawOwnTrades(area)
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.OpenTime
fast_std_value = process_float(self._fast_std, candle.ClosePrice, t, True)
slow_std_value = process_float(self._slow_std, candle.ClosePrice, t, True)
ema_value = process_float(self._ema, candle.ClosePrice, t, True)
if not fast_std_value.IsFinal or not slow_std_value.IsFinal or not ema_value.IsFinal:
return
if not self._fast_std.IsFormed or not self._slow_std.IsFormed or not self._ema.IsFormed:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
fast_std_val = float(fast_std_value)
slow_std_val = float(slow_std_value)
ema_val = float(ema_value)
close = float(candle.ClosePrice)
if self.Position > 0 and self._entry_price > 0 and self._stop_dist > 0:
if close <= self._entry_price - self._stop_dist or close >= self._entry_price + self._stop_dist * float(self.tp_mult):
self.SellMarket(self.Position)
self._entry_price = 0.0
self._stop_dist = 0.0
self._cooldown_remaining = self.signal_cooldown_bars
elif self.Position < 0 and self._entry_price > 0 and self._stop_dist > 0:
if close >= self._entry_price + self._stop_dist or close <= self._entry_price - self._stop_dist * float(self.tp_mult):
self.BuyMarket(-self.Position)
self._entry_price = 0.0
self._stop_dist = 0.0
self._cooldown_remaining = self.signal_cooldown_bars
if self._prev_fast_std == 0 or self._prev_slow_std == 0 or fast_std_val <= 0 or slow_std_val <= 0:
self._prev_fast_std = fast_std_val
self._prev_slow_std = slow_std_val
return
vol_expanding = fast_std_val > slow_std_val
was_contracting = self._prev_fast_std <= self._prev_slow_std
bull_cross = self._cooldown_remaining == 0 and was_contracting and vol_expanding and close > ema_val
bear_cross = self._cooldown_remaining == 0 and was_contracting and vol_expanding and close < ema_val
if bull_cross and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self._entry_price = close
self._stop_dist = close * float(self.stop_pct) / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
elif bear_cross and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._entry_price = close
self._stop_dist = close * float(self.stop_pct) / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
self._prev_fast_std = fast_std_val
self._prev_slow_std = slow_std_val
def CreateClone(self):
return vo_vix_devma_strategy()