在 GitHub 上查看

Multi Indicator Optimizer 策略

该策略在 StockSharp 高级 API 上还原 MetaTrader 专家顾问 MultiIndicatorOptimizer 的投票机制。五个经典振荡指标在每根已完成的 K 线上给出加权投票,最终汇总成一条综合信号,再根据用户设定的阈值决定是做多、做空还是平仓观望。

交易逻辑

  1. MACD 模块:检查柱状图符号以及主线与信号线的相对位置(均基于上一根收盘 K 线),两种判断结果求平均后乘以 MacdWeight
  2. Awesome Oscillator 模块:判断指标是否位于零轴上方,同时比较动能相对于前一根 K 线是否增强,平均得分乘以 AoWeight
  3. OsMA 模块:读取上一根 K 线的 MACD 柱状图符号并乘以 OsmaWeight
  4. Williams %R 模块:检测是否突破 WilliamsLowerLevelWilliamsUpperLevel 所定义的超卖/超买区。向上离开下轨给出看涨票,向下跌破上轨给出看跌票,并乘以 WilliamsWeight
  5. Stochastic 模块:组合两个条件——%K 穿越 StochasticLowerLevel/StochasticUpperLevel,以及 %K 与 %D 的大小关系。两个子信号平均后乘以 StochasticWeight

综合得分会写入日志的 Signal 列,同时保存在 _lastSignal 字段。交易引擎按以下规则处理:

  • signal >= EntryThreshold:如有空头则平仓,并建立/保持多头。
  • signal <= -EntryThreshold:如有多头则平仓,并建立/保持空头。
  • abs(signal) <= ExitThreshold:若市场处于中性区间,则平仓观望。

所有判断均基于上一根完成的 K 线,与原始 MQL 版本使用的 shift = 1/2 索引保持一致。

参数

参数 说明 默认值
CandleType 指标计算所用的主时间框架。 H1 K 线
MacdFast / MacdSlow / MacdSignal MACD 模块的 EMA 周期。 12 / 26 / 9
MacdWeight MACD 模块的投票权重(可为负以反向)。 1
AoShortPeriod / AoLongPeriod Awesome Oscillator 的快/慢均线周期。 5 / 34
AoWeight Awesome 模块权重。 1
OsmaFastPeriod / OsmaSlowPeriod / OsmaSignalPeriod 构建 OsMA 柱状图的 MACD 参数。 12 / 26 / 9
OsmaWeight OsMA 模块权重。 1
WilliamsPeriod Williams %R 的回溯长度。 14
WilliamsLowerLevel / WilliamsUpperLevel 超卖/超买边界(百分比)。 -80 / -20
WilliamsWeight Williams 模块权重。 1
StochasticKPeriod / StochasticDPeriod / StochasticSlowing 随机指标 %K、%D 及平滑参数。 5 / 3 / 3
StochasticLowerLevel / StochasticUpperLevel %K 的超卖/超买阈值。 20 / 80
StochasticWeight Stochastic 模块权重。 1
EntryThreshold 开仓或反向所需的最小绝对得分。 0.5
ExitThreshold 中性区间宽度:当 信号的绝对值小于该值时全部平仓。 0.1

各个权重均允许设置为负值,以关闭或反向某个模块的投票,便于参数优化。

说明

  • 全部逻辑基于高级 API:使用 SubscribeCandles 订阅、指标绑定以及 BuyMarket/SellMarket 下单助手。
  • 仅在蜡烛收盘后更新信号,确保决策来自确认数据。
  • 持仓量由 Strategy.Volume 决定,若需要止盈止损可以额外调用 StartProtection 设置。
  • 代码内附有详细的英文注释,方便继续维护与扩展。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-indicator voting strategy that aggregates MACD, Awesome Oscillator,
