Открыть на GitHub

Стратегия Multi Time Frame Trader

Стратегия воспроизводит оригинальную MQL-реализацию «Multi Time Frame Trader» на базе высокоуровневого API StockSharp. Она использует три полиномиальных регрессионных канала (M1, M5 и H1) и открывает сделки только тогда, когда младшие таймфреймы касаются границ канала в сторону, заданную наклоном часового канала.

Для каждого таймфрейма на закрытии свечи заново рассчитываются верхняя, средняя и нижняя границы канала. Если верхняя граница на H1 снижается, стратегия работает в сторону продаж; если растет — в сторону покупок. Точки входа формируются, когда свечи M5 и M1 достигают соответствующих границ, а фильтр по часовому наклону подтверждает направление.

Основные этапы работы

  • Подписки: стратегия одновременно подписывается на минутные, пятиминутные и часовые свечи.
  • Регрессионный канал: на каждом потоке строится полиномиальная регрессия степени Degree по Bars точкам и смещается на StdMultiplier стандартных отклонений для получения уровней сопротивления и поддержки.
  • Оценка наклона: наклон определяется разницей между текущей верхней границей и значением этой границы Bars свечей назад — так же, как в индикаторе i-Regr.
  • Фильтр направления: наклон H1 задает, разрешены ли только продажи (наклон отрицательный) или только покупки (наклон положительный).

Правила входа

Короткие позиции

  1. Наклон часового канала отрицательный.
  2. Максимум последней свечи M5 касается или пробивает верхнюю границу пятиминутного канала.
  3. Максимум последней свечи M1 касается или пробивает верхнюю границу минутного канала.
  4. Открытых коротких позиций нет (Position >= 0).
  5. Регистрируется рыночная продажа, стоп-лосс ставится на половину ширины канала выше цены входа, тейк-профит равен средней линии M5.

Длинные позиции

  1. Наклон часового канала положительный.
  2. Минимум последней свечи M5 касается или пробивает нижнюю границу пятиминутного канала.
  3. Минимум последней свечи M1 касается или пробивает нижнюю границу минутного канала.
  4. Открытых длинных позиций нет (Position <= 0).
  5. Регистрируется рыночная покупка, стоп-лосс ставится на половину ширины канала ниже цены входа, тейк-профит равен средней линии M5.

Правила выхода

  • Стоп-лоссы и тейк-профиты хранятся внутри стратегии и проверяются на каждой закрытой свече M1. Если диапазон свечи пересекает стоп-уровень, позиция немедленно закрывается.
  • Если раньше достигается тейк-профит, позиция также закрывается.
  • После выхода все уровни обнуляются, чтобы следующий сигнал обработался без задержки.

Параметры

Параметр Значение по умолчанию Описание
Degree 1 Степень полинома регрессионного канала (1 — линейная, 2 — параболическая, 3 — кубическая).
StdMultiplier 2.0 Множитель стандартного отклонения, определяющий ширину канала.
Bars 250 Количество свечей для расчета регрессии и сравнения наклона.
Shift 0 Горизонтальный сдвиг точки оценки регрессии (ограничен диапазоном 0…Bars - 1).
UseTrading true При значении false заявки не выставляются, но каналы продолжают обновляться.

Дополнительная информация

  • Стоп- и целевые уровни управляются вручную, так как рыночные заявки StockSharp не прикрепляют SL/TP автоматически.
  • Стратегия применима к любым инструментам с минутными и часовыми свечами; исходная версия ориентировалась на валютные пары.
  • Параметр Bars стоит адаптировать под волатильность инструмента: меньшие значения дают более быстрые реакции, большие — более сглаженные каналы.
  • Значение Degree = 1 соответствует классическому линейному каналу, а более высокие степени повторяют полиномиальные режимы оригинального индикатора.
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>
/// Multi time frame regression channel strategy.
/// Converts the MQL "Multi Time Frame Trader" logic to StockSharp.
/// </summary>
public class MultiTimeFrameTraderStrategy : Strategy
{
	private static readonly DataType M1Type = TimeSpan.FromMinutes(5).TimeFrame();
	private static readonly DataType M5Type = TimeSpan.FromHours(1).TimeFrame();
	private static readonly DataType H1Type = TimeSpan.FromHours(4).TimeFrame();

