Стратегия Multi Time Frame Trader
Стратегия воспроизводит оригинальную MQL-реализацию «Multi Time Frame Trader» на базе высокоуровневого API StockSharp. Она использует три полиномиальных регрессионных канала (M1, M5 и H1) и открывает сделки только тогда, когда младшие таймфреймы касаются границ канала в сторону, заданную наклоном часового канала.
Для каждого таймфрейма на закрытии свечи заново рассчитываются верхняя, средняя и нижняя границы канала. Если верхняя граница на H1 снижается, стратегия работает в сторону продаж; если растет — в сторону покупок. Точки входа формируются, когда свечи M5 и M1 достигают соответствующих границ, а фильтр по часовому наклону подтверждает направление.
Основные этапы работы
- Подписки: стратегия одновременно подписывается на минутные, пятиминутные и часовые свечи.
- Регрессионный канал: на каждом потоке строится полиномиальная регрессия степени
DegreeпоBarsточкам и смещается наStdMultiplierстандартных отклонений для получения уровней сопротивления и поддержки. - Оценка наклона: наклон определяется разницей между текущей верхней границей и значением этой границы
Barsсвечей назад — так же, как в индикатореi-Regr. - Фильтр направления: наклон H1 задает, разрешены ли только продажи (наклон отрицательный) или только покупки (наклон положительный).
Правила входа
Короткие позиции
- Наклон часового канала отрицательный.
- Максимум последней свечи M5 касается или пробивает верхнюю границу пятиминутного канала.
- Максимум последней свечи M1 касается или пробивает верхнюю границу минутного канала.
- Открытых коротких позиций нет (
Position >= 0). - Регистрируется рыночная продажа, стоп-лосс ставится на половину ширины канала выше цены входа, тейк-профит равен средней линии M5.
Длинные позиции
- Наклон часового канала положительный.
- Минимум последней свечи M5 касается или пробивает нижнюю границу пятиминутного канала.
- Минимум последней свечи M1 касается или пробивает нижнюю границу минутного канала.
- Открытых длинных позиций нет (
Position <= 0). - Регистрируется рыночная покупка, стоп-лосс ставится на половину ширины канала ниже цены входа, тейк-профит равен средней линии 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;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class _RegressionChannelState(object):
"""Internal regression channel calculator."""
def __init__(self, length, degree, multiplier, shift):
self._length = length
self._degree = max(1, min(3, degree))
self._multiplier = multiplier
self._shift = shift
self._closes = []
self._upper_history = []
self.Upper = None
self.Middle = None
self.Lower = None
self.Slope = None
self.High = None
self.Low = None
self.IsReady = False
def process(self, candle):
self.High = float(candle.HighPrice)
self.Low = float(candle.LowPrice)
self._closes.append(float(candle.ClosePrice))
if len(self._closes) > self._length:
self._closes.pop(0)
if len(self._closes) < self._length:
self.IsReady = False
self.Upper = None
self.Middle = None
self.Lower = None
self.Slope = None
return
values = self._closes[:]
coeffs = self._poly_fit(values, self._degree)
index = len(values) - 1 - min(self._shift, len(values) - 1)
mid = self._poly_eval(coeffs, index)
sum_sq = 0.0
for i in range(len(values)):
estimate = self._poly_eval(coeffs, i)
diff = values[i] - estimate
sum_sq += diff * diff
std = math.sqrt(sum_sq / len(values))
upper = mid + std * self._multiplier
lower = mid - std * self._multiplier
self._upper_history.append(upper)
if len(self._upper_history) > self._length + 1:
self._upper_history.pop(0)
slope = None
if len(self._upper_history) > self._length:
slope = upper - self._upper_history[0]
self.Upper = upper
self.Middle = mid
self.Lower = lower
self.Slope = slope
self.IsReady = True
def _poly_fit(self, values, degree):
n = len(values)
order = min(degree, n - 1)
size = order + 1
# Build augmented matrix
matrix = [[0.0] * (size + 1) for _ in range(size)]
for row in range(size):
for col in range(size):
s = 0.0
for i in range(n):
s += i ** (row + col)
matrix[row][col] = s
sy = 0.0
for i in range(n):
sy += values[i] * (i ** row)
matrix[row][size] = sy
# Gauss-Jordan elimination
for i in range(size):
if matrix[i][i] == 0:
swap_row = i + 1
while swap_row < size and matrix[swap_row][i] == 0:
swap_row += 1
if swap_row < size:
matrix[i], matrix[swap_row] = matrix[swap_row], matrix[i]
pivot = matrix[i][i]
if pivot == 0:
continue
for j in range(i, size + 1):
matrix[i][j] /= pivot
for k in range(size):
if k == i:
continue
factor = matrix[k][i]
if factor == 0:
continue
for j in range(i, size + 1):
matrix[k][j] -= factor * matrix[i][j]
return [matrix[i][size] for i in range(size)]
def _poly_eval(self, coeffs, x):
y = 0.0
power = 1.0
for c in coeffs:
y += c * power
power *= x
return y
class multi_time_frame_trader_strategy(Strategy):
"""Multi time frame regression channel strategy with 3 timeframes."""
def __init__(self):
super(multi_time_frame_trader_strategy, self).__init__()
self._degree = self.Param("Degree", 1) \
.SetGreaterThanZero() \
.SetDisplay("Polynomial Degree", "Degree for regression channel", "Regression")
self._std_multiplier = self.Param("StdMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Std Multiplier", "Standard deviation multiplier", "Regression")
self._bars = self.Param("Bars", 20) \
.SetGreaterThanZero() \
.SetDisplay("Regression Bars", "Bars for regression and slope", "Regression")
self._shift = self.Param("Shift", 0) \
.SetDisplay("Shift", "Bars to shift regression evaluation", "Regression")
self._use_trading = self.Param("UseTrading", True) \
.SetDisplay("Use Trading", "Enable order execution", "Trading")
self._m1_type = DataType.TimeFrame(TimeSpan.FromMinutes(5))
self._m5_type = DataType.TimeFrame(TimeSpan.FromHours(1))
self._h1_type = DataType.TimeFrame(TimeSpan.FromHours(4))
self._m1_state = None
self._m5_state = None
self._h1_state = None
self._position_side = None # 'buy' or 'sell'
self._stop_price = None
self._target_price = None
@property
def Degree(self):
return int(self._degree.Value)
@property
def StdMultiplier(self):
return float(self._std_multiplier.Value)
@property
def Bars(self):
return int(self._bars.Value)
@property
def Shift(self):
return int(self._shift.Value)
@property
def UseTrading(self):
return self._use_trading.Value
def OnStarted2(self, time):
super(multi_time_frame_trader_strategy, self).OnStarted2(time)
degree = max(1, min(3, self.Degree))
bars = max(1, self.Bars)
shift = max(0, min(self.Shift, bars - 1))
multiplier = max(0.1, self.StdMultiplier)
self._m1_state = _RegressionChannelState(bars, degree, multiplier, shift)
self._m5_state = _RegressionChannelState(bars, degree, multiplier, shift)
self._h1_state = _RegressionChannelState(bars, degree, multiplier, shift)
self._position_side = None
self._stop_price = None
self._target_price = None
m1_sub = self.SubscribeCandles(self._m1_type)
m1_sub.Bind(self._process_m1).Start()
m5_sub = self.SubscribeCandles(self._m5_type)
m5_sub.Bind(self._process_m5).Start()
h1_sub = self.SubscribeCandles(self._h1_type)
h1_sub.Bind(self._process_h1).Start()
def _process_m1(self, candle):
if candle.State != CandleStates.Finished:
return
self._m1_state.process(candle)
self._try_manage_position(candle)
if not self.UseTrading:
return
if self._m1_state is None or self._m5_state is None or self._h1_state is None:
return
if not self._m1_state.IsReady or not self._m5_state.IsReady or not self._h1_state.IsReady:
return
slope_h1 = self._h1_state.Slope
if slope_h1 is None:
return
m5_upper = self._m5_state.Upper
m5_middle = self._m5_state.Middle
m5_lower = self._m5_state.Lower
m1_upper = self._m1_state.Upper
m1_lower = self._m1_state.Lower
if m5_upper is None or m5_middle is None or m5_lower is None or m1_upper is None or m1_lower is None:
return
m5_high = self._m5_state.High
m5_low = self._m5_state.Low
m1_high = self._m1_state.High
m1_low = self._m1_state.Low
if m5_high is None or m5_low is None or m1_high is None or m1_low is None:
return
close = float(candle.ClosePrice)
# Short setup
if slope_h1 < 0 and self.Position >= 0:
if m5_high >= m5_upper and m1_high >= m1_upper:
half_width = abs(m5_upper - m5_middle) / 2.0
stop = close + half_width
target = m5_middle
self._enter_short(stop, target)
return
# Long setup
if slope_h1 > 0 and self.Position <= 0:
if m5_low <= m5_lower and m1_low <= m1_lower:
half_width = abs(m5_middle - m5_lower) / 2.0
stop = close - half_width
target = m5_middle
self._enter_long(stop, target)
def _process_m5(self, candle):
if candle.State != CandleStates.Finished:
return
self._m5_state.process(candle)
def _process_h1(self, candle):
if candle.State != CandleStates.Finished:
return
self._h1_state.process(candle)
def _try_manage_position(self, candle):
if not self.UseTrading or self._position_side is None:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._position_side == 'buy':
if self._stop_price is not None and lo <= self._stop_price:
self._exit_long()
return
if self._target_price is not None and h >= self._target_price:
self._exit_long()
elif self._position_side == 'sell':
if self._stop_price is not None and h >= self._stop_price:
self._exit_short()
return
if self._target_price is not None and lo <= self._target_price:
self._exit_short()
def _enter_long(self, stop, target):
self.BuyMarket()
self._position_side = 'buy'
self._stop_price = stop
self._target_price = target
def _enter_short(self, stop, target):
self.SellMarket()
self._position_side = 'sell'
self._stop_price = stop
self._target_price = target
def _exit_long(self):
self.SellMarket()
self._position_side = None
self._stop_price = None
self._target_price = None
def _exit_short(self):
self.BuyMarket()
self._position_side = None
self._stop_price = None
self._target_price = None
def OnReseted(self):
super(multi_time_frame_trader_strategy, self).OnReseted()
self._position_side = None
self._stop_price = None
self._target_price = None
self._m1_state = None
self._m5_state = None
self._h1_state = None
def CreateClone(self):
return multi_time_frame_trader_strategy()