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 线序列上计算两条 MACD:
- 持仓管理
- 通过市价单入场,并在每根已完成 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;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
MovingAverageConvergenceDivergence, ExponentialMovingAverage
)
class pro_mart_macd_martingale_strategy(Strategy):
"""MACD-based martingale strategy with dual MACD and volume doubling on loss."""
def __init__(self):
super(pro_mart_macd_martingale_strategy, self).__init__()
self._max_doubling = self.Param("MaxDoublingCount", 2) \
.SetDisplay("Max Doubling", "Max volume doublings after losses", "Risk")
self._macd1_fast = self.Param("Macd1Fast", 5) \
.SetGreaterThanZero() \
.SetDisplay("MACD1 Fast", "Fast EMA period for primary MACD", "Signal")
self._macd1_slow = self.Param("Macd1Slow", 20) \
.SetGreaterThanZero() \
.SetDisplay("MACD1 Slow", "Slow EMA period for primary MACD", "Signal")
self._macd2_fast = self.Param("Macd2Fast", 10) \
.SetGreaterThanZero() \
.SetDisplay("MACD2 Fast", "Fast EMA period for secondary MACD", "Filter")
self._macd2_slow = self.Param("Macd2Slow", 15) \
.SetGreaterThanZero() \
.SetDisplay("MACD2 Slow", "Slow EMA period for secondary MACD", "Filter")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Data type for signal generation", "General")
self._macd1_history = []
self._macd2_history = []
self._entry_price = 0.0
self._in_position = False
self._is_long = False
self._last_loss = False
self._mart_counter = 0
self._current_volume = 0.0
@property
def MaxDoublingCount(self):
return self._max_doubling.Value
@property
def Macd1Fast(self):
return self._macd1_fast.Value
@property
def Macd1Slow(self):
return self._macd1_slow.Value
@property
def Macd2Fast(self):
return self._macd2_fast.Value
@property
def Macd2Slow(self):
return self._macd2_slow.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(pro_mart_macd_martingale_strategy, self).OnStarted2(time)
self._macd1_history = []
self._macd2_history = []
self._in_position = False
self._is_long = False
self._last_loss = False
self._mart_counter = 0
self._current_volume = float(self.Volume) if self.Volume > 0 else 1.0
self._entry_price = 0.0
slow1 = ExponentialMovingAverage()
slow1.Length = self.Macd1Slow
fast1 = ExponentialMovingAverage()
fast1.Length = self.Macd1Fast
macd1 = MovingAverageConvergenceDivergence(slow1, fast1)
slow2 = ExponentialMovingAverage()
slow2.Length = self.Macd2Slow
fast2 = ExponentialMovingAverage()
fast2.Length = self.Macd2Fast
macd2 = MovingAverageConvergenceDivergence(slow2, fast2)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(macd1, macd2, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, macd1)
self.DrawOwnTrades(area)
def process_candle(self, candle, macd1_val, macd2_val):
if candle.State != CandleStates.Finished:
return
m1 = float(macd1_val)
m2 = float(macd2_val)
self._macd1_history.append(m1)
self._macd2_history.append(m2)
if len(self._macd1_history) > 4:
self._macd1_history.pop(0)
if len(self._macd2_history) > 3:
self._macd2_history.pop(0)
close = float(candle.ClosePrice)
# Check exit
if self._in_position:
pnl = close - self._entry_price if self._is_long else self._entry_price - close
should_exit = False
if len(self._macd1_history) >= 3:
m0 = self._macd1_history[-1]
m1p = self._macd1_history[-2]
m2p = self._macd1_history[-3]
if self._is_long and m0 < m1p and m1p > m2p:
should_exit = True
elif not self._is_long and m0 > m1p and m1p < m2p:
should_exit = True
if should_exit:
if self._is_long:
self.SellMarket()
else:
self.BuyMarket()
self._last_loss = pnl < 0
if self._last_loss and self._mart_counter < self.MaxDoublingCount:
self._current_volume *= 2.0
self._mart_counter += 1
else:
self._current_volume = float(self.Volume) if self.Volume > 0 else 1.0
self._mart_counter = 0
self._in_position = False
return
# Check entry
if not self._in_position and len(self._macd1_history) >= 3 and len(self._macd2_history) >= 2:
m0 = self._macd1_history[-1]
m1p = self._macd1_history[-2]
m2p = self._macd1_history[-3]
f0 = self._macd2_history[-1]
f1 = self._macd2_history[-2]
buy_signal = m0 > m1p and m1p < m2p and f1 > f0
sell_signal = m0 < m1p and m1p > m2p and f1 < f0
if buy_signal and self.Position <= 0:
self.BuyMarket()
self._in_position = True
self._is_long = True
self._entry_price = close
elif sell_signal and self.Position >= 0:
self.SellMarket()
self._in_position = True
self._is_long = False
self._entry_price = close
def OnReseted(self):
super(pro_mart_macd_martingale_strategy, self).OnReseted()
self._macd1_history = []
self._macd2_history = []
self._entry_price = 0.0
self._in_position = False
self._is_long = False
self._last_loss = False
self._mart_counter = 0
self._current_volume = 0.0
def CreateClone(self):
return pro_mart_macd_martingale_strategy()