Открыть на GitHub

Стратегия ARD Order Management Command

Обзор стратегии

ARD Order Management — это перенос эксперта MetaTrader 4 ARD_ORDER_MANAGEMENT_.mq4 на высокоуровневый фреймворк StockSharp. Оригинальный советник реагировал на четыре ручные команды (покупка, продажа, закрытие и модификация), которые можно было вызывать из панели управления. Каждая команда заново рассчитывала объём позиции от доступной маржи, открывала или переворачивала рыночную позицию и проставляла фиксированные стоп-лосс и тейк-профит.

В версии для StockSharp управление осталось прежним: достаточно задать значение параметру Command. Как только оно отличится от None, стратегия выполнит требуемое действие при следующем обновлении котировок Level 1 и автоматически вернёт параметр к None. Защитные заявки пересоздаются при каждом входе и при модификации, поэтому расстояния стопа и тейка всегда соответствуют текущим настройкам.

Жизненный цикл команд

  1. Инициация – при Command = Buy или Command = Sell стратегия запоминает запрос и сразу вызывает ClosePosition(), чтобы закрыть все активные сделки. Перед этим отменяются действующие защитные заявки, что повторяет циклы OrderClose из исходного кода MQL.
  2. Расчёт объёма – перед каждым действием объём пересчитывается. Используется Portfolio.CurrentValue (при отсутствии – Portfolio.BeginValue), делится на LotSizeDivisor и умножается на 1/1000, что полностью повторяет выражение AccountFreeMargin()/lotsize/1000. Полученное значение округляется до LotDecimals и при необходимости повышается до MinimumVolume.
  3. Ожидание плоской позиции – если на момент команды позиция уже открыта, новый вход откладывается до тех пор, пока Position не станет равной нулю. Проверка выполняется на каждом тике Level 1, чтобы не конкурировать с асинхронным исполнением заявок.
  4. Рыночное исполнение – после выхода в ноль отправляется BuyMarket или SellMarket. Последние значения бид/аск сохраняются и используются при расчёте уровней стопа и тейка.
  5. Постановка защиты – стоп-лосс и тейк-профит отправляются как отдельные stop/limit-заявки. Для длинной позиции стоп ставится по формуле bid − StopLossPoints * PriceStep, а тейк — ask + TakeProfitPoints * PriceStep. Для короткой позиции формулы зеркальны. Команда Modify использует параметры ModifyStopLossPoints и ModifyTakeProfitPoints.
  6. Команда Close – при Command = Close отменяются все защитные заявки и вызывается ClosePosition(). Если позиция уже закрыта, в журнал записывается информационное сообщение без дополнительных действий.

Управление капиталом

  • Пропорциональный объём – объём сделки зависит от текущей стоимости портфеля и автоматически уменьшается или увеличивается вместе со счётом. Если параметр делителя случайно станет нулём, стратегия выводит предупреждение и использует MinimumVolume.
  • Округление – параметр LotDecimals задаёт точность округления. Применяется Math.Round с режимом MidpointRounding.AwayFromZero, что аналогично функции NormalizeDouble в MetaTrader.
  • Минимальный лот – после округления значение сравнивается с MinimumVolume. Это воспроизводит правило оригинала, где объём меньше lotmax заменялся на 0.1.

Стоп-лосс и тейк-профит

  • При каждом входе или модификации защитные заявки пересоздаются с нуля. Все предыдущие stop/limit-заявки отменяются перед выставлением новых.
  • Перед вычислением цен проверяется Security.PriceStep. Если шаг цены неизвестен или равен нулю, защитные заявки не отправляются, а в лог пишется предупреждение.
  • Команда Modify перестраивает защиту по новым расстояниям, но не изменяет текущий размер позиции.

Данные и исполнение

  • Подписка ведётся только на Level 1 (SubscribeLevel1()), что соответствует тиковой логике MetaTrader с полями Bid и Ask.
  • Свечи и индикаторы не требуются — все расчёты выполняются на уровне котировок.
  • Для заявок используются высокоуровневые методы BuyMarket, SellMarket, BuyStop, SellStop, BuyLimit, SellLimit, CancelOrder, что соответствует рекомендациям из AGENTS.md.

