Стратегия Stochastic Chaikin's Volatility
Общее описание
Эта стратегия — перенос на StockSharp советника MetaTrader Exp_Stochastic_Chaikins_Volatility. Алгоритм измеряет разброс между максимумом и минимумом свечи, сглаживает его выбранным типом скользящей средней, после чего нормализует результат подобно стохастическому осциллятору. Торговая логика соответствует оригиналу и носит контртрендовый характер: входы выполняются в момент разворота осциллятора, а закрытия — при восстановлении импульса в противоположную сторону.
Формирование индикатора
- Chaikin-подобная волатильность — разница
High-Lowсглаживается первой скользящей средней. Доступные методы сглаживания: SMA, EMA, SMMA (Wilder), LWMA и Jurik. - Стохастическая нормализация — из последних
Stochastic Lengthсглаженных значений берутся максимум и минимум, относительно которых рассчитывается текущая позиция в диапазоне 0–100. - Вторичное сглаживание — коэффициент из п.2 дополнительно фильтруется второй скользящей средней (из того же списка методов), что даёт основную линию осциллятора. Сигнальная линия равна значению основной линии на предыдущей закрытой свече, как в исходном индикаторе.
Правила торговли
- Входы
- Покупка: когда осциллятор сформировал вершину и последняя закрытая свеча пересекла предыдущий максимум сверху вниз.
- Продажа: когда осциллятор сформировал впадину и последняя свеча пересекла предыдущий минимум снизу вверх.
- Выходы
- Длинные позиции закрываются, если предыдущее значение осциллятора стало ниже ещё более раннего (импульс вниз).
- Короткие позиции закрываются, если предыдущее значение стало выше ещё более раннего (импульс вверх).
- Параметр
Signal Shiftзадаёт, какую по счёту закрытую свечу анализировать. Значение 1 полностью повторяет логику MQL.
Параметры
| Имя | Описание |
|---|---|
Candle Type |
Таймфрейм свечей для расчётов (по умолчанию 4 часа). |
Primary Method / Primary Length |
Тип и период первичной скользящей для сглаживания диапазона High-Low. |
Secondary Method / Secondary Length |
Тип и период вторичного сглаживания нормализованного осциллятора. |
Stochastic Length |
Размер окна для поиска минимума и максимума при нормализации. |
Signal Shift |
Сдвиг по закрытым свечам для расчёта сигналов (минимум 1). |
Allow Long/Short Entry |
Разрешение на открытие длинных / коротких позиций. |
Allow Long/Short Exit |
Разрешение на закрытие позиций при развороте осциллятора. |
High/Middle/Low Level |
Визуальные уровни из оригинального индикатора (на торговлю не влияют). |
Особенности использования
- В StockSharp используются стандартные индикаторы. Экзотические методы сглаживания из MQL (ParMA, VIDYA, AMA и т.д.) заменяются ближайшими доступными аналогами; для более плавного поведения выбирайте вариант Jurik.
- Размер позиции определяется параметром
Volumeбазового класса. Системы стоп-лосса и тейк-профита из MQL-библиотеки не переносятся — выходы выполняются по сигналам осциллятора либо через внешнее управление риском (StartProtection). - Расчёты ведутся только по завершённым свечам. Убедитесь, что поставщик данных отдаёт выбранный таймфрейм с достаточной историей для прогрева обоих сглаживаний и стохастического окна.
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>
/// Strategy converted from the "Stochastic Chaikin's Volatility" MQL expert advisor.
/// Combines a smoothed Chaikin volatility measure with a stochastic oscillator style normalization.
/// </summary>
public class StochasticChaikinsVolatilityStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<SmoothMethods> _primaryMethod;
private readonly StrategyParam<int> _primaryLength;
private readonly StrategyParam<SmoothMethods> _secondaryMethod;
private readonly StrategyParam<int> _secondaryLength;
private readonly StrategyParam<int> _stochasticLength;
private readonly StrategyParam<int> _signalShift;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<decimal> _highLevel;
private readonly StrategyParam<decimal> _middleLevel;
private readonly StrategyParam<decimal> _lowLevel;
private DecimalLengthIndicator _primarySmoother = null!;
private DecimalLengthIndicator _secondarySmoother = null!;
private readonly List<decimal> _volatilityWindow = new();
private readonly List<decimal> _mainHistory = new();
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Smoothing method applied to the high-low spread.
/// </summary>
public SmoothMethods PrimaryMethod
{
get => _primaryMethod.Value;
set => _primaryMethod.Value = value;
}
/// <summary>
/// Length of the primary smoothing moving average.
/// </summary>
public int PrimaryLength
{
get => _primaryLength.Value;
set => _primaryLength.Value = value;
}
/// <summary>
/// Smoothing method applied to the stochastic ratio.
/// </summary>
public SmoothMethods SecondaryMethod
{
get => _secondaryMethod.Value;
set => _secondaryMethod.Value = value;
}
/// <summary>
/// Length of the secondary smoothing moving average.
/// </summary>
public int SecondaryLength
{
get => _secondaryLength.Value;
set => _secondaryLength.Value = value;
}
/// <summary>
/// Lookback for calculating the stochastic style normalization.
/// </summary>
public int StochasticLength
{
get => _stochasticLength.Value;
set => _stochasticLength.Value = value;
}
/// <summary>
/// Number of completed candles used as signal shift.
/// </summary>
public int SignalShift
{
get => _signalShift.Value;
set => _signalShift.Value = value;
}
/// <summary>
/// Enable opening of long positions.
/// </summary>
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
/// <summary>
/// Enable opening of short positions.
/// </summary>
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
/// <summary>
/// Enable closing of long positions on indicator reversal.
/// </summary>
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
/// <summary>
/// Enable closing of short positions on indicator reversal.
/// </summary>
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
/// <summary>
/// Upper visual level for the oscillator.
/// </summary>
public decimal HighLevel
{
get => _highLevel.Value;
set => _highLevel.Value = value;
}
/// <summary>
/// Middle visual level for the oscillator.
/// </summary>
public decimal MiddleLevel
{
get => _middleLevel.Value;
set => _middleLevel.Value = value;
}
/// <summary>
/// Lower visual level for the oscillator.
/// </summary>
public decimal LowLevel
{
get => _lowLevel.Value;
set => _lowLevel.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="StochasticChaikinsVolatilityStrategy"/> class.
/// </summary>
public StochasticChaikinsVolatilityStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for indicator calculations", "General");
_primaryMethod = Param(nameof(PrimaryMethod), SmoothMethods.Sma)
.SetDisplay("Primary Method", "Smoothing applied to high-low spread", "Indicator")
;
_primaryLength = Param(nameof(PrimaryLength), 20)
.SetGreaterThanZero()
.SetDisplay("Primary Length", "Periods for primary smoothing", "Indicator")
;
_secondaryMethod = Param(nameof(SecondaryMethod), SmoothMethods.Jurik)
.SetDisplay("Secondary Method", "Smoothing applied to stochastic ratio", "Indicator")
;
_secondaryLength = Param(nameof(SecondaryLength), 10)
.SetGreaterThanZero()
.SetDisplay("Secondary Length", "Periods for secondary smoothing", "Indicator")
;
_stochasticLength = Param(nameof(StochasticLength), 14)
.SetGreaterThanZero()
.SetDisplay("Stochastic Length", "Lookback for highest-lowest range", "Indicator")
;
_signalShift = Param(nameof(SignalShift), 1)
.SetGreaterThanZero()
.SetDisplay("Signal Shift", "Completed candles offset for signals", "Trading")
;
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Allow Long Entry", "Enable opening of buy trades", "Trading");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Allow Short Entry", "Enable opening of sell trades", "Trading");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Allow Long Exit", "Enable closing longs on reversal", "Trading");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Allow Short Exit", "Enable closing shorts on reversal", "Trading");
_highLevel = Param(nameof(HighLevel), 70m)
.SetDisplay("High Level", "Upper visual threshold", "Visualization");
_middleLevel = Param(nameof(MiddleLevel), 50m)
.SetDisplay("Middle Level", "Middle visual threshold", "Visualization");
_lowLevel = Param(nameof(LowLevel), 30m)
.SetDisplay("Low Level", "Lower visual threshold", "Visualization");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_primarySmoother = null!;
_secondarySmoother = null!;
_volatilityWindow.Clear();
_mainHistory.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_primarySmoother = CreateSmoother(PrimaryMethod, PrimaryLength);
_secondarySmoother = CreateSmoother(SecondaryMethod, SecondaryLength);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var diff = candle.HighPrice - candle.LowPrice;
var smoothedValue = _primarySmoother.Process(diff, candle.OpenTime, true);
if (!smoothedValue.IsFormed)
return;
var smoothedDiff = smoothedValue.ToDecimal();
UpdateQueue(_volatilityWindow, smoothedDiff, StochasticLength);
if (_volatilityWindow.Count < StochasticLength)
return;
decimal highest = decimal.MinValue;
decimal lowest = decimal.MaxValue;
var count = _volatilityWindow.Count;
for (var vi = 0; vi < count; vi++)
{
var value = _volatilityWindow[vi];
if (value > highest)
highest = value;
if (value < lowest)
lowest = value;
}
var priceStep = Security?.PriceStep ?? 0.0001m;
if (priceStep <= 0m)
priceStep = 0.0001m;
var range = highest - lowest;
var denominator = range < priceStep ? priceStep : range;
var normalized = denominator == 0m ? 0m : (smoothedDiff - lowest) / denominator;
if (normalized < 0m)
normalized = 0m;
else if (normalized > 1m)
normalized = 1m;
var scaled = normalized * 100m;
var stochasticValue = _secondarySmoother.Process(scaled, candle.OpenTime, true);
if (!stochasticValue.IsFormed)
return;
var main = stochasticValue.ToDecimal();
AddHistory(main);
var minHistory = SignalShift + 3;
if (_mainHistory.Count < minHistory)
return;
var idx = SignalShift;
var value0 = _mainHistory[idx];
var value1 = _mainHistory[idx + 1];
var value2 = _mainHistory[idx + 2];
var buyClose = AllowLongExit && value1 > HighLevel && value1 < value2;
var sellClose = AllowShortExit && value1 < LowLevel && value1 > value2;
var buyOpen = AllowLongEntry && value1 < LowLevel && value1 > value2 && value0 <= value1;
var sellOpen = AllowShortEntry && value1 > HighLevel && value1 < value2 && value0 >= value1;
// proceed with trading logic
if (Position > 0m && buyClose)
{
SellMarket();
}
else if (Position < 0m && sellClose)
{
BuyMarket();
}
if (buyOpen && Position <= 0m)
{
BuyMarket();
}
else if (sellOpen && Position >= 0m)
{
SellMarket();
}
}
private void AddHistory(decimal value)
{
_mainHistory.Insert(0, value);
var maxSize = SignalShift + 4;
while (_mainHistory.Count > maxSize)
_mainHistory.RemoveAt(_mainHistory.Count - 1);
}
private static void UpdateQueue(List<decimal> queue, decimal value, int length)
{
queue.Add(value);
while (queue.Count > length)
queue.RemoveAt(0);
}
private static DecimalLengthIndicator CreateSmoother(SmoothMethods method, int length)
{
return method switch
{
SmoothMethods.Sma => new SimpleMovingAverage { Length = length },
SmoothMethods.Ema => new ExponentialMovingAverage { Length = length },
SmoothMethods.Smma => new SmoothedMovingAverage { Length = length },
SmoothMethods.Lwma => new WeightedMovingAverage { Length = length },
SmoothMethods.Jurik => new JurikMovingAverage { Length = length },
_ => new SMA { Length = length },
};
}
/// <summary>
/// Available smoothing methods supported by the strategy.
/// </summary>
public enum SmoothMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Sma,
/// <summary>
/// Exponential moving average.
/// </summary>
Ema,
/// <summary>
/// Smoothed moving average (RMA/SMMA).
/// </summary>
Smma,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Lwma,
/// <summary>
/// Jurik moving average approximation.
/// </summary>
Jurik
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal, Array
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, ExponentialMovingAverage, SmoothedMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
try:
from StockSharp.Algo.Indicators import JurikMovingAverage
_has_jurik = True
except:
_has_jurik = False
SMOOTH_SMA = 0
SMOOTH_EMA = 1
SMOOTH_SMMA = 2
SMOOTH_LWMA = 3
SMOOTH_JURIK = 4
class stochastic_chaikins_volatility_strategy(Strategy):
def __init__(self):
super(stochastic_chaikins_volatility_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._primary_method = self.Param("PrimaryMethod", SMOOTH_SMA)
self._primary_length = self.Param("PrimaryLength", 20)
self._secondary_method = self.Param("SecondaryMethod", SMOOTH_JURIK)
self._secondary_length = self.Param("SecondaryLength", 10)
self._stochastic_length = self.Param("StochasticLength", 14)
self._signal_shift = self.Param("SignalShift", 1)
self._allow_long_entry = self.Param("AllowLongEntry", True)
self._allow_short_entry = self.Param("AllowShortEntry", True)
self._allow_long_exit = self.Param("AllowLongExit", True)
self._allow_short_exit = self.Param("AllowShortExit", True)
self._high_level = self.Param("HighLevel", Decimal(70))
self._middle_level = self.Param("MiddleLevel", Decimal(50))
self._low_level = self.Param("LowLevel", Decimal(30))
self._volatility_window = []
self._main_history = []
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def PrimaryMethod(self):
return self._primary_method.Value
@PrimaryMethod.setter
def PrimaryMethod(self, value):
self._primary_method.Value = value
@property
def PrimaryLength(self):
return self._primary_length.Value
@PrimaryLength.setter
def PrimaryLength(self, value):
self._primary_length.Value = value
@property
def SecondaryMethod(self):
return self._secondary_method.Value
@SecondaryMethod.setter
def SecondaryMethod(self, value):
self._secondary_method.Value = value
@property
def SecondaryLength(self):
return self._secondary_length.Value
@SecondaryLength.setter
def SecondaryLength(self, value):
self._secondary_length.Value = value
@property
def StochasticLength(self):
return self._stochastic_length.Value
@StochasticLength.setter
def StochasticLength(self, value):
self._stochastic_length.Value = value
@property
def SignalShift(self):
return self._signal_shift.Value
@SignalShift.setter
def SignalShift(self, value):
self._signal_shift.Value = value
@property
def AllowLongEntry(self):
return self._allow_long_entry.Value
@AllowLongEntry.setter
def AllowLongEntry(self, value):
self._allow_long_entry.Value = value
@property
def AllowShortEntry(self):
return self._allow_short_entry.Value
@AllowShortEntry.setter
def AllowShortEntry(self, value):
self._allow_short_entry.Value = value
@property
def AllowLongExit(self):
return self._allow_long_exit.Value
@AllowLongExit.setter
def AllowLongExit(self, value):
self._allow_long_exit.Value = value
@property
def AllowShortExit(self):
return self._allow_short_exit.Value
@AllowShortExit.setter
def AllowShortExit(self, value):
self._allow_short_exit.Value = value
@property
def HighLevel(self):
return self._high_level.Value
@HighLevel.setter
def HighLevel(self, value):
self._high_level.Value = value
@property
def MiddleLevel(self):
return self._middle_level.Value
@MiddleLevel.setter
def MiddleLevel(self, value):
self._middle_level.Value = value
@property
def LowLevel(self):
return self._low_level.Value
@LowLevel.setter
def LowLevel(self, value):
self._low_level.Value = value
def _create_smoother(self, method, length):
m = int(method)
if m == SMOOTH_EMA:
ind = ExponentialMovingAverage()
elif m == SMOOTH_SMMA:
ind = SmoothedMovingAverage()
elif m == SMOOTH_LWMA:
ind = WeightedMovingAverage()
elif m == SMOOTH_JURIK:
if _has_jurik:
ind = JurikMovingAverage()
else:
ind = ExponentialMovingAverage()
else:
ind = SimpleMovingAverage()
ind.Length = length
return ind
def OnStarted2(self, time):
super(stochastic_chaikins_volatility_strategy, self).OnStarted2(time)
self._primary_smoother = self._create_smoother(self.PrimaryMethod, int(self.PrimaryLength))
self._secondary_smoother = self._create_smoother(self.SecondaryMethod, int(self.SecondaryLength))
self._volatility_window = []
self._main_history = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
diff = Decimal.Subtract(candle.HighPrice, candle.LowPrice)
input_val = self._primary_smoother.CreateValue(candle.OpenTime, Array[object]([diff]))
input_val.IsFinal = True
smoothed_result = self._primary_smoother.Process(input_val)
if not smoothed_result.IsFormed:
return
smoothed_diff = float(smoothed_result)
stoch_len = int(self.StochasticLength)
self._volatility_window.append(smoothed_diff)
while len(self._volatility_window) > stoch_len:
self._volatility_window.pop(0)
if len(self._volatility_window) < stoch_len:
return
highest = max(self._volatility_window)
lowest = min(self._volatility_window)
price_step = 0.0001
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0.0:
price_step = ps
range_val = highest - lowest
denominator = range_val if range_val >= price_step else price_step
if denominator == 0.0:
normalized = 0.0
else:
normalized = (smoothed_diff - lowest) / denominator
if normalized < 0.0:
normalized = 0.0
elif normalized > 1.0:
normalized = 1.0
scaled = normalized * 100.0
stoch_input = self._secondary_smoother.CreateValue(candle.OpenTime, Array[object]([Decimal(scaled)]))
stoch_input.IsFinal = True
stoch_result = self._secondary_smoother.Process(stoch_input)
if not stoch_result.IsFormed:
return
main = float(stoch_result)
self._add_history(main)
signal_shift = int(self.SignalShift)
min_history = signal_shift + 3
if len(self._main_history) < min_history:
return
idx = signal_shift
value0 = self._main_history[idx]
value1 = self._main_history[idx + 1]
value2 = self._main_history[idx + 2]
high_lvl = float(self.HighLevel)
low_lvl = float(self.LowLevel)
buy_close = self.AllowLongExit and value1 > high_lvl and value1 < value2
sell_close = self.AllowShortExit and value1 < low_lvl and value1 > value2
buy_open = self.AllowLongEntry and value1 < low_lvl and value1 > value2 and value0 <= value1
sell_open = self.AllowShortEntry and value1 > high_lvl and value1 < value2 and value0 >= value1
if self.Position > 0 and buy_close:
self.SellMarket()
elif self.Position < 0 and sell_close:
self.BuyMarket()
if buy_open and self.Position <= 0:
self.BuyMarket()
elif sell_open and self.Position >= 0:
self.SellMarket()
def _add_history(self, value):
self._main_history.insert(0, value)
max_size = int(self.SignalShift) + 4
while len(self._main_history) > max_size:
self._main_history.pop()
def OnReseted(self):
super(stochastic_chaikins_volatility_strategy, self).OnReseted()
self._volatility_window = []
self._main_history = []
def CreateClone(self):
return stochastic_chaikins_volatility_strategy()