Открыть на GitHub

Стратегия Adjustable Moving Average

Стратегия переносит на StockSharp эксперт-советник MetaTrader «Adjustable Moving Average». Используются две скользящие средней одного типа, но с разными периодами. Когда быстрая средняя пересекает медленную с зазором не меньше заданного, стратегия закрывает встречную позицию и при необходимости открывает новую по направлению тренда. Сессионные фильтры, защитные уровни и опциональный трейлинг-стоп повторяют гибкость исходного робота.

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

  • Быстрая и медленная средние рассчитываются одним алгоритмом. Меньший период автоматически назначается быстрой линии, больший — медленной.
  • Сигнал появляется только после формирования обеих средних и когда их расстояние превышает MinGapPoints, переведённый в цену через шаг инструмента.
  • Если быстрая средняя выше медленной на нужный зазор, внутренняя переменная принимает значение «покупка»; обратная ситуация переключает её в «продажу».
  • При смене состояния закрывается текущая позиция (если включён CloseOutsideSession или время попадает в торговую сессию). Новые сделки подчиняются режиму Mode (только покупки, только продажи или оба направления) и выбранной модели объёма.
  • На каждой завершённой свече проверяются защитные условия:
    • Стоп-лосс и тейк-профит измеряются в пунктах инструмента и сравниваются с диапазоном свечи.
    • Трейлинг-стоп активируется после движения цены в прибыль минимум на TrailStopPoints пунктов. Обновление уровня выполняется только в торговой сессии или если разрешён TrailOutsideSession. После активации стоп остаётся действовать даже вне сессии.

Размер позиции

  • При EnableAutoLot = false используется объём FixedLot, подгоняемый под шаг и границы объёма инструмента.
  • При EnableAutoLot = true объём приближённо вычисляется по формуле (PortfolioValue / 10 000) * LotPer10kFreeMargin и округляется до одной десятой лота, после чего также корректируется по ограничениям инструмента.

Параметры

Имя Тип / Значение по умолчанию Описание
CandleType TimeFrame = 5 минут Таймфрейм, на котором строятся скользящие средние.
FastPeriod int = 3 Период быстрой средней (должен отличаться от SlowPeriod).
SlowPeriod int = 9 Период медленной средней (должен отличаться от FastPeriod).
MaMethod MovingAverageMethod = Exponential Тип средней: Simple, Exponential, Smoothed, Weighted.
MinGapPoints decimal = 3 Минимальный зазор между средними в пунктах инструмента. Переводится через шаг цены.
StopLossPoints decimal = 0 Размер стоп-лосса в пунктах. 0 — отключен.
TakeProfitPoints decimal = 0 Размер тейк-профита в пунктах. 0 — отключен.
TrailStopPoints decimal = 0 Расстояние трейлинг-стопа в пунктах. 0 — отключен.
Mode EntryMode = Both Разрешённые направления (Both, BuyOnly, SellOnly).
SessionStart TimeSpan = 00:00 Время начала торговой сессии (по времени площадки).
SessionEnd TimeSpan = 23:59 Время окончания сессии. Если SessionEnd < SessionStart, поддерживаются ночные сессии.
CloseOutsideSession bool = true При true встречные позиции закрываются даже вне сессии.
TrailOutsideSession bool = true При true трейлинг продолжает подтягиваться после завершения сессии.
FixedLot decimal = 0.1 Объём сделки при отключённом автолоте.
EnableAutoLot bool = false Включает расчёт объёма по стоимости портфеля.
LotPer10kFreeMargin decimal = 1 Количество лотов на каждые 10 000 единиц капитала при автолоте.
MaxSlippage int = 3 Параметр сохранён для совместимости; в StockSharp нельзя задать проскальзывание для рыночных ордеров.
TradeComment string = "AdjustableMovingAverageEA" Текст, выводимый в лог при совершении сделок.

Особенности

  • В MetaTrader стопы и трейлинг выставлялись как модификации ордеров. В StockSharp поведение повторено через проверку диапазонов свечей и обратные рыночные сделки.
  • Показатель AccountFreeMargin() недоступен, поэтому используется стоимость портфеля (Portfolio.CurrentValue с запасным вариантом BeginValue).
  • Если у инструмента не задан шаг цены (PriceStep), расчёты в пунктах (зазор, стопы, трейлинг) не выполняются.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Moving average crossover strategy with adjustable gap, session control, and optional trailing stop.
