Открыть на GitHub

Стратегия CM Panel

Обзор

CM Panel — это перенос ручной панели для отложенных ордеров из MetaTrader 5. Вместо интерфейсных кнопок в терминале StockSharp стратегия предоставляет набор параметров, работающих как переключатели: при установке булевого параметра в true немедленно отправляется (или отменяется) соответствующий ордер, после чего значение автоматически сбрасывается в false. Для каждой стороны рынка задаются собственные расстояния, объёмы и уровни стопов/тейков, выраженные в пунктах.

Перенос выполнен на базе высокоуровневого API StockSharp. Вход в позицию происходит через вспомогательные методы BuyStop и SellStop, а защитные ордера создаются отдельными заявками сразу после исполнения отложки. Цены и объёмы автоматически подгоняются под шаг цены и шаг лота инструмента, поэтому нет необходимости вручную использовать _Point, _Digits или другие метатрейдеровские константы.

Торговая логика

  1. При переключении PlaceBuyStop в true стратегия считывает лучшую цену ask (при её отсутствии используется последняя сделка) и прибавляет к ней BuyStopOffsetPoints, переведённые в денежные единицы. На полученном уровне выставляется стоп-заявка объёмом BuyVolume. Одновременно вычисляются и сохраняются уровни стоп-лосса и тейк-профита.
  2. При включении PlaceSellStop лучшая цена bid (или последняя сделка) уменьшается на SellStopOffsetPoints. На этом уровне размещается sell stop объёмом SellVolume, а защитные уровни записываются для последующего использования.
  3. После исполнения любого из отложенных ордеров стратегия автоматически выставляет защитные заявки:
    • Для длинной позиции создаётся SellStop ниже входа и SellLimit выше него.
    • Для короткой позиции — BuyStop выше входа и BuyLimit ниже него. Каждый защитный ордер ставится только один раз; при срабатывании одного пара автоматически отменяется, что повторяет модель «один SL/TP» в MetaTrader.
  4. Переключение CancelPendingOrders приводит к отмене всех активных buy stop и sell stop, созданных стратегией. Защитные ордера уже открытых позиций намеренно не трогаются, чтобы не оставить позиции без контроля риска.
  5. Объёмы заявок корректируются с учётом VolumeStep, MinVolume и MaxVolume. Если после округления размер оказывается некорректным (например, ниже минимума), операция отменяется с записью предупреждения в журнал.
  6. Все расстояния задаются в пунктах и преобразуются через PriceStep. При отсутствии данных о тиковом шаге используется запасное значение 0.0001, что позволяет работать даже с инструментами без заполненной спецификации.

Параметры

Имя Тип Значение по умолчанию Описание
BuyVolume decimal 0.10 Объём заявок buy stop с учётом шага лота инструмента.
SellVolume decimal 0.10 Объём заявок sell stop.
BuyStopOffsetPoints int 100 Сдвиг в пунктах вверх от текущего ask для размещения buy stop.
SellStopOffsetPoints int 100 Сдвиг в пунктах вниз от текущего bid для размещения sell stop.
BuyStopLossPoints int 100 Расстояние до стоп-лосса (в пунктах) для длинных позиций, открытых через buy stop. Ноль отключает защиту.
SellStopLossPoints int 100 Расстояние до стоп-лосса (в пунктах) для коротких позиций, открытых через sell stop. Ноль отключает защиту.
BuyTakeProfitPoints int 150 Расстояние до тейк-профита (в пунктах) для длинных позиций, открытых через buy stop. Ноль отключает защиту.
SellTakeProfitPoints int 150 Расстояние до тейк-профита (в пунктах) для коротких позиций, открытых через sell stop. Ноль отключает защиту.
PlaceBuyStop bool false Однократное выставление buy stop. После обработки параметр сбрасывается в false.
PlaceSellStop bool false Однократное выставление sell stop. После обработки параметр сбрасывается в false.
CancelPendingOrders bool false Отмена всех активных отложенных ордеров, созданных панелью.

