Ver no GitHub

Diff TF MA Strategy

Overview

  • This strategy is a StockSharp port of the MetaTrader "Diff_TF_MA_EA" expert advisor.
  • Trading signals come from comparing a simple moving average calculated on a higher timeframe with another moving average that is rescaled to the trading timeframe.
  • The code keeps only finished candles, mirrors the original crossover rules, and closes any opposite exposure before opening a new position.

Parameters

Name Description
MaPeriod Length of the simple moving average calculated on the higher timeframe.
CandleType Trading timeframe used for order generation.
HigherCandleType Higher timeframe that supplies the reference moving average.
ReverseSignals Inverts the crossover rules (buy on bearish cross and sell on bullish cross).
Volume Strategy volume used by BuyMarket/SellMarket calls (set through the base Strategy.Volume property).

Trading logic

  1. Subscribe to both the trading timeframe (CandleType) and the higher timeframe (HigherCandleType).
  2. Build a simple moving average with length MaPeriod on the higher timeframe.
  3. Convert the higher timeframe length into the trading timeframe by multiplying by the ratio of timeframe durations and run another moving average on the trading candles.
  4. Store the last two completed values for both moving averages and check for crossings on every finished trading candle.
  5. Open or reverse to a long position when the higher timeframe MA crosses above the trading MA (unless ReverseSignals is true).
  6. Open or reverse to a short position when the higher timeframe MA crosses below the trading MA (unless ReverseSignals is true).
  7. Positions are flattened and flipped by sending enough volume to offset any existing exposure.

Usage notes

  • Choose compatible timeframes: the higher timeframe should usually be larger than the trading timeframe so the rescaled length is meaningful.
  • The default volume is 1. Adjust Strategy.Volume before starting the strategy if another size is required.
  • Stops and take-profits from the MetaTrader version are not reproduced; risk management can be attached through StockSharp protections if needed.
  • When ReverseSignals is enabled, bullish and bearish actions are swapped while the rest of the logic remains unchanged.
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>
/// Two timeframe moving average crossover strategy.
/// </summary>
public class DiffTfMaStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _higherCandleType;
	private readonly StrategyParam<bool> _reverseSignals;

	private SimpleMovingAverage _baseMa;
	private SimpleMovingAverage _higherMa;

	private decimal? _higherMaLast;
	private decimal? _higherMaPrev;
	private decimal? _baseMaLast;
	private decimal? _baseMaPrev;

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public DataType HigherCandleType
	{
		get => _higherCandleType.Value;
		set => _higherCandleType.Value = value;
	}

	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	public DiffTfMaStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average length on the higher timeframe", "General")
			
			.SetOptimize(5, 30, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Base Candle", "Trading timeframe", "General");

		_higherCandleType = Param(nameof(HigherCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Higher Candle", "Higher timeframe for confirmation", "General");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert the crossover logic", "General");

		Volume = 0.1m;
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_higherMaLast = null;
		_higherMaPrev = null;
		_baseMaLast = null;
		_baseMaPrev = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (CandleType.Arg is not TimeSpan baseSpan || baseSpan <= TimeSpan.Zero)
			throw new InvalidOperationException("CandleType must contain a positive TimeSpan argument.");

		if (HigherCandleType.Arg is not TimeSpan higherSpan || higherSpan <= TimeSpan.Zero)
			throw new InvalidOperationException("HigherCandleType must contain a positive TimeSpan argument.");

		var ratio = higherSpan.TotalMinutes / baseSpan.TotalMinutes;
		var baseLength = Math.Max(1, (int)(MaPeriod * ratio));

		_baseMa = new SimpleMovingAverage { Length = baseLength };
		_higherMa = new SimpleMovingAverage { Length = MaPeriod };

		var higherSubscription = SubscribeCandles(HigherCandleType);
		higherSubscription.Bind(_higherMa, ProcessHigher).Start();

		var baseSubscription = SubscribeCandles(CandleType);
		baseSubscription.Bind(_baseMa, ProcessBase).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, baseSubscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessHigher(ICandleMessage candle, decimal higherMaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_higherMa.IsFormed)
			return;

		// Store the last two higher timeframe MA values for crossover comparison.
		_higherMaPrev = _higherMaLast;
		_higherMaLast = higherMaValue;
	}

	private void ProcessBase(ICandleMessage candle, decimal baseMaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_baseMa.IsFormed)
			return;

		// Track the last two base timeframe MA values.
		_baseMaPrev = _baseMaLast;
		_baseMaLast = baseMaValue;

		if (_higherMaPrev is not decimal higherPrev || _higherMaLast is not decimal higherLast)
			return;

		if (_baseMaPrev is not decimal basePrev || _baseMaLast is not decimal baseLast)
			return;

		var crossUp = higherPrev < basePrev && higherLast > baseLast;
		var crossDown = higherPrev > basePrev && higherLast < baseLast;

		if (ReverseSignals)
			(crossUp, crossDown) = (crossDown, crossUp);

		// Execute orders according to the detected crossover direction.
		if (crossUp && Position <= 0)
		{
			BuyMarket(Volume + Math.Abs(Position));
		}
		else if (crossDown && Position >= 0)
		{
			SellMarket(Volume + Math.Abs(Position));
		}
	}
}