Открыть на GitHub

Стратегия MA Cross Method PriceMode

Обзор

MA Cross Method PriceMode — порт на StockSharp эксперта MetaTrader 4 «MA_cross_Method_PriceMode». Стратегия отслеживает две настраиваемые скользящие средние и реагирует каждый раз, когда быстрая средняя пересекает медленную. Обе линии полностью повторяют входные параметры MT4: период, метод сглаживания (SMA, EMA, SMMA, LWMA), тип цены (close, open, high, low, median, typical, weighted) и горизонтальный сдвиг. Стратегию можно применять к любому инструменту, который предоставляет стандартные свечи по времени.

Индикаторы

  • Быстрая скользящая средняя — настраиваемый период, метод и тип цены. Горизонтальный сдвиг MetaTrader реализован через буфер завершённых значений: стратегия хранит последние FirstShift + 2 значений и возвращает значение FirstShift баров назад.
  • Медленная скользящая средняя — аналогичные параметры и тот же механизм буфера для сдвига.

Логика торговли

  1. Стратегия подписывается на выбранный тип свечей и работает только с завершёнными барами, что исключает перерисовку на незакрытых свечах.
  2. На каждой закрытой свече соответствующие цены подаются в обе скользящие средние.
  3. После получения финальных значений рассчитываются два условия:
    • Бычий крест — быстрая средняя была ниже или равна медленной на предыдущем баре и поднимается выше на текущем баре.
    • Медвежий крест — быстрая средняя была выше или равна медленной на предыдущем баре и опускается ниже на текущем баре.
  4. При бычьем кресте отправляется рыночная покупка объёмом OrderVolume. Если открыта короткая позиция, объём автоматически увеличивается, чтобы закрыть шорт и открыть лонг одним ордером.
  5. При медвежьем кресте отправляется рыночная продажа объёмом OrderVolume. При наличии длинной позиции объём увеличивается так, чтобы закрыть лонг и открыть шорт.
  6. Вызов StartProtection() позволяет дополнительно подключить стандартные защитные модули StockSharp (стоп-лосс, безубыток и т. п.).

Параметры

Название Описание Значение по умолчанию
FirstPeriod Период быстрой скользящей средней. 3
SecondPeriod Период медленной скользящей средней. 13
FirstMethod Метод сглаживания быстрой средней (Simple, Exponential, Smoothed, LinearWeighted). Simple
SecondMethod Метод сглаживания медленной средней. LinearWeighted
FirstPriceMode Тип цены для быстрой средней (Close, Open, High, Low, Median, Typical, Weighted). Close
SecondPriceMode Тип цены для медленной средней. Median
FirstShift Горизонтальный сдвиг (в барах) для быстрой средней. 0
SecondShift Горизонтальный сдвиг для медленной средней. 0
OrderVolume Базовый объём заявки при открытии позиции. 0.1
CandleType Тип/таймфрейм свечей, обрабатываемых стратегией. Свечи 5 минут

Отличия от версии MQL

  • Цикл по ордерам MetaTrader (OrdersTotal, OrderSelect, OrderClose) заменён обращением к свойству Strategy.Position и отправкой рыночных ордеров подходящего объёма для реверса позиции.
  • Флаг «новый бар» больше не требуется: ProcessCandle вызывается ровно один раз на завершённую свечу, что естественным образом повторяет поведение «один сигнал на бар» без опроса по тикам.
  • Сдвиг скользящих реализован компактными буферами последнего shift + 2 значения, что повторяет визуальное смещение MetaTrader без использования запрещённых методов обратного доступа к индикаторам (GetValue).
  • Стратегия не задаёт фиксированных стопов и тейков при отправке ордеров; для защиты можно подключать стандартные блоки StockSharp через StartProtection().

