Открыть на GitHub

Стратегия MAMy Expert

Обзор

  • Порт советника MetaTrader 5 «MAMy Expert» Виктора Чеботарёва на высокоуровневый API StockSharp.
  • Полностью воспроизводит оригинальный индикатор, сравнивающий три скользящие средние по разным типам цен (open, close, weighted).
  • Работает только по завершённым свечам и удерживает не более одной совокупной позиции, полностью повторяя логику исходного эксперта.

База индикаторов

  • Стратегия строит три скользящие средние с одинаковой длиной и методом сглаживания:
    • MA(close) — рассчитывается по ценам закрытия свечей.
    • MA(open) — рассчитывается по ценам открытия свечей.
    • MA(weighted) — рассчитывается по взвешенной цене (High + Low + 2 × Close) / 4.
  • Параметр MaType выбирает алгоритм усреднения (Simple, Exponential, Smoothed, Weighted LWMA), что соответствует значениям MODE_* в MetaTrader.
  • «Буфер закрытия» равен разности MA(close) − MA(weighted).
  • «Буфер открытия» появляется только при сформированном трендовом расположении средних:
    • Нисходящее условие: MA(close) и MA(weighted) падают, закрытие ниже взвешенной средней, обе ниже средней по open, а буфер закрытия уменьшается.
    • Восходящее условие: MA(close) и MA(weighted) растут, закрытие выше взвешенной средней, обе выше средней по open, а буфер закрытия увеличивается.
    • При выполнении одного из условий буфер открытия вычисляется как (MA(weighted) − MA(open)) + (MA(close) − MA(weighted)), иначе он сбрасывается в ноль.
  • Если новая положительная величина буфера открытия совпадает с переходом буфера закрытия в отрицательную область, буфер закрытия принудительно обнуляется — так же, как в исходном коде индикатора.

Логика сигналов

  • Входы
    • Покупка — когда буфер открытия пересекает уровень 0 снизу вверх (предыдущее значение ≤ 0, текущее > 0).
    • Продажа — когда буфер открытия пересекает 0 сверху вниз (предыдущее ≥ 0, текущее < 0).
    • Сигналы входа рассматриваются только при отсутствии открытых позиций.
  • Выходы
    • Закрыть лонг — буфер закрытия пересекает 0 сверху вниз (предыдущее ≥ 0, текущее < 0).
    • Закрыть шорт — буфер закрытия пересекает 0 снизу вверх (предыдущее ≤ 0, текущее > 0).
    • Закрытия обрабатываются раньше входов, поэтому стратегия никогда не держит разнонаправленные позиции одновременно.
  • Заявки выставляются по рынку с объёмом TradeVolume. Вызов StartProtection() обеспечивает защитные механизмы, аналогичные примерам StockSharp.

График и поток данных

  • Подписывается на таймфрейм, заданный CandleType, и анализирует только свечи в состоянии Finished.
  • На графике отображаются свечи цены, все три скользящие средние и сделки, что обеспечивает визуальное сходство с оригинальным индикатором MetaTrader.

Параметры

Имя Тип Значение по умолчанию Описание
CandleType DataType TimeSpan.FromHours(1).TimeFrame() Основной таймфрейм для расчёта индикаторов и сигналов.
MaPeriod int 3 Длина всех трёх скользящих средних.
MaType MaCalculationType Weighted Метод усреднения (Simple, Exponential, Smoothed, Weighted).
TradeVolume decimal 1 Объём каждой рыночной заявки.

Особенности реализации

  • Используется высокоуровневый конвейер SubscribeCandles().Bind(...) и встроенные индикаторы StockSharp; хранится только минимум последних значений, необходимых для сигналов.
  • Сигналы анализируются лишь после формирования всех индикаторов и проверки готовности к торговле (IsFormedAndOnlineAndAllowTrading()).
  • Во время открытой позиции новые входы игнорируются, что соответствует поведению оригинального советника.

namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

