在 GitHub 上查看

MartingaleEA-5 Levels 策略(StockSharp)

MartingaleEA-5 Levels 策略 将 MetaTrader 5 专家顾问 “MartingaleEA-5 Levels” 迁移到 StockSharp 高级 API。系统会监控已有的多头或空头仓位,当价格向不利方向移动时构建最多五层的马丁格尔加仓网格。所有计算都在收盘后的 K 线完成,确保历史回测与实时交易具有一致的行为。

交易逻辑

  1. 监控现有仓位:策略假设已经存在初始的多头或空头仓位,可以手动下单或由其他模型开出第一笔交易。
  2. 判断不利波动:每根完成的 K 线都会计算当前价格距离最差开仓价(多头取最高价,空头取最低价)的幅度。
  3. 马丁格尔加仓:当该方向的浮动盈亏为负且不利波动超过累计距离阈值时,策略会按照 VolumeMultiplier 倍数递增成交量,依次发送市价单加仓。参数最多提供五个层级,实际使用的数量由 MaxAdditions 控制。
  4. 利润/亏损触发关闭:在加仓组持有期间,策略持续累加该方向的未实现盈亏。一旦达到 TakeProfitCurrency 或跌破 StopLossCurrency,便使用市价单一次性平掉该方向的全部仓位,并重置马丁格尔计数。
  5. 数量归一化:所有下单量都会根据交易品种的 VolumeStepMinVolumeMaxVolume 进行调整,避免发送交易所无法接受的数量。

参数

名称 说明 默认值
EnableMartingale 是否启用马丁格尔加仓与平仓逻辑。 true
VolumeMultiplier 每次加仓相对于上一笔成交量的放大倍数。 2.0
MaxAdditions 每个方向允许的最大加仓次数(最多五层)。 4
Level1DistancePips 触发第二笔订单的不利波动距离(以点/点值表示)。 300
Level2DistancePips 触发第三笔订单所需的附加距离。 400
Level3DistancePips 触发第四笔订单所需的附加距离。 500
Level4DistancePips 触发第五笔订单所需的附加距离。 600
Level5DistancePips 触发第六笔订单所需的附加距离(若仍允许加仓)。 700
TakeProfitCurrency 浮动盈利达到该货币金额时平掉整组仓位。 200
StopLossCurrency 浮动亏损跌破该货币金额时强制平仓。 -500
CandleType 用于评估的时间框架(默认 1 分钟 K 线)。 TimeFrame(1m)

点值换算:所有距离会乘以品种的价格步长(PriceStepMinPriceStep)。若品种报价使用分数点,请相应调整参数。

