在 GitHub 上查看
CM面板策略
概述
CM面板策略 是对 MetaTrader 5 脚本“cm panel”的人工挂单面板的还原。StockSharp 版本不再绘制界面元素,而是通过一组交互式参数模拟按钮:将布尔参数切换为 true 就会发送或取消挂单,随后参数会自动恢复为 false,与原始面板的“按下即执行”体验一致。策略为买入和卖出分别保留距离、手数和止盈止损等配置,并以“点”为统一单位。
整个移植完全建立在 StockSharp 的高级 API 之上。挂单通过 BuyStop、SellStop 等助手方法注册;当挂单成交后,策略立即补充独立的止损和止盈订单,从而重现 MetaTrader 中直接附加 SL/TP 的行为。价格与成交量都会根据标的证券的最小价差 (PriceStep) 与最小手数 (VolumeStep) 自动调整,无需开发者手工调用 _Point、_Digits 等常量来规范化数值。
交易逻辑
- 当用户把
PlaceBuyStop 设为 true 时,策略读取最新的卖价(若缺失则回退到最近成交价),再加上 BuyStopOffsetPoints 换算成的价格距离,得到买入止损挂单价位。以 BuyVolume 为手数发送买入止损挂单,同时计算出期望的止损与止盈价格并暂存。
- 当用户把
PlaceSellStop 设为 true 时,策略读取最新买价(或最近成交价)并减去 SellStopOffsetPoints 换算出的距离,以得到卖出止损挂单的触发价,同时记录对应的止损与止盈目标。
- 当任一挂单成交后,策略会按记录的价位自动放置保护性订单:
- 多头成交后,在入场价下方放置
SellStop 止损,并在上方放置 SellLimit 止盈;
- 空头成交后,在入场价上方放置
BuyStop 止损,并在下方放置 BuyLimit 止盈。
每组保护订单只会下达一次,若其中一个成交,另一个会被取消,模拟 MT5 中“只有一组 SL/TP”的逻辑。
- 当
CancelPendingOrders 被切换时,策略会取消所有仍在挂出的买入止损和卖出止损订单。已经用于保护持仓的止损/止盈订单不会被取消,以确保持仓安全。
- 策略会依据
VolumeStep、MinVolume、MaxVolume 自动调整下单手数;若调整后的手数仍不合法(例如低于最小交易量),策略会记录警告并放弃下单。
- 所有距离参数都以“点”为单位,通过证券的
PriceStep 转换为价格。如果缺乏最小跳动数据,策略会使用 0.0001 的保守缺省值,从而在缺乏元数据的品种上仍可使用。
参数说明
| 名称 |
类型 |
默认值 |
说明 |
BuyVolume |
decimal |
0.10 |
下达买入止损挂单时使用的手数,会自动匹配品种的最小手数。 |
SellVolume |
decimal |
0.10 |
下达卖出止损挂单时使用的手数。 |
BuyStopOffsetPoints |
int |
100 |
在当前卖价基础上向上偏移的点数,用于计算买入止损价位。 |
SellStopOffsetPoints |
int |
100 |
在当前买价基础上向下偏移的点数,用于计算卖出止损价位。 |
BuyStopLossPoints |
int |
100 |
买入止损成交后,多头仓位的止损距离(点数)。为零时不放置止损单。 |
SellStopLossPoints |
int |
100 |
卖出止损成交后,空头仓位的止损距离(点数)。为零时不放置止损单。 |
BuyTakeProfitPoints |
int |
150 |
买入止损成交后,多头仓位的止盈距离(点数)。为零时不放置止盈单。 |
SellTakeProfitPoints |
int |
150 |
卖出止损成交后,空头仓位的止盈距离(点数)。为零时不放置止盈单。 |
PlaceBuyStop |
bool |
false |
触发一次买入止损挂单。处理完毕后自动复位为 false。 |
PlaceSellStop |
bool |
false |
触发一次卖出止损挂单。处理完毕后自动复位为 false。 |
CancelPendingOrders |
bool |
false |
取消策略创建的所有止损挂单。 |
- 在 MT5 中,止损和止盈可以作为挂单属性直接提交;StockSharp 版本在成交后通过额外的保护性订单来重建同样的结果。
- 新版本根据证券元数据自动规范价格与手数,无需手工调用
_Point、_Digits 等常量。
- 策略不会自动检查券商的最小止损距离,用户仍需自行设定足够的偏移量。
- “删除”开关 (
CancelPendingOrders) 只会取消挂单,不会移除已经保护持仓的止损/止盈,以免裸露已有仓位。
使用建议
- 在切换任何操作参数之前,务必先指定证券与投资组合;否则策略会记录警告并忽略操作。
- 若希望像原脚本一样通过面板操作,可在 Designer 或 Runner 中加载策略,把布尔参数展示在属性面板里,再在需要时手动切换。
- 策略优先使用最新买卖报价,请确保订阅了 Level 1 数据;若缺少报价,会退回到最近成交价,可能导致挂单与预期距离略有偏差。
- 根据交易商的最小止损距离调整点数参数,策略不会主动施加额外缓冲。
- 当需要发送不带止盈止损的挂单时,将对应距离设置为零即可。
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()