GitHub で見る

CM Panel Strategy

Overview

The CM Panel Strategy is a manual pending-order helper that recreates the behaviour of the original MetaTrader 5 script "cm panel". Instead of drawing on-screen controls, the StockSharp port exposes interactive parameters that work like buttons: setting a flag to true places or cancels pending stop orders and the flag immediately resets to false, mimicking the push-button workflow of the panel. The strategy keeps separate configuration for buy and sell orders, including distances, volumes, and protective targets expressed in points.

The conversion relies entirely on StockSharp’s high-level API. Pending orders are submitted with the BuyStop and SellStop helpers, while post-fill protection is implemented by registering independent stop-loss and take-profit orders. Price and volume values are automatically adapted to the security’s tick size and lot step so the strategy honours exchange constraints without requiring manual normalization.

Trading logic

  1. When the user toggles PlaceBuyStop to true, the strategy reads the best ask (falling back to the last trade price if necessary) and shifts it by BuyStopOffsetPoints converted to price units. A buy stop order with volume BuyVolume is submitted at the resulting level. The desired stop-loss and take-profit prices are computed immediately and stored as pending protective targets.
  2. When the user toggles PlaceSellStop to true, the best bid (or last trade) is shifted downward by SellStopOffsetPoints. A sell stop order with volume SellVolume is placed at that price, and the corresponding protective levels are recorded.
  3. After a pending stop order trades, the strategy automatically places the recorded protective orders:
    • Long positions receive a SellStop stop-loss below the entry price and a SellLimit take-profit above it.
    • Short positions receive a BuyStop stop-loss above the entry price and a BuyLimit take-profit below it. Each protective order is submitted only once; if one fills, the other is cancelled to emulate MetaTrader’s single SL/TP pair.
  4. When the CancelPendingOrders flag is toggled, any active buy or sell stop orders created by the strategy are cancelled. Protective orders already guarding open positions are intentionally left untouched so ongoing trades remain protected.
  5. Volumes are adjusted to the security’s VolumeStep, MinVolume, and MaxVolume. If the resulting size becomes invalid (for example below the minimum lot), the operation is aborted and a warning is logged instead of sending an order.
  6. All price distances are expressed in points and converted using the security’s PriceStep. If the step is unknown, a conservative fallback of 0.0001 is applied so the panel remains usable on symbols without tick metadata.

Parameters

Name Type Default Description
BuyVolume decimal 0.10 Volume sent with each buy stop order after respecting the instrument’s lot step.
SellVolume decimal 0.10 Volume sent with each sell stop order after respecting the instrument’s lot step.
BuyStopOffsetPoints int 100 Distance in points added above the current ask to position the pending buy stop.
SellStopOffsetPoints int 100 Distance in points subtracted from the current bid to position the pending sell stop.
BuyStopLossPoints int 100 Stop-loss distance (in points) for long positions triggered by the buy stop. Set to zero to skip the protective order.
SellStopLossPoints int 100 Stop-loss distance (in points) for short positions triggered by the sell stop. Set to zero to skip the protective order.
BuyTakeProfitPoints int 150 Take-profit distance (in points) for long positions triggered by the buy stop. Set to zero to skip the protective order.
SellTakeProfitPoints int 150 Take-profit distance (in points) for short positions triggered by the sell stop. Set to zero to skip the protective order.
PlaceBuyStop bool false Toggle that places a buy stop order once. The value resets to false automatically after processing.
PlaceSellStop bool false Toggle that places a sell stop order once. The value resets to false automatically after processing.
CancelPendingOrders bool false Toggle that cancels all active pending stop orders created by the panel.

Differences from the MetaTrader version

  • MetaTrader attaches stop-loss and take-profit levels directly to pending orders. StockSharp keeps the same behaviour by generating dedicated protective orders immediately after an entry fills.
  • The StockSharp implementation transparently adapts volumes and prices to the security metadata, removing the need for manual normalization with _Point, _Digits, or volume rounding.
  • Stop-level limitations from the trading venue are not queried automatically. Users should configure offsets that respect the broker’s minimum distance, just as they would in MetaTrader.
  • The delete toggle (CancelPendingOrders) cancels only pending stops. Existing protective orders for open positions remain active so live trades stay guarded.

Usage tips

  • Assign a portfolio and security before toggling any action flags; otherwise the strategy logs a warning and ignores the request.
  • To emulate the original panel workflow, add the strategy to the Designer or Runner UI, expose the parameters in the property grid, and flip the booleans when you want to submit or cancel orders.
  • Because the logic relies on best bid/ask quotes, ensure Level 1 data is streamed. If the best prices are missing, the code falls back to the last traded price, but pending orders may end up closer to the market than intended.
  • Adjust the point distances to respect the instrument’s minimum stop level. The helper does not automatically enforce broker-specific buffers.
  • Set protective distances to zero whenever you want to place naked stop orders without accompanying SL/TP levels.
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;
	}
}