Открыть на GitHub

Стратегия Sample Check Pending Order

Стратегия Sample Check Pending Order постоянно поддерживает в стакане ровно один стоп-ордер на покупку и один стоп-ордер на продажу. Исходный советник MetaTrader 5 от Tungman проверял допустимость указанного лота у брокера, контролировал достаточность свободной маржи в обоих направлениях и затем размещал новые отложенные ордера прямо по текущим BID/ASK с истечением через сутки. В этой реализации те же действия перенесены на StockSharp с использованием высокоуровневого API и данных уровня 1.

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

  1. Обработка рыночных данных
    • Стратегия подписывается на поток Level 1 и кеширует последние значения лучшего BID и лучшего ASK.
    • Пока обе цены недоступны или IsFormedAndOnlineAndAllowTrading() возвращает false, торговые решения приостанавливаются.
  2. Проверка объёма
    • Каждый тик запускает проверку параметра OrderVolume относительно Security.MinVolume, Security.MaxVolume и Security.VolumeStep.
    • Объём должен входить в допустимый диапазон и быть кратным шагу лота. При нарушении выводится сообщение в лог, а новые ордера не отправляются.
  3. Предварительная оценка маржи
    • Перед постановкой ордеров рассчитывается требуемая маржа для длинной и короткой позиции указанного размера. Используются текущие BID/ASK, множитель инструмента и заданное пользователем значение AccountLeverage.
    • Если фактическая или стартовая стоимость портфеля недостаточна хотя бы для одного направления, стратегия пропускает тик, что соответствует поведению функции CheckMoneyForTrade.
  4. Размещение отложенных ордеров
    • При отсутствии активного buy-stop стратегия выставляет его по текущему ASK (после округления к ближайшему шагу цены). Аналогично sell-stop размещается по текущему BID.
    • Для каждого ордера запоминается срок действия (ExpirationMinutes, по умолчанию сутки). При последующих тиках, если срок истёк, ордер отменяется и ячейка освобождается под новую заявку.
  5. Управление рисками
    • Метод StartProtection автоматически добавляет стоп-лосс и тейк-профит на расстояниях StopLossPoints и TakeProfitPoints. После срабатывания stop-ордера защитные заявки выставляются движком StockSharp, как и в оригинальной версии MT5.

В результате получается минималистичная стратегия пробоя, которая «зажимает» рынок между двумя стопами. Как только один из них активируется, противоположный остаётся в силе, а недостающий ордер будет создан при следующем обновлении котировок.

Параметры

Параметр Описание
OrderVolume Объём заявки. Должен соответствовать ограничениям брокера и шагу лота.
StopLossPoints Расстояние в пунктах до защитного стоп-лосса после открытия позиции.
TakeProfitPoints Расстояние в пунктах до тейк-профита, который ставится после входа.
ExpirationMinutes Время жизни каждой отложенной заявки. По истечении ордер отменяется.
AccountLeverage Оценка кредитного плеча, используемая при расчёте требуемой маржи.

Все расстояния переводятся в абсолютные цены через Security.PriceStep. Если шаг цены или множитель инструмента отсутствуют, применяется запасное значение 1, чтобы избежать неопределённостей. Логи фиксируют нестандартные ситуации для облегчения диагностики.

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

  • Жизненный цикл ордеров – В полях стратегии хранятся последние объекты Order, возвращённые BuyStop и SellStop. Вспомогательные методы обнуляют ссылки после перехода ордера в состояние Done, Canceled или Failed, предотвращая ложное распознавание неактивных заявок.
  • Контроль срока действия – Поскольку не все площадки поддерживают серверное истечение стоп-ордеров, время жизни отслеживается на стороне стратегии. При превышении лимита вызывается CancelOrder.
  • Оценка маржи – Требования по марже оцениваются через доступный капитал портфеля и введённое плечо. Такой подход близок к OrderCalcMargin и не зависит от специфики биржи.
  • Высокоуровневый API – Весь функционал реализован средствами SubscribeLevel1, BuyStop, SellStop и StartProtection, что соответствует методическим рекомендациям и упрощает код.

Документация намеренно насыщена деталями, чтобы трейдер мог легко сопоставить конверсию с оригинальным советником и адаптировать параметры под своего брокера.

namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Sample Check Pending Order strategy: CCI trend following.
/// Buys when CCI crosses above zero, sells when CCI crosses below zero.
/// </summary>
public class SampleCheckPendingOrderStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _period;
	private readonly StrategyParam<decimal> _cciLevel;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private decimal _prevCci;
	private int _candlesSinceTrade;
	private bool _hasPrev;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int Period { get => _period.Value; set => _period.Value = value; }
	public decimal CciLevel { get => _cciLevel.Value; set => _cciLevel.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public SampleCheckPendingOrderStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_period = Param(nameof(Period), 30)
			.SetGreaterThanZero()
			.SetDisplay("Period", "CCI period", "Indicators");
		_cciLevel = Param(nameof(CciLevel), 100m)
			.SetDisplay("CCI Level", "CCI crossover threshold", "Signals");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevCci = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevCci = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
		var cci = new CommodityChannelIndex { Length = Period };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(cci, ProcessCandle).Start();
	}

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

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		if (_hasPrev)
		{
			if (_prevCci <= -CciLevel && cciValue > -CciLevel && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
			else if (_prevCci >= CciLevel && cciValue < CciLevel && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
		}

		_prevCci = cciValue;
		_hasPrev = true;
	}
}