Рекомендации по использованию

  • Выбирайте таймфрейм, соответствующий исходной торговой системе (например, M5 или H1). При необходимости параметр CandleType можно изменить на другой интервал.
  • Положительные значения FirstShift или SecondShift смещают момент входа на указанное количество закрытых баров — аналогично параметру Shift в MetaTrader.
  • Режим цены Weighted повторяет формулу MetaTrader (High + Low + 2 * Close) / 4; Median и Typical соответствуют (High + Low) / 2 и (High + Low + Close) / 3.
  • Все сделки исполняются рыночными ордерами, поэтому заранее убедитесь, что торговый счёт допускает нужный объём и возможный проскальзывание.
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>
/// Moving average crossover strategy converted from the MetaTrader script "MA_cross_Method_PriceMode".
/// Allows selecting the smoothing method, applied price and horizontal shift for each average.
/// </summary>
public class MaCrossMethodPriceModeStrategy : Strategy
{
	private readonly StrategyParam<int> _firstPeriod;
	private readonly StrategyParam<int> _secondPeriod;
	private readonly StrategyParam<MaMethods> _firstMethod;
	private readonly StrategyParam<MaMethods> _secondMethod;
	private readonly StrategyParam<AppliedPriceModes> _firstPriceMode;
	private readonly StrategyParam<AppliedPriceModes> _secondPriceMode;
	private readonly StrategyParam<int> _firstShift;
	private readonly StrategyParam<int> _secondShift;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _firstMa = null!;
	private DecimalLengthIndicator _secondMa = null!;

	private readonly List<decimal> _firstValues = new();
	private readonly List<decimal> _secondValues = new();

	/// <summary>
	/// Initializes a new instance of <see cref="MaCrossMethodPriceModeStrategy"/>.
	/// </summary>
	public MaCrossMethodPriceModeStrategy()
	{
		_firstPeriod = Param(nameof(FirstPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the first moving average.", "Indicators")
			
			.SetOptimize(2, 50, 1);

		_secondPeriod = Param(nameof(SecondPeriod), 13)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the second moving average.", "Indicators")
			
			.SetOptimize(5, 100, 1);

		_firstMethod = Param(nameof(FirstMethod), MaMethods.Simple)
			.SetDisplay("Fast MA Method", "Smoothing method applied to the first moving average.", "Indicators")
			;

		_secondMethod = Param(nameof(SecondMethod), MaMethods.LinearWeighted)
			.SetDisplay("Slow MA Method", "Smoothing method applied to the second moving average.", "Indicators")
			;

		_firstPriceMode = Param(nameof(FirstPriceMode), AppliedPriceModes.Close)
			.SetDisplay("Fast MA Price", "Price source used for the first moving average.", "Indicators")
			;

		_secondPriceMode = Param(nameof(SecondPriceMode), AppliedPriceModes.Median)
			.SetDisplay("Slow MA Price", "Price source used for the second moving average.", "Indicators")
			;

		_firstShift = Param(nameof(FirstShift), 0)
			.SetNotNegative()
			.SetDisplay("Fast MA Shift", "Horizontal shift (in bars) applied to the first moving average.", "Indicators");

		_secondShift = Param(nameof(SecondShift), 0)
			.SetNotNegative()
			.SetDisplay("Slow MA Shift", "Horizontal shift (in bars) applied to the second moving average.", "Indicators");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Base order volume used for new entries.", "Trading")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for price processing.", "General");
	}

	/// <summary>
	/// Period of the first moving average.
	/// </summary>
	public int FirstPeriod
	{
		get => _firstPeriod.Value;
		set => _firstPeriod.Value = value;
	}

	/// <summary>
	/// Period of the second moving average.
	/// </summary>
	public int SecondPeriod
	{
		get => _secondPeriod.Value;
		set => _secondPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method applied to the first moving average.
	/// </summary>
	public MaMethods FirstMethod
	{
		get => _firstMethod.Value;
		set => _firstMethod.Value = value;
	}

	/// <summary>
	/// Smoothing method applied to the second moving average.
	/// </summary>
	public MaMethods SecondMethod
	{
		get => _secondMethod.Value;
		set => _secondMethod.Value = value;
	}

	/// <summary>
	/// Applied price mode for the first moving average.
	/// </summary>
	public AppliedPriceModes FirstPriceMode
	{
		get => _firstPriceMode.Value;
		set => _firstPriceMode.Value = value;
	}

	/// <summary>
	/// Applied price mode for the second moving average.
	/// </summary>
	public AppliedPriceModes SecondPriceMode
	{
		get => _secondPriceMode.Value;
		set => _secondPriceMode.Value = value;
	}

	/// <summary>
	/// Shift (in bars) applied to the first moving average values.
	/// </summary>
	public int FirstShift
	{
		get => _firstShift.Value;
		set => _firstShift.Value = value;
	}

	/// <summary>
	/// Shift (in bars) applied to the second moving average values.
	/// </summary>
	public int SecondShift
	{
		get => _secondShift.Value;
		set => _secondShift.Value = value;
	}

	/// <summary>
	/// Base order volume used for new positions.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type (timeframe) processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_firstMa = null!;
		_secondMa = null!;
		_firstValues.Clear();
		_secondValues.Clear();
	}

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

		_firstMa = CreateMovingAverage(FirstMethod, FirstPeriod);
		_secondMa = CreateMovingAverage(SecondMethod, SecondPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_firstMa, _secondMa, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _firstMa);
			DrawIndicator(area, _secondMa);
			DrawOwnTrades(area);
		}

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal firstDecimal, decimal secondDecimal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateBuffer(_firstValues, firstDecimal, FirstShift);
		UpdateBuffer(_secondValues, secondDecimal, SecondShift);