Параметры

Имя Тип Значение по умолчанию Описание
SlippageSteps int 4 Допустимая просадка цены в шагах. Сохранена для совместимости, т.к. рыночные заявки StockSharp исполняются без отдельного параметра проскальзывания.
LotDecimals int 1 Количество знаков после запятой при округлении объёма.
StopLossPoints decimal 50 Расстояние в пунктах от входа до первоначального стоп-лосса.
TakeProfitPoints decimal 100 Расстояние в пунктах от входа до первоначального тейк-профита.
LotSizeDivisor decimal 5 Делитель, применяемый к стоимости портфеля перед переводом в лоты (freeMargin / divisor / 1000).
ModifyStopLossPoints decimal 20 Расстояние стопа, используемое при команде Modify.
ModifyTakeProfitPoints decimal 100 Расстояние тейка, используемое при команде Modify.
MinimumVolume decimal 0.1 Минимально допустимый объём после округления.
OrderComment string "Placing Order" Комментарий, записываемый в каждую заявку для удобства учёта.
Command ArdOrderCommand None Исполняемая команда. После выполнения автоматически возвращается к None.

Практические рекомендации

  • Изменяйте Command через интерфейс или программно. Чтобы повторить команду, сначала верните значение в None, затем снова выберите нужное действие.
  • Так как стоп и тейк отправляются как отдельные заявки, брокер/биржа должны поддерживать соответствующие типы заявок. При отсутствии такой возможности используйте синтетические выходы в коде.
  • Параметр SlippageSteps носит информационный характер и отражает исходную логику MetaTrader. В StockSharp он не влияет на исполнение рыночных заявок.
  • Вся ключевая активность протоколируется через LogInfo/LogWarn, что упрощает аудит ручных операций.

Отличия от оригинального эксперта

  • В MetaTrader стоп и тейк прикреплялись к ордеру. В StockSharp они реализованы отдельными stop/limit-заявками.
  • Благодаря асинхронной модели StockSharp новый вход откладывается до фактического закрытия старой позиции, что предотвращает наложение заявок.
  • Вместо AccountFreeMargin используется информация портфеля. Убедитесь, что адаптер заполняет CurrentValue, либо заранее установите BeginValue.
  • Ошибки исполнения обрабатываются через систему логирования, а не через бесконечные циклы повторной отправки, как в MQL.

Советы по тестированию

  • Проверьте стратегию в тестовом режиме с котировками Level 1 и убедитесь, что защитные заявки выставляются на ожидаемых уровнях.
  • Подберите параметры LotSizeDivisor и LotDecimals в соответствии с требованиями брокера до запуска на реальном счёте.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// ARD Order Management Command: EMA trend following with ATR stops.
/// </summary>
public class ArdOrderManagementCommandStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevClose;
	private decimal _entryPrice;

	public ArdOrderManagementCommandStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "Trend filter.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

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

		_prevClose = 0; _entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevClose = 0; _entryPrice = 0;
		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ema, atr, ProcessCandle).Start();
		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, ema); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal emaVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		var close = candle.ClosePrice;
		if (_prevClose == 0 || atrVal <= 0) { _prevClose = close; return; }

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 2.5m || close <= _entryPrice - atrVal * 1.5m || close < emaVal) { SellMarket(); _entryPrice = 0; }
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 2.5m || close >= _entryPrice + atrVal * 1.5m || close > emaVal) { BuyMarket(); _entryPrice = 0; }
		}

		if (Position == 0)
		{
			if (close > emaVal && _prevClose <= emaVal) { _entryPrice = close; BuyMarket(); }
			else if (close < emaVal && _prevClose >= emaVal) { _entryPrice = close; SellMarket(); }
		}
		_prevClose = close;
	}
}