Отличия от версии для MetaTrader

  • В MetaTrader стоп-лосс и тейк-профит прикрепляются прямо к отложенному ордеру. В StockSharp они создаются отдельными защитными заявками сразу после входа в позицию.
  • Все цены и объёмы автоматически приводятся к спецификации инструмента, поэтому не требуется вручную учитывать _Point, _Digits и т.п.
  • Минимальные допустимые расстояния до стопов со стороны брокера автоматически не проверяются. Пользователь должен задать безопасные значения, как и в оригинальном скрипте.
  • Переключатель CancelPendingOrders отменяет только отложенные заявки. Защитные ордера для действующих позиций остаются активными, чтобы не снимать защиту с открытых сделок.

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

  • Перед переключением параметров обязательно назначьте инструмент и портфель. Иначе стратегия зафиксирует предупреждение и проигнорирует запрос.
  • Чтобы повторить рабочий процесс оригинальной панели, добавьте стратегию в Designer или Runner, отобразите параметры в свойствах и управляйте ими вручную.
  • Для корректного расчёта уровней требуется поток котировок Level 1. При отсутствии bid/ask используется цена последней сделки, что может привести к более близким уровням, чем ожидалось.
  • Настраивайте расстояния в пунктах с учётом минимального стоп-уровня брокера — стратегия не накладывает дополнительных ограничений.
  • Если необходимо выставить «голые» стоп-заявки без SL/TP, просто установите соответствующие расстояния в ноль.
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>
/// Trading panel strategy that enters positions using configurable offset distances
/// and manages them with stop-loss and take-profit levels.
/// Simplified from the CM Panel MetaTrader script.
/// </summary>
public class CmPanelStrategy : Strategy
{
	private readonly StrategyParam<int> _buyOffsetPoints;
	private readonly StrategyParam<int> _sellOffsetPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _priceStep;

	/// <summary>
	/// Buy trigger offset in points above SMA.
	/// </summary>
	public int BuyOffsetPoints
	{
		get => _buyOffsetPoints.Value;
		set => _buyOffsetPoints.Value = value;
	}

	/// <summary>
	/// Sell trigger offset in points below SMA.
	/// </summary>
	public int SellOffsetPoints
	{
		get => _sellOffsetPoints.Value;
		set => _sellOffsetPoints.Value = value;
	}

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

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

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public CmPanelStrategy()
	{
		_buyOffsetPoints = Param(nameof(BuyOffsetPoints), 100)
			.SetNotNegative()
			.SetDisplay("Buy Offset", "Distance above SMA for buy entry (points)", "Distances");

		_sellOffsetPoints = Param(nameof(SellOffsetPoints), 100)
			.SetNotNegative()
			.SetDisplay("Sell Offset", "Distance below SMA for sell entry (points)", "Distances");

		_stopLossPoints = Param(nameof(StopLossPoints), 100)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in points", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for signals", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_priceStep = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 0.01m;

		var sma = new SimpleMovingAverage { Length = 20 };

		SubscribeCandles(CandleType)
			.Bind(sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var price = candle.ClosePrice;
		var step = _priceStep > 0m ? _priceStep : 0.01m;

		// Check stop-loss / take-profit for open positions
		if (Position != 0 && _entryPrice > 0m)
		{
			if (Position > 0)
			{
				if (_stopPrice.HasValue && price <= _stopPrice.Value)
				{
					SellMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
				if (_takePrice.HasValue && price >= _takePrice.Value)
				{
					SellMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}
			else if (Position < 0)
			{
				if (_stopPrice.HasValue && price >= _stopPrice.Value)
				{
					BuyMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
				if (_takePrice.HasValue && price <= _takePrice.Value)
				{
					BuyMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}
		}

		// Entry: price crosses above SMA + offset => buy, below SMA - offset => sell
		if (Position == 0)
		{
			var buyLevel = smaValue + BuyOffsetPoints * step;
			var sellLevel = smaValue - SellOffsetPoints * step;

			if (price >= buyLevel)
			{
				BuyMarket();
				_entryPrice = price;
				_stopPrice = StopLossPoints > 0 ? price - StopLossPoints * step : null;
				_takePrice = TakeProfitPoints > 0 ? price + TakeProfitPoints * step : null;
			}
			else if (price <= sellLevel)
			{
				SellMarket();
				_entryPrice = price;
				_stopPrice = StopLossPoints > 0 ? price + StopLossPoints * step : null;
				_takePrice = TakeProfitPoints > 0 ? price - TakeProfitPoints * step : null;
			}
		}
	}

	private void ResetPosition()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}
}