在 GitHub 上查看

Multi Time Frame Trader 策略

该策略基于 StockSharp 高级 API 复刻了原始 MQL 版本的「Multi Time Frame Trader」。策略同时使用 1 分钟、5 分钟 和 1 小时三条多项式回归通道,只有在短周期触及通道边界且方向与小时通道斜率一致时才会开仓。

每根完成的 K 线都会重新计算对应时间框架的通道上轨、中轨和下轨。如果小时级上轨下降,系统偏空;上轨 上升则偏多。当 M5 和 M1 的价格触及对应边界且方向筛选满足条件时,触发入场。

运行流程

  • 订阅:同时订阅 1 分钟、5 分钟和 1 小时的蜡烛数据。
  • 回归通道:每个订阅都使用 Bars 根历史数据拟合 Degree 阶多项式回归,并按照 StdMultiplier 个标准 差向上/向下平移,得到阻力和支撑带。
  • 斜率估计:通道斜率由当前上轨与 Bars 根之前的上轨差值计算,完全对齐 i-Regr 指标的做法。
  • 方向过滤:小时图斜率决定是否只允许做空(斜率为负)或只允许做多(斜率为正)。

入场条件

做空

  1. 小时通道斜率为负。
  2. 最新一根 M5 K 线的最高价触及或突破 M5 回归上轨。
  3. 最新一根 M1 K 线的最高价触及或突破 M1 回归上轨。
  4. 当前没有持有空头仓位(Position >= 0)。
  5. 发送市价卖出,止损放在入场价上方半个通道宽度,止盈设为 M5 中轨。

做多

  1. 小时通道斜率为正。
  2. 最新一根 M5 K 线的最低价触及或跌破 M5 回归下轨。
  3. 最新一根 M1 K 线的最低价触及或跌破 M1 回归下轨。
  4. 当前没有持有多头仓位(Position <= 0)。
  5. 发送市价买入,止损放在入场价下方半个通道宽度,止盈设为 M5 中轨。

平仓规则

  • 止损和止盈在策略内部保存,并在每根完成的 M1 K 线上进行检查。一旦 K 线区间触及止损,立即平仓。
  • 如果先到达止盈,也会立刻平仓。
  • 平仓后会清空保存的止损/止盈,以便下一次信号无需等待。

参数说明

参数 默认值 描述
Degree 1 回归通道的多项式阶数(1=线性,2=二次,3=三次)。
StdMultiplier 2.0 用于通道宽度的标准差倍数。
Bars 250 用于回归拟合和斜率比较的历史 K 线数量。
Shift 0 回归评估点的水平偏移(限制在 0 至 Bars - 1)。
UseTrading true false 时仅计算指标,不发送任何委托。

其他提示

  • 因为 StockSharp 的市价单不会自动附带止损/止盈,策略会自行跟踪并在满足条件时手动平仓。
  • 适用于提供分钟和小时数据的任何品种,原始版本主要针对外汇货币对。
  • 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;
		}
	}
}