Открыть на GitHub

Стратегия FineTuning MA Candle Duplex

Обзор

  • Порт советника MetaTrader 5 Exp_FineTuningMACandle_Duplex на C#.
  • Воссоздаёт индикатор FineTuningMA в двух независимых потоках, что позволяет отдельно настраивать логику лонгов и шортов.
  • Построена на высокоуровневом API StockSharp: подписки, индикаторы, риск-менеджмент и отрисовка графиков выполняются стандартными средствами фреймворка.

Модель свечи FineTuningMA

  • Оригинальный индикатор формирует синтетическую свечу, применяя три экспоненты (Rank1Rank3) и соответствующие коэффициенты смещения к последним Length барам.
  • Полученные взвешенные значения открытия и закрытия сравниваются и преобразуются в цвет: 2 — бычья свеча, 1 — нейтральная, 0 — медвежья.
  • Если реальное тело свечи меньше порога Gap, синтетическое открытие принудительно приравнивается к предыдущему синтетическому закрытию, как в оригинале MQL5.
  • В данной реализации индикатор публикует только поток цветов (значения 0/1/2), так как торговые правила завязаны исключительно на смене цвета.

Логика торговли

  1. Стратегия подписывается на два потока свечей (LongCandleType и ShortCandleType). Таймфреймы могут совпадать или отличаться.
  2. Для каждого потока создаётся собственный экземпляр индикатора FineTuningMA с индивидуальными весами и сдвигом сигнала (SignalBar).
  3. Завершённые свечи обрабатываются по правилам:
    • Выход из лонга — если предыдущий цвет равен 0, текущая длинная позиция закрывается.
    • Вход в лонг — если предыдущий цвет равен 2, а текущий отличен от 2, отправляется рыночная покупка (с предварительным закрытием шорта, если он был).
    • Выход из шорта — если предыдущий цвет равен 2, короткая позиция покрывается.
    • Вход в шорт — если предыдущий цвет равен 0, а текущий отличен от 0, отправляется рыночная продажа (с предварительным закрытием лонга).
  4. Объём ордера задаётся параметром OrderVolume. При развороте стратегия автоматически прибавляет модуль текущей позиции, чтобы перевернуться одним рыночным ордером.
  5. Опциональные барьеры (TakeProfitPoints, StopLossPoints) переводятся в цену и активируются через StartProtection.

Параметры

Поток лонгов

  • LongCandleType — тип (таймфрейм) свечей для расчёта длинного индикатора.
  • LongLength — число баров во взвешенном окне.
  • LongRank1, LongRank2, LongRank3 — экспоненты, формирующие кривую весов по окну.
  • LongShift1, LongShift2, LongShift3 — дополнительные модификаторы (0…1), смещающие веса к началу или концу окна.
  • LongGap — максимальное тело реальной свечи, при котором синтетическое открытие фиксируется на предыдущем закрытии.
  • LongSignalBar — сколько завершённых свечей пропустить перед чтением сигнала (0 — последняя закрытая свеча, 1 — предыдущая и т.д.).
  • EnableLongEntries — разрешает открытия лонгов.
  • EnableLongExits — разрешает автоматические закрытия лонгов.

Поток шортов

  • ShortCandleType — тип свечей для расчёта короткого индикатора.
  • ShortLength, ShortRank1, ShortRank2, ShortRank3, ShortShift1, ShortShift2, ShortShift3, ShortGap, ShortSignalBar — аналогичные параметры для короткого потока.
  • EnableShortEntries — разрешает открытия шортов.
  • EnableShortExits — разрешает автоматические закрытия шортов.

Торговые параметры

  • OrderVolume — базовый объём новой позиции. При развороте автоматически добавляется модуль текущей позиции.
  • TakeProfitPoints — опциональный тейк-профит в ценовых пунктах (0 — отключён).
  • StopLossPoints — опциональный стоп-лосс в ценовых пунктах (0 — отключён).

Дополнительные сведения

  • В оригинальном советнике присутствовали режимы манименеджмента от баланса/маржи. Здесь оставлен фиксированный параметр OrderVolume; подберите его под требуемый размер позиции.
  • StartProtection вызывается только при наличии шага цены (Security.Step > 0).
  • Python-версия намеренно не создавалась по требованию задачи.
  • При разных таймфреймах строятся две независимые панели на графике, при одинаковых — одна.
  • Стратегия реагирует только на закрытые свечи и игнорирует внутрибаравые изменения.
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>
/// FineTuning MA Candle Duplex strategy using WMA crossover.
/// Buys when fast WMA crosses above slow WMA, sells on reverse.
/// </summary>
public class FineTuningMaCandleDuplexStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private WeightedMovingAverage _fast;
	private WeightedMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	/// <summary>
	/// Fast WMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow WMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="FineTuningMaCandleDuplexStrategy"/> class.
	/// </summary>
	public FineTuningMaCandleDuplexStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast WMA period", "Indicator");

		_slowPeriod = Param(nameof(SlowPeriod), 85)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow WMA period", "Indicator");

		_stopLossPoints = Param(nameof(StopLossPoints), 200)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

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

		_fast = null;
		_slow = null;
		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;
		_cooldown = 0;
	}

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

		_fast = new WeightedMovingAverage { Length = FastPeriod };
		_slow = new WeightedMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fast.IsFormed || !_slow.IsFormed)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		// Check SL/TP
		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 80;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 80;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 80;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 80;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}

		// WMA crossover
		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 80;
		}
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 80;
		}

		_prevFast = fastValue;
		_prevSlow = slowValue;
	}
}