		if (!TryGetShiftedValues(_firstValues, FirstShift, out var firstCurrent, out var firstPrevious))
			return;

		if (!TryGetShiftedValues(_secondValues, SecondShift, out var secondCurrent, out _))
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var bullishCross = IsBullishCross(firstPrevious, firstCurrent, secondCurrent);
		var bearishCross = IsBearishCross(firstPrevious, firstCurrent, secondCurrent);

		if (bullishCross && OrderVolume > 0m && Position <= 0m)
		{
			var volumeToBuy = OrderVolume + (Position < 0m ? Math.Abs(Position) : 0m);
			BuyMarket(volumeToBuy);
		}
		else if (bearishCross && OrderVolume > 0m && Position >= 0m)
		{
			var volumeToSell = OrderVolume + (Position > 0m ? Position : 0m);
			SellMarket(volumeToSell);
		}
	}

	private static void UpdateBuffer(List<decimal> buffer, decimal value, int shift)
	{
		buffer.Add(value);

		var maxCount = Math.Max(shift + 2, 2);
		while (buffer.Count > maxCount)
		{
			buffer.RemoveAt(0);
		}
	}

	private static bool TryGetShiftedValues(IReadOnlyList<decimal> buffer, int shift, out decimal current, out decimal previous)
	{
		var currentIndex = buffer.Count - 1 - shift;
		var previousIndex = buffer.Count - 2 - shift;

		if (previousIndex < 0 || currentIndex < 0 || currentIndex >= buffer.Count)
		{
			current = default;
			previous = default;
			return false;
		}

		current = buffer[currentIndex];
		previous = buffer[previousIndex];
		return true;
	}

	private static bool IsBullishCross(decimal previousFast, decimal currentFast, decimal currentSlow)
	{
		return (previousFast <= currentSlow && currentFast > currentSlow)
			|| (previousFast < currentSlow && currentFast >= currentSlow);
	}

	private static bool IsBearishCross(decimal previousFast, decimal currentFast, decimal currentSlow)
	{
		return (previousFast >= currentSlow && currentFast < currentSlow)
			|| (previousFast > currentSlow && currentFast <= currentSlow);
	}

	private static decimal SelectPrice(ICandleMessage candle, AppliedPriceModes mode)
	{
		return mode switch
		{
			AppliedPriceModes.Close => candle.ClosePrice,
			AppliedPriceModes.Open => candle.OpenPrice,
			AppliedPriceModes.High => candle.HighPrice,
			AppliedPriceModes.Low => candle.LowPrice,
			AppliedPriceModes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceModes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceModes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			_ => candle.ClosePrice
		};
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SMA { Length = period },
			MaMethods.Exponential => new EMA { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SMA { Length = period }
		};
	}

	/// <summary>
	/// Moving average smoothing methods that mirror the MetaTrader inputs.
	/// </summary>
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Applied price options equivalent to the MetaTrader constants.
	/// </summary>
	public enum AppliedPriceModes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
}