Ver en GitHub

DoubleUp2 Martingale Strategy

Overview

The DoubleUp2 Martingale strategy reproduces the original MetaTrader expert by combining the Commodity Channel Index (CCI) and the MACD oscillator. Trades are opened only when both indicators reach extreme levels in the same direction. Position sizing follows a martingale scheme where the volume doubles after a losing trade. Profitable trades are partially locked by closing the position once price travels a configurable distance in favor of the position.

How It Works

  1. Subscribe to a single candle series (default 1 minute) and calculate CCI and MACD on every completed bar.
  2. Detect extreme momentum:
    • Enter short when both CCI and MACD exceed the positive threshold.
    • Enter long when both drop below the negative threshold.
  3. Before reversing, the current position is closed and the martingale step is updated based on the simulated profit of the last trade.
  4. Trade volume equals the base volume derived from account equity divided by a balance divisor, multiplied by the martingale factor raised to the current step.
  5. Lock in profits by closing any open position once price advances by a predefined number of points from the last entry. Winning exits increase the martingale step by two to match the original EA behaviour.

Parameters

Name Description Default
CciPeriod Lookback period for the CCI indicator. 8
MacdFastPeriod Fast EMA length for MACD. 13
MacdSlowPeriod Slow EMA length for MACD. 33
MacdSignalPeriod Signal EMA length for MACD smoothing. 2
Threshold Absolute indicator threshold that must be exceeded to trigger entries. 230
ExitDistancePoints Profit distance in points that triggers position closure. 120
BalanceDivisor Divisor applied to portfolio equity to obtain base volume. 50001
MinimumVolume Lower limit for the computed trade volume. 0.1
MartingaleMultiplier Multiplier applied to position size after each losing close. 2
CandleType Candle timeframe used for all calculations. 1 minute

Notes

  • The martingale logic increases position size after losses and resets after profitable reversals, mirroring the source MQL logic.
  • Price step information is used to convert the exit distance (points) into absolute price units. If the instrument does not provide a price step, a value of 1 is used.
  • The strategy expects a single instrument and does not place simultaneous long and short positions.
using System;

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

namespace StockSharp.Samples.Strategies;

public class DoubleUp2MartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _cooldownCandles;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _hasPrev;
	private int _cooldownRemaining;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int CooldownCandles { get => _cooldownCandles.Value; set => _cooldownCandles.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public DoubleUp2MartingaleStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 20).SetDisplay("Fast EMA", "Fast EMA period", "Indicators");
		_slowPeriod = Param(nameof(SlowPeriod), 80).SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
		_cooldownCandles = Param(nameof(CooldownCandles), 100).SetDisplay("Cooldown", "Candles between signals", "General");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevFast = default;
		_prevSlow = default;
		_hasPrev = default;
		_cooldownRemaining = default;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0;
		_prevSlow = 0;
		_hasPrev = false;
		_cooldownRemaining = 0;

		var fast = new ExponentialMovingAverage { Length = FastPeriod };
		var slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fast, slow, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_hasPrev) { _prevFast = fast; _prevSlow = slow; _hasPrev = true; return; }

		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
			_prevFast = fast;
			_prevSlow = slow;
			return;
		}

		if (_prevFast <= _prevSlow && fast > slow && Position <= 0)
		{
			if (Position < 0) BuyMarket();
			BuyMarket();
			_cooldownRemaining = CooldownCandles;
		}
		else if (_prevFast >= _prevSlow && fast < slow && Position >= 0)
		{
			if (Position > 0) SellMarket();
			SellMarket();
			_cooldownRemaining = CooldownCandles;
		}
		_prevFast = fast;
		_prevSlow = slow;
	}
}