	private readonly StrategyParam<int> _degree;
	private readonly StrategyParam<decimal> _stdMultiplier;
	private readonly StrategyParam<int> _bars;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<bool> _useTrading;

	private RegressionChannelState _m1State;
	private RegressionChannelState _m5State;
	private RegressionChannelState _h1State;

	private Sides? _positionSide;
	private decimal? _stopPrice;
	private decimal? _targetPrice;

	/// <summary>
	/// Polynomial degree for the regression channel (1-3).
	/// </summary>
	public int Degree
	{
		get => _degree.Value;
		set => _degree.Value = value;
	}

	/// <summary>
	/// Standard deviation multiplier used to build the channel width.
	/// </summary>
	public decimal StdMultiplier
	{
		get => _stdMultiplier.Value;
		set => _stdMultiplier.Value = value;
	}

	/// <summary>
	/// Bars used for regression fitting and slope comparison.
	/// </summary>
	public int Bars
	{
		get => _bars.Value;
		set => _bars.Value = value;
	}

	/// <summary>
	/// Bars to shift the regression evaluation point.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// Enables or disables trading logic.
	/// </summary>
	public bool UseTrading
	{
		get => _useTrading.Value;
		set => _useTrading.Value = value;
	}

	public MultiTimeFrameTraderStrategy()
	{
		_degree = Param(nameof(Degree), 1)
			.SetGreaterThanZero()
			.SetDisplay("Polynomial Degree", "Degree for regression channel", "Regression")
			;

		_stdMultiplier = Param(nameof(StdMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Std Multiplier", "Standard deviation multiplier", "Regression")
			;

		_bars = Param(nameof(Bars), 20)
			.SetGreaterThanZero()
			.SetDisplay("Regression Bars", "Bars for regression and slope", "Regression")
			;

		_shift = Param(nameof(Shift), 0)
			.SetNotNegative()
			.SetDisplay("Shift", "Bars to shift regression evaluation", "Regression");

		_useTrading = Param(nameof(UseTrading), true)
			.SetDisplay("Use Trading", "Enable order execution", "Trading");
	}

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

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

		// Clear manual stop/target tracking when the strategy is reset.
		_positionSide = null;
		_stopPrice = null;
		_targetPrice = null;
		_m1State = null;
		_m5State = null;
		_h1State = null;
	}

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

		var degree = Math.Max(1, Math.Min(3, Degree));
		var bars = Math.Max(1, Bars);
		var shift = Math.Max(0, Math.Min(Shift, bars - 1));
		var multiplier = Math.Max(0.1m, StdMultiplier);

		// Initialize regression states for each time frame.
		_m1State = new RegressionChannelState(bars, degree, multiplier, shift);
		_m5State = new RegressionChannelState(bars, degree, multiplier, shift);
		_h1State = new RegressionChannelState(bars, degree, multiplier, shift);

		var m1Subscription = SubscribeCandles(M1Type);
		m1Subscription.Bind(ProcessM1).Start();

		var m5Subscription = SubscribeCandles(M5Type);
		m5Subscription.Bind(ProcessM5).Start();

		var h1Subscription = SubscribeCandles(H1Type);
		h1Subscription.Bind(ProcessH1).Start();
	}

