在 GitHub 上查看

ProMart MACD 马丁格尔策略

本策略是历史 MQL 专家 MartGreg_1 / ProMart 的 StockSharp 版本。它组合了两组 MACD 参数,并加入可控的马丁格尔资金管理。第一组 MACD 用于寻找动量的局部低点与高点,第二组 MACD 用于确认最近的斜率方向。每次平仓后,如果上一笔交易盈利,策略会继续等待指标形态;若上一笔交易亏损,则立即反向开仓,并可能把下一笔的下单量翻倍。

交易逻辑

  • 信号生成
    • 在选定的 K 线序列上计算两条 MACD:
      • MACD1(快线=5,慢线=20,信号线=3)负责识别形态。
      • MACD2(快线=10,慢线=15,信号线=3)确认短期斜率。
    • 仅在收盘后的 K 线上评估信号,使用前 3 根 K 线的 MACD1 数值和前 2 根的 MACD2 数值(对应 MQL 版本“向前看一根 K 线”的行为)。
    • 做多:MACD1 形成局部低点(MACD1[t-1] > MACD1[t-2] < MACD1[t-3]),且 MACD2 上升(MACD2[t-2] > MACD2[t-1])。
    • 做空:MACD1 形成局部高点,同时 MACD2 下降。
    • 如果上一笔平仓盈利,策略等待下一次有效形态;若上一笔亏损,则不看指标立即反向开仓,复现原始马丁格尔逻辑。
  • 持仓管理
    • 通过市价单入场,并在每根已完成 K 线上检查仓位。
    • 止损与止盈按价格点数(PriceStep × 参数值)从入场价计算。如果 K 线高/低触及目标,立即市价平仓并记录结果。
    • 与原始 EA 相同,策略不会在同一根 K 线上重新开仓,而是等待下一根 K 线。
  • 马丁格尔仓位
    • 基础下单量 = 投资组合权益 ÷ BalanceDivider,再按交易品种的最小变动量对齐(若缺少账户数据,则退回到策略 Volume 或品种最小下单量)。
    • 亏损后,下一笔可以把前一笔的下单量翻倍,连续翻倍次数受 MaxDoublingCount 限制;盈利会将翻倍计数归零。
    • 始终受品种最大下单量限制,避免过度放大仓位。

参数

参数 说明 默认值
BalanceDivider 用于计算基础下单量的权益除数。 1000
MaxDoublingCount 连续翻倍的最大次数。 1
StopLossPoints 止损距离(价格点数)。 500
TakeProfitPoints 止盈距离(价格点数)。 1500
Macd1Fast / Macd1Slow / Macd1Signal 主 MACD 的周期设置。 5 / 20 / 3
Macd2Fast / Macd2Slow / Macd2Signal 斜率确认用 MACD 的周期设置。 10 / 15 / 3
CandleType 使用的 K 线类型(默认 1 分钟)。 TimeSpan.FromMinutes(1).TimeFrame()

说明

  • 由于示例基于收盘 K 线运行,止损和止盈使用 K 线最高价/最低价近似触发。
  • 如果无法获取投资组合权益,仓位大小会回落到策略 Volume 或交易品种的最小下单量。
  • 暂无 Python 版本,仅提供 C# 实现。
  • 请务必先做历史回测再用于真实交易,马丁格尔会显著放大风险。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// MACD-based martingale strategy. Uses two MACD indicators to detect turning points.
