在 GitHub 上查看

Stochastic Chaikin's Volatility 策略

概述

本策略是 MetaTrader 指标专家 Exp_Stochastic_Chaikins_Volatility 的 StockSharp 版本。策略先计算每根 K 线高低价之间的区间,并通过可配置的移动平均线进行平滑处理,再将结果以类似随机指标 (Stochastic) 的方式标准化。交易逻辑保持原始 EA 的逆势思路:在振荡指标出现拐点时入场,振荡恢复原方向时出场。

指标构建

  1. Chaikin 式波动率High-Low 差值通过“第一平滑”移动平均处理。可选方法包括 SMA、EMA、SMMA、LWMA 以及 Jurik。
  2. 随机标准化:最近 Stochastic Length 个平滑值用于确定最高值与最低值,再将当前平滑值映射到 0–100 之间。
  3. 再次平滑:对标准化后的结果应用“第二平滑”移动平均,得到主振荡线;信号线等于上一根已完成 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
	}
}