使用建议

  • 该实现与原始 EA 一致,假设同一时刻仅有一个方向的网格在运行。如同时持有多空仓位,系统会分别跟踪各自的浮动盈亏。
  • 策略仅在 K 线收盘时做出反应,请选择与你所需响应速度匹配的时间框架。较低周期更接近逐笔行情的行为。
  • 马丁格尔方法风险极高。投入真实资金前务必使用真实滑点与手续费进行充分回测,并设置合理的止损水平。
  • 按需求仅提供 C# 高级 API 版本,目前未创建 Python 版本或对应目录。
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>
/// Martingale averaging strategy converted from "MartingaleEA-5 Levels".
/// Opens initial position on simple momentum, then averages down with
/// increasing lot sizes up to 5 levels. Closes when floating profit
/// reaches target or stop threshold.
/// </summary>
public class MartingaleEa5LevelsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<int> _maxAdditions;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _stopLossPercent;

	private SimpleMovingAverage _sma;
	private decimal? _prevClose;
	private decimal? _prevMa;

	private readonly List<(decimal price, decimal vol)> _entries = new();
	private int _additions;
	private decimal _lastVolume;
	private Sides? _activeSide;
	private int _candleCount;
	private int _lastOrderCandle;

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

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	public int MaxAdditions
	{
		get => _maxAdditions.Value;
		set => _maxAdditions.Value = value;
	}

	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	public MartingaleEa5LevelsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "SMA period for entry signal", "Indicators");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Multiplier for each martingale level", "Money Management");

		_maxAdditions = Param(nameof(MaxAdditions), 4)
			.SetDisplay("Max Additions", "Maximum martingale additions", "Money Management");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Floating profit % to close group", "Risk");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Floating loss % to close group", "Risk");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = default;
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_sma = new SimpleMovingAverage { Length = MaPeriod };
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;

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

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

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

		if (!_sma.IsFormed)
		{
			_prevClose = candle.ClosePrice;
			_prevMa = smaValue;
			return;
		}

		_candleCount++;

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Cooldown: allow at most one order per 100 candles
		var cooldownPassed = (_candleCount - _lastOrderCandle) >= 100;

		// Check martingale closure first
		if (_entries.Count > 0)
		{
			var floatingPnl = CalculateFloatingPnl(close);
			var totalCost = CalculateTotalCost();

			if (totalCost > 0)
			{
				var pnlPercent = floatingPnl / totalCost * 100m;

				if (cooldownPassed && (pnlPercent >= TakeProfitPercent || pnlPercent <= -StopLossPercent))
				{
					// Close entire position
					if (Position > 0)
						SellMarket(Position);
					else if (Position < 0)
						BuyMarket(Math.Abs(Position));

					_lastOrderCandle = _candleCount;
					_entries.Clear();
					_additions = 0;
					_lastVolume = 0;
					_activeSide = null;

					_prevClose = close;
					_prevMa = smaValue;
					return;
				}
			}

			// Check for martingale additions
			if (cooldownPassed && _additions < MaxAdditions)
			{
				var avgPrice = CalculateAvgPrice();
				var adversePercent = _activeSide == Sides.Buy
					? (avgPrice - close) / avgPrice * 100m
					: (close - avgPrice) / avgPrice * 100m;

				// Add at each 0.3% adverse move beyond previous level
				var threshold = 0.3m * (_additions + 1);
				if (adversePercent >= threshold)
				{
					var nextVol = _lastVolume * VolumeMultiplier;
					if (nextVol < 1) nextVol = 1;

					if (_activeSide == Sides.Buy)
					{
						BuyMarket(nextVol);
						_entries.Add((close, nextVol));
					}
					else
					{
						SellMarket(nextVol);
						_entries.Add((close, nextVol));
					}

					_lastVolume = nextVol;
					_additions++;
					_lastOrderCandle = _candleCount;
				}
			}
		}

		// Initial entry signal: MA crossover
		if (cooldownPassed && _prevClose != null && _prevMa != null && _activeSide == null)
		{
			var buySignal = _prevClose.Value < _prevMa.Value && close > smaValue;
			var sellSignal = _prevClose.Value > _prevMa.Value && close < smaValue;

			if (buySignal)
			{
				BuyMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Buy;
				_lastOrderCandle = _candleCount;
			}
			else if (sellSignal)
			{
				SellMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Sell;
				_lastOrderCandle = _candleCount;
			}
		}

		_prevClose = close;
		_prevMa = smaValue;
	}

	private decimal CalculateFloatingPnl(decimal currentPrice)
	{
		var pnl = 0m;
		foreach (var (price, vol) in _entries)
		{
			if (_activeSide == Sides.Buy)
				pnl += (currentPrice - price) * vol;
			else
				pnl += (price - currentPrice) * vol;
		}
		return pnl;
	}

	private decimal CalculateTotalCost()
	{
		var cost = 0m;
		foreach (var (price, vol) in _entries)
			cost += price * vol;
		return cost;
	}

	private decimal CalculateAvgPrice()
	{
		var totalVol = 0m;
		var totalCost = 0m;
		foreach (var (price, vol) in _entries)
		{
			totalVol += vol;
			totalCost += price * vol;
		}
		return totalVol > 0 ? totalCost / totalVol : 0;
	}
}