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
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.
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.
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.
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.
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class cm_panel_strategy(Strategy):
def __init__(self):
super(cm_panel_strategy, self).__init__()
self._buy_offset_points = self.Param("BuyOffsetPoints", 100) \
.SetDisplay("Buy Offset", "Distance above SMA for buy entry (points)", "Distances")
self._sell_offset_points = self.Param("SellOffsetPoints", 100) \
.SetDisplay("Sell Offset", "Distance below SMA for sell entry (points)", "Distances")
self._stop_loss_points = self.Param("StopLossPoints", 100) \
.SetDisplay("Stop Loss", "Stop-loss distance in points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 150) \
.SetDisplay("Take Profit", "Take-profit distance in points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle series for signals", "General")
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._price_step = 0.0
@property
def buy_offset_points(self):
return self._buy_offset_points.Value
@property
def sell_offset_points(self):
return self._sell_offset_points.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(cm_panel_strategy, self).OnReseted()
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._price_step = 0.0
def OnStarted2(self, time):
super(cm_panel_strategy, self).OnStarted2(time)
step = self.Security.PriceStep if self.Security is not None else None
self._price_step = float(step) if step is not None and float(step) > 0 else 0.01
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
price = float(candle.ClosePrice)
step = self._price_step if self._price_step > 0 else 0.01
sma_val = float(sma_value)
# Check SL/TP for open positions
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
if self._stop_price is not None and price <= self._stop_price:
self.SellMarket(abs(self.Position))
self._reset_position()
return
if self._take_price is not None and price >= self._take_price:
self.SellMarket(abs(self.Position))
self._reset_position()
return
elif self.Position < 0:
if self._stop_price is not None and price >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_position()
return
if self._take_price is not None and price <= self._take_price:
self.BuyMarket(abs(self.Position))
self._reset_position()
return
# Entry signals
if self.Position == 0:
buy_level = sma_val + self.buy_offset_points * step
sell_level = sma_val - self.sell_offset_points * step
if price >= buy_level:
self.BuyMarket()
self._entry_price = price
self._stop_price = price - self.stop_loss_points * step if self.stop_loss_points > 0 else None
self._take_price = price + self.take_profit_points * step if self.take_profit_points > 0 else None
elif price <= sell_level:
self.SellMarket()
self._entry_price = price
self._stop_price = price + self.stop_loss_points * step if self.stop_loss_points > 0 else None
self._take_price = price - self.take_profit_points * step if self.take_profit_points > 0 else None
def _reset_position(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return cm_panel_strategy()