Ver en GitHub

FineTuning MA Candle Duplex Strategy

Overview

  • C# port of the MetaTrader 5 expert advisor Exp_FineTuningMACandle_Duplex.
  • Replicates the FineTuningMA candle indicator in two independent streams so that long and short logic can be tuned separately.
  • Designed for StockSharp's high-level strategy API: subscriptions, indicators, risk management and chart drawing are all managed automatically by the framework.

FineTuningMA candle model

  • The original indicator builds a synthetic candle by applying three weighted exponents (Rank1Rank3) and corresponding shift coefficients to the last Length bars.
  • The resulting weighted open and close values are compared to generate a color code: 2 for bullish, 1 for neutral, 0 for bearish.
  • When the real candle body is smaller than the configurable Gap, the synthetic open is flattened to the previous synthetic close. This reproduces the "flat body" logic from the MQL5 version.
  • The indicator in this port emits only the color stream (decimal values 0/1/2) because the trading rules depend exclusively on the color transitions.

Trading logic

  1. Subscribes to two candle feeds (LongCandleType and ShortCandleType). They can point to the same timeframe or different ones.
  2. For each feed a dedicated FineTuningMA indicator instance is created with its own weighting parameters and signal offset (SignalBar).
  3. Finished candle events are processed with the following rules:
    • Long exit – if the previous color equals 0 the existing long position is closed.
    • Long entry – if the previous color equals 2 and the current color changed away from 2, a buy order is sent (after covering any short position).
    • Short exit – if the previous color equals 2 the existing short position is covered.
    • Short entry – if the previous color equals 0 and the current color changed away from 0, a sell order is sent (after covering any long position).
  4. Order volume is controlled by OrderVolume. When a reversal is required the strategy automatically adds the absolute current position so the position flips in a single market order.
  5. Optional protective barriers (TakeProfitPoints, StopLossPoints) are translated into price points and applied through StartProtection.

Parameters

Long stream

  • LongCandleType – candle data type (timeframe) for the long indicator stream.
  • LongLength – number of bars used in the weighted calculation.
  • LongRank1, LongRank2, LongRank3 – exponent coefficients that shape the weight curve across the lookback window.
  • LongShift1, LongShift2, LongShift3 – additional modifiers (0…1) that bias the weights toward the beginning or the end of the window.
  • LongGap – maximal size of the real candle body that keeps the synthetic open price equal to the previous synthetic close.
  • LongSignalBar – how many completed candles to skip before reading the signal (0 evaluates the last closed candle, 1 uses the previous one, etc.).
  • EnableLongEntries – toggles long entries.
  • EnableLongExits – toggles automatic long exits.

Short stream

  • ShortCandleType – candle data type for the short indicator stream.
  • ShortLength, ShortRank1, ShortRank2, ShortRank3, ShortShift1, ShortShift2, ShortShift3, ShortGap, ShortSignalBar – identical to their long-side counterparts but applied to the short stream.
  • EnableShortEntries – toggles short entries.
  • EnableShortExits – toggles automatic short exits.

Trading

  • OrderVolume – base quantity for new positions. Reversals automatically add the absolute current position to this value.
  • TakeProfitPoints – optional take-profit distance expressed in price points (0 disables it).
  • StopLossPoints – optional stop-loss distance expressed in price points (0 disables it).

Notes

  • The original expert advisor included money-management modes based on balance or margin. The port exposes a simpler fixed OrderVolume parameter. Adjust it to match the desired position sizing.
  • StartProtection is invoked only when the instrument exposes a valid price step (Security.Step > 0).
  • No Python version is provided intentionally (as requested).
  • Chart areas are created automatically: if long and short candle feeds differ, two separate panels are displayed; otherwise only one chart is shown.
  • The strategy relies on finished candles; it does not react to intrabar updates.
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;
	}
}