/// </summary>
public class AdjustableMovingAverageStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<MovingAverageMethods> _maMethod;
	private readonly StrategyParam<decimal> _minGapPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingPoints;
	private readonly StrategyParam<EntryModes> _entryMode;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<bool> _closeOutsideSession;
	private readonly StrategyParam<bool> _trailOutsideSession;
	private readonly StrategyParam<decimal> _fixedLot;
	private readonly StrategyParam<bool> _enableAutoLot;
	private readonly StrategyParam<decimal> _lotPer10k;
	private readonly StrategyParam<int> _maxSlippage;
	private readonly StrategyParam<string> _tradeComment;

	private DecimalLengthIndicator _fastMa;
	private DecimalLengthIndicator _slowMa;
	private decimal _pointValue;
	private decimal _minGapThreshold;
	private int _previousSignal;
	private bool _hasInitialSignal;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AdjustableMovingAverageStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle timeframe", "Timeframe used to build moving averages", "General")
			;

		_fastPeriod = Param(nameof(FastPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast period", "Short moving average length", "Moving averages")
			
			.SetOptimize(2, 30, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Slow period", "Long moving average length", "Moving averages")
			
			.SetOptimize(3, 60, 1);

		_maMethod = Param(nameof(MaMethod), MovingAverageMethods.Exponential)
			.SetDisplay("MA method", "Moving average calculation method", "Moving averages")
			;

		_minGapPoints = Param(nameof(MinGapPoints), 3m)
			.SetNotNegative()
			.SetDisplay("Minimum gap (points)", "Required distance between fast and slow MAs before signalling", "Trading")
			
			.SetOptimize(0m, 20m, 1m);

		_stopLossPoints = Param(nameof(StopLossPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Profit target distance in price points", "Risk management");

		_trailingPoints = Param(nameof(TrailStopPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Trailing stop (points)", "Trailing stop distance in price points", "Risk management");

		_entryMode = Param(nameof(Mode), EntryModes.Both)
			.SetDisplay("Entry mode", "Allowed trade direction", "Trading");

		_sessionStart = Param(nameof(SessionStart), TimeSpan.Zero)
			.SetDisplay("Session start", "Trading session start time (platform time)", "Session");

		_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 59, 0))
			.SetDisplay("Session end", "Trading session end time (platform time)", "Session");

		_closeOutsideSession = Param(nameof(CloseOutsideSession), true)
			.SetDisplay("Close outside session", "Allow closing positions when the session filter is inactive", "Session");

		_trailOutsideSession = Param(nameof(TrailOutsideSession), true)
			.SetDisplay("Trail outside session", "Continue trailing even when trading session is closed", "Session");

		_fixedLot = Param(nameof(FixedLot), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Fixed lot", "Volume used when auto lot sizing is disabled", "Money management");

		_enableAutoLot = Param(nameof(EnableAutoLot), false)
			.SetDisplay("Enable auto lot", "Approximate AccountFreeMargin based sizing", "Money management");

		_lotPer10k = Param(nameof(LotPer10kFreeMargin), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lots per 10k", "Lots per 10,000 of account value when auto lot is enabled", "Money management");

		_maxSlippage = Param(nameof(MaxSlippage), 3)
			.SetNotNegative()
			.SetDisplay("Max slippage", "Placeholder parameter retained from the MQL version", "Trading");

		_tradeComment = Param(nameof(TradeComment), "AdjustableMovingAverageEA")
			.SetDisplay("Trade comment", "Tag applied to diagnostic messages", "General");
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Fast moving average length.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average length.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Minimum distance between fast and slow moving averages in instrument points.
	/// </summary>
	public decimal MinGapPoints
	{
		get => _minGapPoints.Value;
		set => _minGapPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in instrument points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in instrument points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in instrument points.
	/// </summary>
	public decimal TrailStopPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Allowed trade direction.
	/// </summary>
	public EntryModes Mode
	{
		get => _entryMode.Value;
		set => _entryMode.Value = value;
	}

	/// <summary>
	/// Session start time in platform time zone.
	/// </summary>
	public TimeSpan SessionStart
	{
		get => _sessionStart.Value;
		set => _sessionStart.Value = value;
	}

	/// <summary>
	/// Session end time in platform time zone.
	/// </summary>
	public TimeSpan SessionEnd
	{
		get => _sessionEnd.Value;
		set => _sessionEnd.Value = value;
	}

	/// <summary>
	/// Close positions even when the session filter is inactive.
	/// </summary>
	public bool CloseOutsideSession
	{
		get => _closeOutsideSession.Value;
		set => _closeOutsideSession.Value = value;
	}

	/// <summary>
	/// Continue updating the trailing stop outside the session window.
	/// </summary>
	public bool TrailOutsideSession
	{
		get => _trailOutsideSession.Value;
		set => _trailOutsideSession.Value = value;
	}

	/// <summary>
	/// Fixed order volume used when auto lot sizing is disabled.
	/// </summary>
	public decimal FixedLot
	{
		get => _fixedLot.Value;
		set => _fixedLot.Value = value;
	}

	/// <summary>
	/// Toggle automatic lot sizing based on approximate free margin.
	/// </summary>
	public bool EnableAutoLot
	{
		get => _enableAutoLot.Value;
		set => _enableAutoLot.Value = value;
	}

	/// <summary>
	/// Lots allocated per 10,000 units of portfolio value.
	/// </summary>
	public decimal LotPer10kFreeMargin
	{
		get => _lotPer10k.Value;
		set => _lotPer10k.Value = value;
	}

	/// <summary>
	/// Placeholder for the original slippage tolerance.
	/// </summary>
	public int MaxSlippage
	{
		get => _maxSlippage.Value;
		set => _maxSlippage.Value = value;
	}

	/// <summary>
	/// Comment attached to log messages when orders are placed.
	/// </summary>
	public string TradeComment
	{
		get => _tradeComment.Value;
		set => _tradeComment.Value = value;
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_pointValue = 0m;
		_minGapThreshold = 0m;
		_previousSignal = 0;
		_hasInitialSignal = false;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

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

		var fastLength = Math.Min(FastPeriod, SlowPeriod);
		var slowLength = Math.Max(FastPeriod, SlowPeriod);

		if (fastLength == slowLength)
		{
			LogWarning("Fast and slow periods must differ.");
			Stop();
			return;
		}

		_fastMa = CreateMovingAverage(MaMethod, fastLength);
		_slowMa = CreateMovingAverage(MaMethod, slowLength);

		_pointValue = CalculatePointValue();
		_minGapThreshold = MinGapPoints * _pointValue;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, _slowMa, ProcessCandle)
			.Start();
	}

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

		var inSession = InSession(candle.OpenTime);
		var allowTrading = inSession && true;

		UpdateTrailing(candle, inSession || TrailOutsideSession);
		HandleProtectiveExits(candle);

		if (_fastMa == null || _slowMa == null)
			return;

		if (!_fastMa.IsFormed || !_slowMa.IsFormed)
			return;

		var gapUp = fast - slow;
		var gapDown = slow - fast;

		if (!_hasInitialSignal)
		{
			if (gapUp >= _minGapThreshold)
			{
				_previousSignal = 1;
				_hasInitialSignal = true;
			}
			else if (gapDown >= _minGapThreshold)
			{
				_previousSignal = -1;
				_hasInitialSignal = true;
			}
			return;
		}

		if (_previousSignal > 0)
		{
			if (gapDown >= _minGapThreshold)
			{
				if (CloseOutsideSession || inSession)
					CloseCurrentPosition();

				if (allowTrading && Mode != EntryModes.BuyOnly)
				{
					OpenShort(candle.ClosePrice);
				}

				_previousSignal = -1;
				ResetTrailing();
			}
		}
		else if (_previousSignal < 0)
		{
			if (gapUp >= _minGapThreshold)
			{
				if (CloseOutsideSession || inSession)
					CloseCurrentPosition();

				if (allowTrading && Mode != EntryModes.SellOnly)
				{
					OpenLong(candle.ClosePrice);
				}

				_previousSignal = 1;
				ResetTrailing();
			}
		}
	}

	private void OpenLong(decimal price)
	{
		var volume = CalculateOrderVolume(price);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		LogInfo($"{TradeComment}: opened long, volume={volume:0.###}");
	}

	private void OpenShort(decimal price)
	{
		var volume = CalculateOrderVolume(price);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		LogInfo($"{TradeComment}: opened short, volume={volume:0.###}");
	}

	private void CloseCurrentPosition()
	{
		if (Position > 0m)
		{
			SellMarket(Position);
			LogInfo($"{TradeComment}: closed existing long");
		}
		else if (Position < 0m)
		{
			BuyMarket(-Position);
			LogInfo($"{TradeComment}: closed existing short");
		}
	}

	private void UpdateTrailing(ICandleMessage candle, bool allowUpdate)
	{
		if (TrailStopPoints <= 0m || _pointValue <= 0m)
			return;

		var distance = TrailStopPoints * _pointValue;

		if (Position > 0m)
		{
			if (allowUpdate)
			{
				var move = candle.ClosePrice - 0m;
				if (move >= distance)
				{
					var newStop = candle.ClosePrice - distance;
					if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
						_longTrailingStop = newStop;
				}
			}

			if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: trailing stop hit (long)");
				ResetTrailing();
			}
		}
		else if (Position < 0m)
		{
			var absPosition = -Position;

			if (allowUpdate)
			{
				var move = 0m - candle.ClosePrice;
				if (move >= distance)
				{
					var newStop = candle.ClosePrice + distance;
					if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
						_shortTrailingStop = newStop;
				}
			}

			if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: trailing stop hit (short)");
				ResetTrailing();
			}
		}
		else
		{
			ResetTrailing();
		}
	}

	private void HandleProtectiveExits(ICandleMessage candle)
	{
		if (_pointValue <= 0m)
			return;

		if (Position > 0m)
		{
			var stop = StopLossPoints > 0m ? 0m - StopLossPoints * _pointValue : (decimal?)null;
			var target = TakeProfitPoints > 0m ? 0m + TakeProfitPoints * _pointValue : (decimal?)null;

			if (stop.HasValue && candle.LowPrice <= stop.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: stop-loss hit (long)");
				ResetTrailing();
				return;
			}

			if (target.HasValue && candle.HighPrice >= target.Value)
			{
				SellMarket(Position);
				LogInfo($"{TradeComment}: take-profit hit (long)");
				ResetTrailing();
			}
		}
		else if (Position < 0m)
		{
			var absPosition = -Position;
			var stop = StopLossPoints > 0m ? 0m + StopLossPoints * _pointValue : (decimal?)null;
			var target = TakeProfitPoints > 0m ? 0m - TakeProfitPoints * _pointValue : (decimal?)null;

			if (stop.HasValue && candle.HighPrice >= stop.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: stop-loss hit (short)");
				ResetTrailing();
				return;
			}

			if (target.HasValue && candle.LowPrice <= target.Value)
			{
				BuyMarket(absPosition);
				LogInfo($"{TradeComment}: take-profit hit (short)");
				ResetTrailing();
			}
		}
		else
		{
			ResetTrailing();
		}
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		var desired = FixedLot;

		if (EnableAutoLot)
		{
			var equity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
			if (equity is decimal value && value > 0m && price > 0m)
			{
				var lots = Math.Round((value / 10000m) * LotPer10kFreeMargin, 1, MidpointRounding.AwayFromZero);
				if (lots > 0m)
					desired = lots;
			}
		}

		var adjusted = AdjustVolume(desired);
		return adjusted > 0m ? adjusted : 0m;
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 1m;
		if (step > 0m)
		{
			var steps = Math.Max(1m, Math.Round(volume / step, 0, MidpointRounding.AwayFromZero));
			volume = steps * step;
		}

		var minVolume = security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = security.MaxVolume ?? decimal.MaxValue;
		if (volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private bool InSession(DateTimeOffset time)
	{
		var start = SessionStart;
		var end = SessionEnd;
		var current = time.TimeOfDay;

		if (end < start)
		{
			return current >= start || current <= end;
		}

		return current >= start && current <= end;
	}

	private decimal CalculatePointValue()
	{
		var step = Security?.PriceStep ?? Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

		var point = step;
		if (point == 0.00001m || point == 0.001m)
			point *= 10m;

		return point;
	}

	private DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		DecimalLengthIndicator indicator = method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = length },
			_ => new ExponentialMovingAverage { Length = length }
		};

		return indicator;
	}

	private void ResetTrailing()
	{
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Weighted
	}

	/// <summary>
	/// Directional filter for new positions.
	/// </summary>
	public enum EntryModes
	{
		/// <summary>
		/// Allow both long and short entries.
		/// </summary>
		Both,

		/// <summary>
		/// Allow only long entries.
		/// </summary>
		BuyOnly,

		/// <summary>
		/// Allow only short entries.
		/// </summary>
		SellOnly
	}
}