在 GitHub 上查看

Long/Short Expert MACD 策略

概述

Long/Short Expert MACD 策略是 MetaTrader 专家顾问「LongShortExpertMACD」的 StockSharp 版本。策略使用 MACD 与其信号线的交叉来产生买卖信号,同时在开仓后立即应用固定距离的止损和止盈。交易者可以选择仅做多、仅做空或允许双向交易,所有保护距离均以价格点数表示。

实现基于 StockSharp 的高级 API:策略订阅单一时间框架的蜡烛数据,并把 MovingAverageConvergenceDivergenceSignal 指标绑定到订阅上。所有订单均以市价提交,因此能够方便地用于实时运行或历史回测。

指标与行情

  • 蜡烛数据:由参数 CandleType 指定的时间框架(默认 1 分钟)。策略通过 SubscribeCandles 订阅该序列。
  • MovingAverageConvergenceDivergenceSignal:StockSharp 内置的 MACD 指标,可分别设置快 EMA、慢 EMA 和信号 EMA 的周期。MACD 与信号线的差值隐含形成直方图。

交易逻辑

  1. 信号准备

    • 每根完成的蜡烛都会触发计算,指标绑定返回当前的 MACD 值与信号值。
    • 状态变量 _prevIsMacdAboveSignal 记录上一根蜡烛时 MACD 是否位于信号线上方,用于检测交叉。
  2. 入场规则

    • 向上交叉:当 MACD 从下方穿越信号线时,若允许多头交易则开多。
      • 如果当前持有空头且允许反手 (AllowedPosition = Both),下单数量会包含现有空头的绝对值,从而一次市价单即可完成平仓并开多。
      • 在多头专用模式中,若仍有空头仓位则立即平仓,但不会立即建立新的多头仓位,需等待下一次信号。
    • 向下交叉:规则与多头对称,用于开空。
  3. 出场规则

    • 风险控制:只要持有仓位,策略都会依据 PositionAvgPrice 重新计算止损与止盈水平。距离等于 Security.PriceStep * 参数,从而在不同合约之间保持一致。
      • 多头仓位在蜡烛最低价触及止损或最高价触及止盈时平仓。
      • 空头仓位在蜡烛最高价达到止损或最低价达到止盈时平仓。
    • 反向交叉:如果允许反向交易,则一旦 MACD 与信号线关系发生反转,仓位会被平掉,并根据模式决定是否立即翻转。
  4. 运行约束

    • 只有当策略满足 IsFormedAndOnlineAndAllowTrading 时才会执行交易逻辑。
    • 当仓位归零时会重置所有保护价格,防止使用过期的水平。

参数

名称 默认值 描述
AllowedPosition Both 交易方向限制:仅多、仅空或双向。
FastLength 12 MACD 快速 EMA 的周期。
SlowLength 24 MACD 慢速 EMA 的周期。
SignalLength 9 信号 EMA 的周期,用于交叉判断。
TakeProfitPoints 50 止盈距离(价格点数,等于 PriceStep * 值)。设为 0 可关闭止盈。
StopLossPoints 20 止损距离(价格点数)。设为 0 可关闭止损。
CandleType TimeFrame(1 minute) 计算所使用的蜡烛类型。
Volume 1 每笔市价单的下单数量。

所有数值型参数都配置了优化区间,可直接在 Designer 或 Runner 中进行批量测试。

仓位管理

  • 翻转逻辑:当允许双向交易时,策略通过单笔市价单完成平仓与反手,完全复刻原始 MQL 专家顾问的行为。
  • 单向模式:对于被禁止的方向,策略会立即平掉已有仓位,但不会反向开仓,直到出现符合方向的下一次信号。
  • 保护水平更新:每根蜡烛都会根据最新的 PositionAvgPrice 重新计算止损/止盈,适用于部分成交或分批加仓的情况。

使用建议

  • 请确认交易标的提供了有效的 PriceStep。若缺失,该策略会退回到 1.0 的价格单位,这对股票合约通常适用,但在外汇品种上可能需要调整。
  • 策略仅对完成的蜡烛做出反应,若希望减少滞后,可选用更低的时间框架。
  • 市价单未考虑滑点,在流动性较差的市场上应额外评估执行风险。
  • 在支持图表的终端中会自动绘制蜡烛、MACD 曲线以及自身成交,方便监控。

转换说明

  • 保留了原 MQL 专家中的所有关键设置:MACD 周期、止损止盈点数以及可交易方向开关。
  • 原策略使用的 TrailingNoneMoneyNone 模块本身不包含逻辑,因此在 StockSharp 版本中无需额外实现。
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>
/// Long/short MACD expert strategy converted from the MetaTrader example.
/// The strategy opens positions on MACD crossovers and applies fixed stop-loss and take-profit distances.
/// Allowed trade direction can be restricted to long only, short only, or both sides.
/// </summary>
public class LongShortExpertMacdStrategy : Strategy
{
	/// <summary>
	/// Trade directions supported by the strategy.
	/// </summary>
	public enum AllowedPositionTypes
	{
		/// <summary>
		/// Long trades only.
		/// </summary>
		Long,