	private void ProcessM1(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Update the regression channel with the latest one-minute candle.
		_m1State?.Process(candle);

		// Manage existing positions before evaluating fresh entry signals.
		TryManagePosition(candle);

		if (!UseTrading)
			return;

		if (_m1State == null || _m5State == null || _h1State == null || !_m1State.IsReady || !_m5State.IsReady || !_h1State.IsReady)
			return;

		var slopeH1 = _h1State.Slope;
		if (slopeH1 is null)
			return;

		var m5Upper = _m5State.Upper;
		var m5Middle = _m5State.Middle;
		var m5Lower = _m5State.Lower;
		var m1Upper = _m1State.Upper;
		var m1Lower = _m1State.Lower;
		if (m5Upper is null || m5Middle is null || m5Lower is null || m1Upper is null || m1Lower is null)
			return;

		var m5High = _m5State.High;
		var m5Low = _m5State.Low;
		var m1High = _m1State.High;
		var m1Low = _m1State.Low;
		if (m5High is null || m5Low is null || m1High is null || m1Low is null)
			return;

		// Short setup: higher time frame slope is down and both M5 and M1 touch the resistance band.
		if (slopeH1 < 0m && Position >= 0m)
		{
			if (m5High >= m5Upper && m1High >= m1Upper)
			{
				var halfWidth = Math.Abs(m5Upper.Value - m5Middle.Value) / 2m;
				var stop = candle.ClosePrice + halfWidth;
				var target = m5Middle.Value;

				EnterShort(stop, target);
				return;
			}
		}

		// Long setup: higher time frame slope is up and both M5 and M1 test the support band.
		if (slopeH1 > 0m && Position <= 0m)
		{
			if (m5Low <= m5Lower && m1Low <= m1Lower)
			{
				var halfWidth = Math.Abs(m5Middle.Value - m5Lower.Value) / 2m;
				var stop = candle.ClosePrice - halfWidth;
				var target = m5Middle.Value;

				EnterLong(stop, target);
			}
		}
	}

	private void ProcessM5(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Store the latest five-minute regression data used for confirmations.
		_m5State?.Process(candle);
	}

	private void ProcessH1(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Track the hourly regression slope to define the dominant direction.
		_h1State?.Process(candle);
	}

	private void TryManagePosition(ICandleMessage candle)
	{
		if (!UseTrading || _positionSide is null)
			return;

		// For long positions check stop loss first, then the profit target.
		if (_positionSide == Sides.Buy)
		{
			if (_stopPrice is not null && candle.LowPrice <= _stopPrice)
			{
				ExitLong();
				return;
			}

			if (_targetPrice is not null && candle.HighPrice >= _targetPrice)
				ExitLong();
		}
		// For short positions mirror the stop and target checks.
		else if (_positionSide == Sides.Sell)
		{
			if (_stopPrice is not null && candle.HighPrice >= _stopPrice)
			{
				ExitShort();
				return;
			}

			if (_targetPrice is not null && candle.LowPrice <= _targetPrice)
				ExitShort();
		}
	}

	private void EnterLong(decimal stop, decimal target)
	{
		// Market entry is issued first, then local stop/target levels are stored.
		BuyMarket();

		_positionSide = Sides.Buy;
		_stopPrice = stop;
		_targetPrice = target;
	}

	private void EnterShort(decimal stop, decimal target)
	{
		SellMarket();

		_positionSide = Sides.Sell;
		_stopPrice = stop;
		_targetPrice = target;
	}

	private void ExitLong()
	{
		SellMarket();

		// Reset tracking so a new setup can be processed immediately.
		_positionSide = null;
		_stopPrice = null;
		_targetPrice = null;
	}

	private void ExitShort()
	{
		BuyMarket();

		_positionSide = null;
		_stopPrice = null;
		_targetPrice = null;
	}

	private sealed class RegressionChannelState
	{
		private readonly int _length;
		private readonly int _degree;
		private readonly decimal _multiplier;
		private readonly int _shift;

		private readonly List<decimal> _closes = new();
		private readonly List<decimal> _upperHistory = new();

		public decimal? Upper { get; private set; }
		public decimal? Middle { get; private set; }
		public decimal? Lower { get; private set; }
		public decimal? Slope { get; private set; }
		public decimal? High { get; private set; }
		public decimal? Low { get; private set; }
		public bool IsReady { get; private set; }