/// OsMA, Williams %R, and Stochastic Oscillator signals.
/// </summary>
public class MultiIndicatorOptimizerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<decimal> _macdWeight;

	private readonly StrategyParam<int> _aoShort;
	private readonly StrategyParam<int> _aoLong;
	private readonly StrategyParam<decimal> _aoWeight;

	private readonly StrategyParam<int> _osmaFast;
	private readonly StrategyParam<int> _osmaSlow;
	private readonly StrategyParam<int> _osmaSignal;
	private readonly StrategyParam<decimal> _osmaWeight;

	private readonly StrategyParam<int> _williamsPeriod;
	private readonly StrategyParam<decimal> _williamsLower;
	private readonly StrategyParam<decimal> _williamsUpper;
	private readonly StrategyParam<decimal> _williamsWeight;

	private readonly StrategyParam<int> _stochKPeriod;
	private readonly StrategyParam<int> _stochDPeriod;
	private readonly StrategyParam<int> _stochSlowing;
	private readonly StrategyParam<decimal> _stochLower;
	private readonly StrategyParam<decimal> _stochUpper;
	private readonly StrategyParam<decimal> _stochWeight;

	private readonly StrategyParam<decimal> _entryThreshold;
	private readonly StrategyParam<decimal> _exitThreshold;

	private decimal? _prevMacdMain;
	private decimal? _prevMacdSignal;
	private decimal? _prevOsma;

	private decimal? _prevAo;
	private decimal? _prevPrevAo;

	private decimal? _prevWilliams;
	private decimal? _prevPrevWilliams;

	private decimal? _prevStochK;
	private decimal? _prevPrevStochK;
	private decimal? _prevStochSignal;

	private decimal _lastSignal;

	/// <summary>
	/// Initializes a new instance of <see cref="MultiIndicatorOptimizerStrategy"/>.
	/// </summary>
	public MultiIndicatorOptimizerStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for indicator calculations", "General");

		_macdFast = Param(nameof(MacdFast), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA period for MACD", "MACD")
			
			.SetOptimize(6, 24, 2);

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA period for MACD", "MACD")
			
			.SetOptimize(20, 40, 2);

		_macdSignal = Param(nameof(MacdSignal), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal line period for MACD", "MACD")
			
			.SetOptimize(5, 15, 1);

		_macdWeight = Param(nameof(MacdWeight), 1m)
			.SetDisplay("MACD Weight", "Voting weight of MACD block", "Weights")
			
			.SetOptimize(-2m, 2m, 0.5m);

		_aoShort = Param(nameof(AoShortPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("AO Short", "Short moving average for Awesome Oscillator", "Awesome")
			
			.SetOptimize(3, 10, 1);

		_aoLong = Param(nameof(AoLongPeriod), 34)
			.SetGreaterThanZero()
			.SetDisplay("AO Long", "Long moving average for Awesome Oscillator", "Awesome")
			
			.SetOptimize(20, 50, 2);

		_aoWeight = Param(nameof(AoWeight), 1m)
			.SetDisplay("AO Weight", "Voting weight of Awesome Oscillator block", "Weights")
			
			.SetOptimize(-2m, 2m, 0.5m);

		_osmaFast = Param(nameof(OsmaFastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Fast", "Fast EMA period for OsMA histogram", "OsMA")
			
			.SetOptimize(6, 24, 2);

		_osmaSlow = Param(nameof(OsmaSlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Slow", "Slow EMA period for OsMA histogram", "OsMA")
			
			.SetOptimize(20, 40, 2);

		_osmaSignal = Param(nameof(OsmaSignalPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("OsMA Signal", "Signal EMA period for OsMA histogram", "OsMA")
			
			.SetOptimize(5, 15, 1);

		_osmaWeight = Param(nameof(OsmaWeight), 1m)
			.SetDisplay("OsMA Weight", "Voting weight of OsMA block", "Weights")
			
			.SetOptimize(-2m, 2m, 0.5m);

		_williamsPeriod = Param(nameof(WilliamsPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Williams %R Period", "Lookback for Williams %R", "Williams %R")
			
			.SetOptimize(10, 30, 2);

		_williamsLower = Param(nameof(WilliamsLowerLevel), -80m)
			.SetDisplay("Williams Lower", "Oversold boundary for Williams %R", "Williams %R");

		_williamsUpper = Param(nameof(WilliamsUpperLevel), -20m)
			.SetDisplay("Williams Upper", "Overbought boundary for Williams %R", "Williams %R");

		_williamsWeight = Param(nameof(WilliamsWeight), 1m)
			.SetDisplay("Williams Weight", "Voting weight of Williams %R block", "Weights")
			
			.SetOptimize(-2m, 2m, 0.5m);

		_stochKPeriod = Param(nameof(StochasticKPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "%K period for Stochastic Oscillator", "Stochastic")
			
			.SetOptimize(3, 15, 1);

		_stochDPeriod = Param(nameof(StochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "%D period for Stochastic Oscillator", "Stochastic")

			.SetOptimize(2, 9, 1);

		_stochSlowing = Param(nameof(StochasticSlowing), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smoothing", "Smoothing applied to %K", "Stochastic")
			
			.SetOptimize(1, 9, 1);

		_stochLower = Param(nameof(StochasticLowerLevel), 20m)
			.SetDisplay("Stochastic Lower", "Oversold threshold for Stochastic", "Stochastic");

		_stochUpper = Param(nameof(StochasticUpperLevel), 80m)
			.SetDisplay("Stochastic Upper", "Overbought threshold for Stochastic", "Stochastic");

		_stochWeight = Param(nameof(StochasticWeight), 1m)
			.SetDisplay("Stochastic Weight", "Voting weight of Stochastic block", "Weights")
			
			.SetOptimize(-2m, 2m, 0.5m);

		_entryThreshold = Param(nameof(EntryThreshold), 0.5m)
			.SetDisplay("Entry Threshold", "Minimum aggregated signal required to open a position", "Trading")
			
			.SetOptimize(0.25m, 2m, 0.25m);

		_exitThreshold = Param(nameof(ExitThreshold), 0.1m)
			.SetDisplay("Exit Threshold", "Signal absolute value required to flat the position", "Trading")
			
			.SetOptimize(0.05m, 1m, 0.05m);
	}

	/// <summary>
	/// Candle type for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Fast EMA period for the MACD block.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period for the MACD block.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal EMA period for the MACD block.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

	/// <summary>
	/// Weight of the MACD voting block.
	/// </summary>
	public decimal MacdWeight
	{
		get => _macdWeight.Value;
		set => _macdWeight.Value = value;
	}

	/// <summary>
	/// Short period used by the Awesome Oscillator.
	/// </summary>
	public int AoShortPeriod
	{
		get => _aoShort.Value;
		set => _aoShort.Value = value;
	}

	/// <summary>
	/// Long period used by the Awesome Oscillator.
	/// </summary>
	public int AoLongPeriod
	{
		get => _aoLong.Value;
		set => _aoLong.Value = value;
	}

	/// <summary>
	/// Weight of the Awesome Oscillator block.
	/// </summary>
	public decimal AoWeight
	{
		get => _aoWeight.Value;
		set => _aoWeight.Value = value;
	}

	/// <summary>
	/// Fast EMA period for the OsMA histogram.
	/// </summary>
	public int OsmaFastPeriod
	{
		get => _osmaFast.Value;
		set => _osmaFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period for the OsMA histogram.
	/// </summary>
	public int OsmaSlowPeriod
	{
		get => _osmaSlow.Value;
		set => _osmaSlow.Value = value;
	}

	/// <summary>
	/// Signal EMA period for the OsMA histogram.
	/// </summary>
	public int OsmaSignalPeriod
	{
		get => _osmaSignal.Value;
		set => _osmaSignal.Value = value;
	}

	/// <summary>
	/// Weight of the OsMA voting block.
	/// </summary>
	public decimal OsmaWeight
	{
		get => _osmaWeight.Value;
		set => _osmaWeight.Value = value;
	}

	/// <summary>
	/// Lookback length for Williams %R.
	/// </summary>
	public int WilliamsPeriod
	{
		get => _williamsPeriod.Value;
		set => _williamsPeriod.Value = value;
	}

	/// <summary>
	/// Oversold level for Williams %R.
	/// </summary>
	public decimal WilliamsLowerLevel
	{
		get => _williamsLower.Value;
		set => _williamsLower.Value = value;
	}

	/// <summary>
	/// Overbought level for Williams %R.
	/// </summary>
	public decimal WilliamsUpperLevel
	{
		get => _williamsUpper.Value;
		set => _williamsUpper.Value = value;
	}

	/// <summary>
	/// Weight of the Williams %R block.
	/// </summary>
	public decimal WilliamsWeight
	{
		get => _williamsWeight.Value;
		set => _williamsWeight.Value = value;
	}

	/// <summary>
	/// %K period for the Stochastic Oscillator.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochKPeriod.Value;
		set => _stochKPeriod.Value = value;
	}

	/// <summary>
	/// %D period for the Stochastic Oscillator.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochDPeriod.Value;
		set => _stochDPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing factor applied to %K.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochSlowing.Value;
		set => _stochSlowing.Value = value;
	}

	/// <summary>
	/// Oversold threshold for the Stochastic Oscillator.
	/// </summary>
	public decimal StochasticLowerLevel
	{
		get => _stochLower.Value;
		set => _stochLower.Value = value;
	}

	/// <summary>
	/// Overbought threshold for the Stochastic Oscillator.
	/// </summary>
	public decimal StochasticUpperLevel
	{
		get => _stochUpper.Value;
		set => _stochUpper.Value = value;
	}

	/// <summary>
	/// Weight of the Stochastic voting block.
	/// </summary>
	public decimal StochasticWeight
	{
		get => _stochWeight.Value;
		set => _stochWeight.Value = value;
	}

	/// <summary>
	/// Minimum aggregated score required to open a position.
	/// </summary>
	public decimal EntryThreshold
	{
		get => _entryThreshold.Value;
		set => _entryThreshold.Value = value;
	}

	/// <summary>
	/// Maximum absolute score to keep an existing position open.
	/// </summary>
	public decimal ExitThreshold
	{
		get => _exitThreshold.Value;
		set => _exitThreshold.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevMacdMain = null;
		_prevMacdSignal = null;
		_prevOsma = null;

		_prevAo = null;
		_prevPrevAo = null;

		_prevWilliams = null;
		_prevPrevWilliams = null;

		_prevStochK = null;
		_prevPrevStochK = null;
		_prevStochSignal = null;

		_lastSignal = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFast },
				LongMa = { Length = MacdSlow }
			},
			SignalMa = { Length = MacdSignal }
		};

		var osma = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = OsmaFastPeriod },
				LongMa = { Length = OsmaSlowPeriod }
			},
			SignalMa = { Length = OsmaSignalPeriod }
		};

		var awesome = new AwesomeOscillator
		{
			ShortMa = { Length = AoShortPeriod },
			LongMa = { Length = AoLongPeriod }
		};

		var williams = new WilliamsR { Length = WilliamsPeriod };

		var stochastic = new StochasticOscillator();
		stochastic.K.Length = StochasticKPeriod;
		stochastic.D.Length = StochasticDPeriod;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(macd, osma, awesome, williams, stochastic, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, macd);
			DrawIndicator(area, awesome);
			DrawIndicator(area, osma);

			var extraArea = CreateChartArea();
			if (extraArea != null)
			{
				DrawIndicator(extraArea, williams);
				DrawIndicator(extraArea, stochastic);
			}

			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(
		ICandleMessage candle,
		IIndicatorValue macdValue,
		IIndicatorValue osmaValue,
		IIndicatorValue awesomeValue,
		IIndicatorValue williamsValue,
		IIndicatorValue stochasticValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!macdValue.IsFinal || !osmaValue.IsFinal || !awesomeValue.IsFinal || !williamsValue.IsFinal || !stochasticValue.IsFinal)
			return;

		var macdSignal = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
		var osmaSignal = (MovingAverageConvergenceDivergenceSignalValue)osmaValue;

		if (macdSignal.Macd is not decimal currentMacd || macdSignal.Signal is not decimal currentMacdSignal)
			return;

		var currentOsma = osmaSignal.Macd is decimal osmaMacd && osmaSignal.Signal is decimal osmaSignalLine
			? osmaMacd - osmaSignalLine
			: (decimal?)null;

		if (currentOsma is null)
			return;

		var currentAo = awesomeValue.ToDecimal();
		var currentWilliams = williamsValue.ToDecimal();

		var stoch = (StochasticOscillatorValue)stochasticValue;
		if (stoch.K is not decimal currentStochK || stoch.D is not decimal currentStochD)
			return;

		decimal signal = 0m;

		if (_prevMacdMain is decimal prevMacd && _prevMacdSignal is decimal prevSignal)
		{
			var mainScore = prevMacd > 0m ? 1m : prevMacd < 0m ? -1m : 0m;
			var crossScore = prevMacd > prevSignal ? 1m : prevMacd < prevSignal ? -1m : 0m;
			signal += (mainScore + crossScore) / 2m * MacdWeight;
		}

		if (_prevAo is decimal prevAo)
		{
			var directionScore = prevAo > 0m ? 1m : prevAo < 0m ? -1m : 0m;
			var momentumScore = _prevPrevAo is decimal prevPrevAo
				? prevAo > prevPrevAo ? 1m : prevAo < prevPrevAo ? -1m : 0m
				: 0m;
			signal += (directionScore + momentumScore) / 2m * AoWeight;
		}

		if (_prevOsma is decimal prevOsma)
		{
			var osmaScore = prevOsma > 0m ? 1m : prevOsma < 0m ? -1m : 0m;
			signal += osmaScore * OsmaWeight;
		}

		if (_prevWilliams is decimal prevWilliams)
		{
			var wprScore = 0m;
			if (_prevPrevWilliams is decimal prevPrevWilliams)
			{
				if (prevWilliams > WilliamsLowerLevel && prevPrevWilliams <= WilliamsLowerLevel)
					wprScore = 1m;
				else if (prevWilliams < WilliamsUpperLevel && prevPrevWilliams >= WilliamsUpperLevel)
					wprScore = -1m;
			}

			signal += wprScore * WilliamsWeight;
		}

		if (_prevStochK is decimal prevStochK && _prevStochSignal is decimal prevStochSignal)
		{
			var stochScore1 = 0m;
			if (_prevPrevStochK is decimal prevPrevStochK)
			{
				if (prevStochK > StochasticLowerLevel && prevPrevStochK <= StochasticLowerLevel)
					stochScore1 = 1m;
				else if (prevStochK < StochasticUpperLevel && prevPrevStochK >= StochasticUpperLevel)
					stochScore1 = -1m;
			}

			var stochScore2 = prevStochK > prevStochSignal ? 1m : prevStochK < prevStochSignal ? -1m : 0m;
			signal += (stochScore1 + stochScore2) / 2m * StochasticWeight;
		}

		_lastSignal = signal;

		ExecuteTradingLogic(signal);

		_prevMacdMain = currentMacd;
		_prevMacdSignal = currentMacdSignal;
		_prevOsma = currentOsma;

		_prevPrevAo = _prevAo;
		_prevAo = currentAo;

		_prevPrevWilliams = _prevWilliams;
		_prevWilliams = currentWilliams;

		_prevPrevStochK = _prevStochK;
		_prevStochK = currentStochK;
		_prevStochSignal = currentStochD;
	}

	private void ExecuteTradingLogic(decimal signal)
	{
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Volume <= 0)
			return;

		if (signal >= EntryThreshold)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position) + Volume);
			else if (Position == 0)
				BuyMarket(Volume);

			return;
		}

		if (signal <= -EntryThreshold)
		{
			if (Position > 0)
				SellMarket(Position + Volume);
			else if (Position == 0)
				SellMarket(Volume);

			return;
		}

		if (Math.Abs(signal) <= ExitThreshold && Position != 0)
		{
			if (Position > 0)
				SellMarket(Position);
			else
				BuyMarket(Math.Abs(Position));
		}
	}
}