		/// <summary>
		/// Short trades only.
		/// </summary>
		Short,

		/// <summary>
		/// Long and short trades are allowed.
		/// </summary>
		Both
	}

	private readonly StrategyParam<AllowedPositionTypes> _allowedPosition;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _signalLength;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;

	private bool? _prevIsMacdAboveSignal;
	private decimal _longStopPrice;
	private decimal _longTakePrice;
	private decimal _shortStopPrice;
	private decimal _shortTakePrice;
	private decimal? _entryPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="LongShortExpertMacdStrategy"/>.
	/// </summary>
	public LongShortExpertMacdStrategy()
	{
		_allowedPosition = Param(nameof(AllowedPosition), AllowedPositionTypes.Both)
			.SetDisplay("Allowed Positions", "Permitted trade direction", "General");

		_fastLength = Param(nameof(FastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast MACD EMA length", "MACD")

			.SetOptimize(8, 16, 2);

		_slowLength = Param(nameof(SlowLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow MACD EMA length", "MACD")

			.SetOptimize(20, 40, 2);

		_signalLength = Param(nameof(SignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("Signal EMA", "MACD signal EMA length", "MACD")

			.SetOptimize(5, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")

			.SetOptimize(0, 150, 10);

		_stopLossPoints = Param(nameof(StopLossPoints), 20)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance in price points", "Risk")

			.SetOptimize(0, 100, 10);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to process", "General");

		Volume = 1;
	}

	/// <summary>
	/// Allowed trade direction.
	/// </summary>
	public AllowedPositionTypes AllowedPosition
	{
		get => _allowedPosition.Value;
		set => _allowedPosition.Value = value;
	}

	/// <summary>
	/// Fast EMA length used by MACD.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length used by MACD.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// Signal EMA length used by MACD.
	/// </summary>
	public int SignalLength
	{
		get => _signalLength.Value;
		set => _signalLength.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	private bool CanEnterLong => AllowedPosition != AllowedPositionTypes.Short;
	private bool CanEnterShort => AllowedPosition != AllowedPositionTypes.Long;
	private bool AllowReverse => AllowedPosition == AllowedPositionTypes.Both;

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevIsMacdAboveSignal = null;
		_entryPrice = null;
		ResetProtection();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = FastLength },
				LongMa = { Length = SlowLength },
			},
			SignalMa = { Length = SignalLength }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _macd);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

		if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
			return;

		UpdateProtectionLevels();

		var isMacdAboveSignal = macd > signal;

		if (!_macd.IsFormed)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (TryExitWithProtection(candle))
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (_prevIsMacdAboveSignal is null)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		var crossUp = isMacdAboveSignal && _prevIsMacdAboveSignal == false;
		var crossDown = !isMacdAboveSignal && _prevIsMacdAboveSignal == true;

		if (crossUp)
		{
			if (CanEnterLong)
			{
				if (Position < 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							BuyMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							BuyMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position < 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}
		else if (crossDown)
		{
			if (CanEnterShort)
			{
				if (Position > 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							SellMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							SellMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position > 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}

		_prevIsMacdAboveSignal = isMacdAboveSignal;
	}

	private void UpdateProtectionLevels()
	{
		if (_entryPrice is not decimal entry)
		{
			ResetProtection();
			return;
		}

		if (Position > 0)
		{
			var step = GetPriceStep();
			_longStopPrice = StopLossPoints > 0 ? entry - StopLossPoints * step : 0m;
			_longTakePrice = TakeProfitPoints > 0 ? entry + TakeProfitPoints * step : 0m;
			_shortStopPrice = 0m;
			_shortTakePrice = 0m;
		}
		else if (Position < 0)
		{
			var step = GetPriceStep();
			_shortStopPrice = StopLossPoints > 0 ? entry + StopLossPoints * step : 0m;
			_shortTakePrice = TakeProfitPoints > 0 ? entry - TakeProfitPoints * step : 0m;
			_longStopPrice = 0m;
			_longTakePrice = 0m;
		}
		else
		{
			ResetProtection();
		}
	}

	private bool TryExitWithProtection(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _longStopPrice > 0m && candle.LowPrice <= _longStopPrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _longTakePrice > 0m && candle.HighPrice >= _longTakePrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _shortStopPrice > 0m && candle.HighPrice >= _shortStopPrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _shortTakePrice > 0m && candle.LowPrice <= _shortTakePrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		return false;
	}

	private void ResetProtection()
	{
		_longStopPrice = 0m;
		_longTakePrice = 0m;
		_shortStopPrice = 0m;
		_shortTakePrice = 0m;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}