public class MamyExpertStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaCalculationTypes> _maType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private DecimalLengthIndicator _closeMa;
	private DecimalLengthIndicator _openMa;
	private DecimalLengthIndicator _weightedPriceMa;

	private decimal? _previousCloseMa;
	private decimal? _previousOpenMa;
	private decimal? _previousWeightedMa;
	private decimal? _previousOpenSignal;
	private decimal? _previousCloseSignal;

	public MamyExpertStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe used for price aggregation.", "General");

		_maPeriod = Param(nameof(MaPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length applied to all moving averages.", "Indicator");

		_maType = Param(nameof(MaType), MaCalculationTypes.Weighted)
			.SetDisplay("MA method", "Averaging algorithm applied to open/close/weighted prices.", "Indicator");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume submitted for entries.", "Trading");
	}

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

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

	public MaCalculationTypes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}

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

		_previousCloseMa = null;
		_previousOpenMa = null;
		_previousWeightedMa = null;
		_previousOpenSignal = null;
		_previousCloseSignal = null;
	}

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

		Volume = TradeVolume;
		StartProtection(null, null);

		_closeMa = CreateMovingAverage(MaType, MaPeriod);
		_openMa = CreateMovingAverage(MaType, MaPeriod);
		_weightedPriceMa = CreateMovingAverage(MaType, MaPeriod);

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _closeMa);
			DrawIndicator(area, _openMa);
			DrawIndicator(area, _weightedPriceMa);
			DrawOwnTrades(area);
		}
	}

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

		if (_closeMa == null || _openMa == null || _weightedPriceMa == null)
			return;

		var closeMaResult = _closeMa.Process(new DecimalIndicatorValue(_closeMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var openMaResult = _openMa.Process(new DecimalIndicatorValue(_openMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
		var weightedPrice = CalculateWeightedPrice(candle);
		var weightedMaResult = _weightedPriceMa.Process(new DecimalIndicatorValue(_weightedPriceMa, weightedPrice, candle.OpenTime) { IsFinal = true });

		if (closeMaResult.IsEmpty || openMaResult.IsEmpty || weightedMaResult.IsEmpty)
			return;

		var closeMaValue = closeMaResult.ToDecimal();
		var openMaValue = openMaResult.ToDecimal();
		var weightedMaValue = weightedMaResult.ToDecimal();

		var previousCloseMa = _previousCloseMa;
		var previousOpenMa = _previousOpenMa;
		var previousWeightedMa = _previousWeightedMa;
		var previousOpenSignal = _previousOpenSignal;
		var previousCloseSignal = _previousCloseSignal;

		_previousCloseMa = closeMaValue;
		_previousOpenMa = openMaValue;
		_previousWeightedMa = weightedMaValue;

		if (!_closeMa.IsFormed || !_openMa.IsFormed || !_weightedPriceMa.IsFormed)
		{
			_previousOpenSignal = null;
			_previousCloseSignal = null;
			return;
		}

		var closeSignal = closeMaValue - weightedMaValue;
		var openSignal = 0m;

		if (previousCloseMa.HasValue && previousOpenMa.HasValue && previousWeightedMa.HasValue && previousCloseSignal.HasValue)
		{
			var closeDecreasing = closeMaValue < previousCloseMa.Value &&
				weightedMaValue < previousWeightedMa.Value &&
				closeMaValue < weightedMaValue &&
				weightedMaValue < openMaValue &&
				previousWeightedMa.Value < previousOpenMa.Value &&
				closeSignal <= previousCloseSignal.Value;

			var closeIncreasing = closeMaValue > previousCloseMa.Value &&
				weightedMaValue > previousWeightedMa.Value &&
				closeMaValue > weightedMaValue &&
				weightedMaValue > openMaValue &&
				previousWeightedMa.Value > previousOpenMa.Value &&
				closeSignal >= previousCloseSignal.Value;

			if (closeDecreasing || closeIncreasing)
				openSignal = (weightedMaValue - openMaValue) + (closeMaValue - weightedMaValue);
		}

		if (previousOpenSignal.HasValue && previousCloseSignal.HasValue &&
			openSignal >= 0m &&
			openSignal > previousOpenSignal.Value &&
			closeSignal < 0m &&
			previousCloseSignal.Value >= 0m)
		{
			closeSignal = 0m;
		}

		var hasPreviousOpenSignal = previousOpenSignal.HasValue;
		var hasPreviousCloseSignal = previousCloseSignal.HasValue;

		var openBuy = hasPreviousOpenSignal && openSignal > 0m && previousOpenSignal.Value <= 0m;
		var openSell = hasPreviousOpenSignal && openSignal < 0m && previousOpenSignal.Value >= 0m;
		var closeBuy = hasPreviousCloseSignal && closeSignal < 0m && previousCloseSignal.Value >= 0m;
		var closeSell = hasPreviousCloseSignal && closeSignal > 0m && previousCloseSignal.Value <= 0m;

		_previousOpenSignal = openSignal;
		_previousCloseSignal = closeSignal;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (TradeVolume <= 0m)
			return;

		var longPosition = Position > 0m ? Position : 0m;
		var shortPosition = Position < 0m ? -Position : 0m;

		if (longPosition > 0m)
		{
			if (closeBuy)
				SellMarket(longPosition);
		}
		else if (shortPosition > 0m)
		{
			if (closeSell)
				BuyMarket(shortPosition);
		}
		else
		{
			if (openBuy)
				BuyMarket(TradeVolume);
			else if (openSell)
				SellMarket(TradeVolume);
		}
	}

	private static decimal CalculateWeightedPrice(ICandleMessage candle)
	{
		return (candle.HighPrice + candle.LowPrice + candle.ClosePrice * 2m) / 4m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaCalculationTypes type, int length)
	{
		return type switch
		{
			MaCalculationTypes.Simple => new SMA { Length = length },
			MaCalculationTypes.Exponential => new EMA { Length = length },
			MaCalculationTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			_ => new WeightedMovingAverage { Length = length },
		};
	}

	public enum MaCalculationTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}
}