Stochastic Chaikin's Volatility 策略
概述
本策略是 MetaTrader 指标专家 Exp_Stochastic_Chaikins_Volatility 的 StockSharp 版本。策略先计算每根 K 线高低价之间的区间,并通过可配置的移动平均线进行平滑处理,再将结果以类似随机指标 (Stochastic) 的方式标准化。交易逻辑保持原始 EA 的逆势思路:在振荡指标出现拐点时入场,振荡恢复原方向时出场。
指标构建
- Chaikin 式波动率:
High-Low差值通过“第一平滑”移动平均处理。可选方法包括 SMA、EMA、SMMA、LWMA 以及 Jurik。 - 随机标准化:最近
Stochastic Length个平滑值用于确定最高值与最低值,再将当前平滑值映射到 0–100 之间。 - 再次平滑:对标准化后的结果应用“第二平滑”移动平均,得到主振荡线;信号线等于上一根已完成 K 线的主振荡值,与 MQL 指标缓存完全一致。
交易规则
- 进场
- 做多:当振荡指标在上一根 K 线形成顶部且当前值向下穿越该顶部时触发逆势多单。
- 做空:当振荡指标在上一根 K 线形成底部且当前值向上穿越该底部时触发逆势空单。
- 离场
- 多头仓位在前一根 K 线的振荡值低于更早一次的值时平仓,表明下行动量恢复。
- 空头仓位在前一根 K 线的振荡值高于更早一次的值时平仓,表明上行动量恢复。
Signal Shift参数决定使用第几根已完成 K 线进行比较;默认值 1 复现了 MQL 脚本的行为。
参数说明
| 名称 | 说明 |
|---|---|
Candle Type |
计算所用的 K 线类型/周期,默认 4 小时时间周期。 |
Primary Method / Primary Length |
第一层平滑(高低价差)的移动平均类型与周期。 |
Secondary Method / Secondary Length |
第二层平滑(标准化结果)的移动平均类型与周期。 |
Stochastic Length |
用于归一化的窗口长度,决定最高值和最低值的回溯范围。 |
Signal Shift |
生成信号时回看已完成 K 线的数量,需 ≥1。 |
Allow Long/Short Entry |
是否允许开多 / 开空。 |
Allow Long/Short Exit |
是否允许在振荡器反转时平多 / 平空。 |
High/Middle/Low Level |
来自原始指标的参考水平线,仅用于图形展示。 |
使用提示
- StockSharp 版本使用平台内置的移动平均线。MQL 中的 ParMA、VIDYA、AMA 等特殊算法会被映射到最接近的可选项;如需更平滑的响应,可选择 Jurik。
- 仓位规模由基础策略的
Volume决定,原 EA 中的自动止损/止盈函数未移植,可结合StartProtection或其他风险控制模块使用。 - 指标仅基于已完成的 K 线计算,请确保数据源能够提供所选周期的足够历史,以便完成两层平滑和随机窗口的预热。
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()