在 GitHub 上查看

CM面板策略

概述

CM面板策略 是对 MetaTrader 5 脚本“cm panel”的人工挂单面板的还原。StockSharp 版本不再绘制界面元素,而是通过一组交互式参数模拟按钮:将布尔参数切换为 true 就会发送或取消挂单,随后参数会自动恢复为 false,与原始面板的“按下即执行”体验一致。策略为买入和卖出分别保留距离、手数和止盈止损等配置,并以“点”为统一单位。

整个移植完全建立在 StockSharp 的高级 API 之上。挂单通过 BuyStopSellStop 等助手方法注册;当挂单成交后,策略立即补充独立的止损和止盈订单,从而重现 MetaTrader 中直接附加 SL/TP 的行为。价格与成交量都会根据标的证券的最小价差 (PriceStep) 与最小手数 (VolumeStep) 自动调整,无需开发者手工调用 _Point_Digits 等常量来规范化数值。

交易逻辑

  1. 当用户把 PlaceBuyStop 设为 true 时,策略读取最新的卖价(若缺失则回退到最近成交价),再加上 BuyStopOffsetPoints 换算成的价格距离,得到买入止损挂单价位。以 BuyVolume 为手数发送买入止损挂单,同时计算出期望的止损与止盈价格并暂存。
  2. 当用户把 PlaceSellStop 设为 true 时,策略读取最新买价(或最近成交价)并减去 SellStopOffsetPoints 换算出的距离,以得到卖出止损挂单的触发价,同时记录对应的止损与止盈目标。
  3. 当任一挂单成交后,策略会按记录的价位自动放置保护性订单:
    • 多头成交后,在入场价下方放置 SellStop 止损,并在上方放置 SellLimit 止盈;
    • 空头成交后,在入场价上方放置 BuyStop 止损,并在下方放置 BuyLimit 止盈。 每组保护订单只会下达一次,若其中一个成交,另一个会被取消,模拟 MT5 中“只有一组 SL/TP”的逻辑。
  4. CancelPendingOrders 被切换时,策略会取消所有仍在挂出的买入止损和卖出止损订单。已经用于保护持仓的止损/止盈订单不会被取消,以确保持仓安全。
  5. 策略会依据 VolumeStepMinVolumeMaxVolume 自动调整下单手数;若调整后的手数仍不合法(例如低于最小交易量),策略会记录警告并放弃下单。
  6. 所有距离参数都以“点”为单位,通过证券的 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 取消策略创建的所有止损挂单。

与 MetaTrader 原版的差异

  • 在 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;
	}
}