/// Doubles volume after a loss up to MaxDoublingCount times.
/// </summary>
public class ProMartMacdMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _maxDoublingCount;
	private readonly StrategyParam<int> _macd1Fast;
	private readonly StrategyParam<int> _macd1Slow;
	private readonly StrategyParam<int> _macd2Fast;
	private readonly StrategyParam<int> _macd2Slow;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _macd1History = new();
	private readonly List<decimal> _macd2History = new();

	private decimal _entryPrice;
	private bool _inPosition;
	private bool _isLong;
	private bool _lastTradeWasLoss;
	private int _martingaleCounter;
	private decimal _currentVolume;

	public int MaxDoublingCount
	{
		get => _maxDoublingCount.Value;
		set => _maxDoublingCount.Value = value;
	}

	public int Macd1Fast
	{
		get => _macd1Fast.Value;
		set => _macd1Fast.Value = value;
	}

	public int Macd1Slow
	{
		get => _macd1Slow.Value;
		set => _macd1Slow.Value = value;
	}

	public int Macd2Fast
	{
		get => _macd2Fast.Value;
		set => _macd2Fast.Value = value;
	}

	public int Macd2Slow
	{
		get => _macd2Slow.Value;
		set => _macd2Slow.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public ProMartMacdMartingaleStrategy()
	{
		_maxDoublingCount = Param(nameof(MaxDoublingCount), 2)
			.SetNotNegative()
			.SetDisplay("Max Doubling", "Maximum number of volume doublings after losses.", "Risk");

		_macd1Fast = Param(nameof(Macd1Fast), 5)
			.SetGreaterThanZero()
			.SetDisplay("MACD1 Fast", "Fast EMA period for the primary MACD.", "Signal");

		_macd1Slow = Param(nameof(Macd1Slow), 20)
			.SetGreaterThanZero()
			.SetDisplay("MACD1 Slow", "Slow EMA period for the primary MACD.", "Signal");

		_macd2Fast = Param(nameof(Macd2Fast), 10)
			.SetGreaterThanZero()
			.SetDisplay("MACD2 Fast", "Fast EMA period for the secondary MACD.", "Filter");

		_macd2Slow = Param(nameof(Macd2Slow), 15)
			.SetGreaterThanZero()
			.SetDisplay("MACD2 Slow", "Slow EMA period for the secondary MACD.", "Filter");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Data type used for signal generation.", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd1History.Clear();
		_macd2History.Clear();
		_entryPrice = 0;
		_inPosition = false;
		_isLong = false;
		_lastTradeWasLoss = false;
		_martingaleCounter = 0;
		_currentVolume = 0;
	}

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

		_macd1History.Clear();
		_macd2History.Clear();
		_inPosition = false;
		_isLong = false;
		_lastTradeWasLoss = false;
		_martingaleCounter = 0;
		_currentVolume = Volume;
		_entryPrice = 0;

		var macd1 = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = Macd1Slow },
			new ExponentialMovingAverage { Length = Macd1Fast });

		var macd2 = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = Macd2Slow },
			new ExponentialMovingAverage { Length = Macd2Fast });

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(macd1, macd2, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal macd1Value, decimal macd2Value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_macd1History.Add(macd1Value);
		_macd2History.Add(macd2Value);

		if (_macd1History.Count > 4)
			_macd1History.RemoveAt(0);
		if (_macd2History.Count > 3)
			_macd2History.RemoveAt(0);

		// Check exit for open position
		if (_inPosition)
		{
			var pnl = _isLong
				? candle.ClosePrice - _entryPrice
				: _entryPrice - candle.ClosePrice;

			// Detect reversal to exit
			var shouldExit = false;
			if (_macd1History.Count >= 3)
			{
				var m0 = _macd1History[^1];
				var m1 = _macd1History[^2];
				var m2 = _macd1History[^3];

				if (_isLong && m0 < m1 && m1 > m2)
					shouldExit = true;
				else if (!_isLong && m0 > m1 && m1 < m2)
					shouldExit = true;
			}

			if (shouldExit)
			{
				if (_isLong)
					SellMarket();
				else
					BuyMarket();

				_lastTradeWasLoss = pnl < 0;
				if (_lastTradeWasLoss && _martingaleCounter < MaxDoublingCount)
				{
					_currentVolume *= 2;
					_martingaleCounter++;
				}
				else
				{
					_currentVolume = Volume;
					_martingaleCounter = 0;
				}

				_inPosition = false;
				return;
			}
		}

		// Check entry
		if (!_inPosition && _macd1History.Count >= 3 && _macd2History.Count >= 2)
		{
			var m0 = _macd1History[^1];
			var m1 = _macd1History[^2];
			var m2 = _macd1History[^3];
			var f0 = _macd2History[^1];
			var f1 = _macd2History[^2];

			// MACD1 turns up from bottom + MACD2 confirms
			var buySignal = m0 > m1 && m1 < m2 && f1 > f0;
			var sellSignal = m0 < m1 && m1 > m2 && f1 < f0;

			if (buySignal && Position <= 0)
			{
				BuyMarket();
				_inPosition = true;
				_isLong = true;
				_entryPrice = candle.ClosePrice;
			}
			else if (sellSignal && Position >= 0)
			{
				SellMarket();
				_inPosition = true;
				_isLong = false;
				_entryPrice = candle.ClosePrice;
			}
		}
	}
}