La Estrategia CM Panel es un asistente manual de órdenes pendientes que recrea el comportamiento del script original de MetaTrader 5 "cm panel". En lugar de dibujar controles en pantalla, el puerto a StockSharp expone parámetros interactivos que funcionan como botones: establecer un indicador en true coloca o cancela órdenes stop pendientes y el indicador se reinicia inmediatamente a false, imitando el flujo de trabajo de botón pulsable del panel. La estrategia mantiene configuración separada para órdenes de compra y venta, incluyendo distancias, volúmenes y objetivos de protección expresados en puntos.
La conversión se basa completamente en la API de alto nivel de StockSharp. Las órdenes pendientes se envían con los helpers BuyStop y SellStop, mientras que la protección posterior a la ejecución se implementa registrando órdenes de stop-loss y take-profit independientes. Los valores de precio y volumen se adaptan automáticamente al tamaño de tick y paso de lote del instrumento, por lo que la estrategia respeta las restricciones de la bolsa sin requerir normalización manual.
Lógica de trading
Cuando el usuario activa PlaceBuyStop en true, la estrategia lee el mejor ask (con respaldo al último precio negociado si es necesario) y lo desplaza por BuyStopOffsetPoints convertidos a unidades de precio. Se envía una orden stop de compra con volumen BuyVolume al nivel resultante. Los precios deseados de stop-loss y take-profit se calculan inmediatamente y se almacenan como objetivos de protección pendientes.
Cuando el usuario activa PlaceSellStop en true, el mejor bid (o última operación) se desplaza hacia abajo por SellStopOffsetPoints. Se coloca una orden stop de venta con volumen SellVolume a ese precio y los niveles de protección correspondientes se registran.
Después de que una orden stop pendiente se ejecuta, la estrategia coloca automáticamente las órdenes de protección registradas:
Las posiciones largas reciben un SellStop de stop-loss por debajo del precio de entrada y un SellLimit de take-profit por encima.
Las posiciones cortas reciben un BuyStop de stop-loss por encima del precio de entrada y un BuyLimit de take-profit por debajo.
Cada orden de protección se envía solo una vez; si una se ejecuta, la otra se cancela para emular el par SL/TP único de MetaTrader.
Cuando se activa el indicador CancelPendingOrders, se cancelan todas las órdenes stop de compra o venta activas creadas por la estrategia. Las órdenes de protección que ya guardan posiciones abiertas se dejan intencionalmente intactas para que las operaciones en curso permanezcan protegidas.
Los volúmenes se ajustan al VolumeStep, MinVolume y MaxVolume del instrumento. Si el tamaño resultante se vuelve inválido (por ejemplo, inferior al lote mínimo), la operación se cancela y se registra una advertencia en lugar de enviar una orden.
Todas las distancias de precio se expresan en puntos y se convierten usando el PriceStep del instrumento. Si el paso es desconocido, se aplica un respaldo conservador de 0.0001 para que el panel siga siendo utilizable en símbolos sin metadatos de tick.
Parámetros
Nombre
Tipo
Predeterminado
Descripción
BuyVolume
decimal
0.10
Volumen enviado con cada orden stop de compra después de respetar el paso de lote del instrumento.
SellVolume
decimal
0.10
Volumen enviado con cada orden stop de venta después de respetar el paso de lote del instrumento.
BuyStopOffsetPoints
int
100
Distancia en puntos añadida por encima del ask actual para posicionar el stop de compra pendiente.
SellStopOffsetPoints
int
100
Distancia en puntos restada del bid actual para posicionar el stop de venta pendiente.
BuyStopLossPoints
int
100
Distancia del stop-loss (en puntos) para posiciones largas activadas por el stop de compra. Establecer en cero para omitir la orden de protección.
SellStopLossPoints
int
100
Distancia del stop-loss (en puntos) para posiciones cortas activadas por el stop de venta. Establecer en cero para omitir la orden de protección.
BuyTakeProfitPoints
int
150
Distancia del take-profit (en puntos) para posiciones largas activadas por el stop de compra. Establecer en cero para omitir la orden de protección.
SellTakeProfitPoints
int
150
Distancia del take-profit (en puntos) para posiciones cortas activadas por el stop de venta. Establecer en cero para omitir la orden de protección.
PlaceBuyStop
bool
false
Indicador que coloca una orden stop de compra una vez. El valor se reinicia a false automáticamente después del procesamiento.
PlaceSellStop
bool
false
Indicador que coloca una orden stop de venta una vez. El valor se reinicia a false automáticamente después del procesamiento.
CancelPendingOrders
bool
false
Indicador que cancela todas las órdenes stop pendientes activas creadas por el panel.
Diferencias respecto a la versión MetaTrader
MetaTrader adjunta niveles de stop-loss y take-profit directamente a las órdenes pendientes. StockSharp mantiene el mismo comportamiento generando órdenes de protección dedicadas inmediatamente después de que se ejecuta una entrada.
La implementación de StockSharp adapta transparentemente los volúmenes y precios a los metadatos del instrumento, eliminando la necesidad de normalización manual con _Point, _Digits o redondeo de volumen.
Las limitaciones de nivel de stop de la sede de negociación no se consultan automáticamente. Los usuarios deben configurar desplazamientos que respeten la distancia mínima del bróker, como lo harían en MetaTrader.
El indicador de eliminación (CancelPendingOrders) cancela solo los stops pendientes. Las órdenes de protección existentes para posiciones abiertas permanecen activas para que las operaciones en curso estén protegidas.
Consejos de uso
Asignar un portafolio e instrumento antes de activar cualquier indicador de acción; de lo contrario, la estrategia registra una advertencia e ignora la solicitud.
Para emular el flujo de trabajo del panel original, agregar la estrategia a la interfaz de Designer o Runner, exponer los parámetros en la cuadrícula de propiedades y cambiar los booleanos cuando se quiera enviar o cancelar órdenes.
Como la lógica depende de las mejores cotizaciones bid/ask, asegurarse de que los datos Level 1 estén siendo transmitidos. Si los mejores precios faltan, el código recurre al último precio negociado, pero las órdenes pendientes pueden quedar más cerca del mercado de lo previsto.
Ajustar las distancias en puntos para respetar el nivel de stop mínimo del instrumento. El helper no aplica automáticamente buffers específicos del bróker.
Establecer las distancias de protección en cero cuando se quieran colocar órdenes stop desnudas sin niveles de SL/TP adjuntos.
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()