CM Panel — это перенос ручной панели для отложенных ордеров из MetaTrader 5. Вместо интерфейсных кнопок в терминале StockSharp стратегия предоставляет набор параметров, работающих как переключатели: при установке булевого параметра в true немедленно отправляется (или отменяется) соответствующий ордер, после чего значение автоматически сбрасывается в false. Для каждой стороны рынка задаются собственные расстояния, объёмы и уровни стопов/тейков, выраженные в пунктах.
Перенос выполнен на базе высокоуровневого API StockSharp. Вход в позицию происходит через вспомогательные методы BuyStop и SellStop, а защитные ордера создаются отдельными заявками сразу после исполнения отложки. Цены и объёмы автоматически подгоняются под шаг цены и шаг лота инструмента, поэтому нет необходимости вручную использовать _Point, _Digits или другие метатрейдеровские константы.
Торговая логика
При переключении PlaceBuyStop в true стратегия считывает лучшую цену ask (при её отсутствии используется последняя сделка) и прибавляет к ней BuyStopOffsetPoints, переведённые в денежные единицы. На полученном уровне выставляется стоп-заявка объёмом BuyVolume. Одновременно вычисляются и сохраняются уровни стоп-лосса и тейк-профита.
При включении PlaceSellStop лучшая цена bid (или последняя сделка) уменьшается на SellStopOffsetPoints. На этом уровне размещается sell stop объёмом SellVolume, а защитные уровни записываются для последующего использования.
После исполнения любого из отложенных ордеров стратегия автоматически выставляет защитные заявки:
Для длинной позиции создаётся SellStop ниже входа и SellLimit выше него.
Для короткой позиции — BuyStop выше входа и BuyLimit ниже него.
Каждый защитный ордер ставится только один раз; при срабатывании одного пара автоматически отменяется, что повторяет модель «один SL/TP» в MetaTrader.
Переключение CancelPendingOrders приводит к отмене всех активных buy stop и sell stop, созданных стратегией. Защитные ордера уже открытых позиций намеренно не трогаются, чтобы не оставить позиции без контроля риска.
Объёмы заявок корректируются с учётом VolumeStep, MinVolume и MaxVolume. Если после округления размер оказывается некорректным (например, ниже минимума), операция отменяется с записью предупреждения в журнал.
Все расстояния задаются в пунктах и преобразуются через 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;
}
}
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()