		public RegressionChannelState(int length, int degree, decimal multiplier, int shift)
		{
			_length = length;
			_degree = Math.Max(1, Math.Min(3, degree));
			_multiplier = multiplier;
			_shift = shift;
		}

		public void Process(ICandleMessage candle)
		{
			High = candle.HighPrice;
			Low = candle.LowPrice;

			_closes.Add(candle.ClosePrice);
			if (_closes.Count > _length)
				try { _closes.RemoveAt(0); } catch { }

			if (_closes.Count < _length)
			{
				IsReady = false;
				Upper = null;
				Middle = null;
				Lower = null;
				Slope = null;
				return;
			}

			var values = _closes.ToArray();
			var coeffs = PolyFit(values, _degree);

			var index = values.Length - 1 - Math.Min(_shift, values.Length - 1);
			var mid = PolyEval(coeffs, index);

			decimal sumSquares = 0m;
			for (var i = 0; i < values.Length; i++)
			{
				var estimate = PolyEval(coeffs, i);
				var diff = values[i] - estimate;
				sumSquares += diff * diff;
			}

			var std = (decimal)Math.Sqrt((double)(sumSquares / values.Length));
			var upper = mid + std * _multiplier;
			var lower = mid - std * _multiplier;

			_upperHistory.Add(upper);
			if (_upperHistory.Count > _length + 1)
				try { _upperHistory.RemoveAt(0); } catch { }

			decimal? slope = null;
			if (_upperHistory.Count > _length)
				slope = upper - _upperHistory[0];

			Upper = upper;
			Middle = mid;
			Lower = lower;
			Slope = slope;
			IsReady = true;
		}

		private static decimal[] PolyFit(IReadOnlyList<decimal> values, int degree)
		{
			var n = values.Count;
			var order = Math.Min(degree, n - 1);
			var size = order + 1;
			var matrix = new decimal[size, size + 1];

			for (var row = 0; row < size; row++)
			{
				for (var col = 0; col < size; col++)
				{
					decimal sum = 0m;
					for (var i = 0; i < n; i++)
						sum += (decimal)Math.Pow(i, row + col);

					matrix[row, col] = sum;
				}

				decimal sumY = 0m;
				for (var i = 0; i < n; i++)
					sumY += values[i] * (decimal)Math.Pow(i, row);

				matrix[row, size] = sumY;
			}

			for (var i = 0; i < size; i++)
			{
				if (matrix[i, i] == 0m)
				{
					var swapRow = i + 1;
					while (swapRow < size && matrix[swapRow, i] == 0m)
						swapRow++;

					if (swapRow < size)
						SwapRows(matrix, i, swapRow, size + 1);
				}

				var pivot = matrix[i, i];
				if (pivot == 0m)
					continue;

				for (var j = i; j < size + 1; j++)
					matrix[i, j] /= pivot;

				for (var k = 0; k < size; k++)
				{
					if (k == i)
						continue;

					var factor = matrix[k, i];
					if (factor == 0m)
						continue;

					for (var j = i; j < size + 1; j++)
						matrix[k, j] -= factor * matrix[i, j];
				}
			}

			var coeffs = new decimal[size];
			for (var i = 0; i < size; i++)
				coeffs[i] = matrix[i, size];

			return coeffs;
		}

		private static void SwapRows(decimal[,] matrix, int a, int b, int width)
		{
			for (var col = 0; col < width; col++)
			{
				(matrix[a, col], matrix[b, col]) = (matrix[b, col], matrix[a, col]);
			}
		}

		private static decimal PolyEval(IReadOnlyList<decimal> coeffs, int x)
		{
			decimal y = 0m;
			decimal power = 1m;

			for (var i = 0; i < coeffs.Count; i++)
			{
				y += coeffs[i] * power;
				power *= x;
			}

			return y;
		}
	}
}