Ver no GitHub

Master MM Droid Strategy

Overview

The Master MM Droid strategy is a multi-module port of the original MetaTrader 5 expert advisor. The StockSharp implementation keeps the core ideas of the legacy robot while using the high-level API for candle subscriptions, indicator binding and order management. Four independent money-management blocks can be switched on or off, allowing the strategy to mix momentum entries with scheduled breakout orders and weekly gap plays.

Modules

  1. RSI Block
    • Uses a 14-period Relative Strength Index on the configured candle type.
    • Enters long when RSI crosses up from the oversold threshold and short when it crosses down from the overbought level.
    • Allows pyramiding with a configurable number of additional entries separated by a fixed price step.
    • Applies a fixed initial stop based on point distance and activates a trailing stop once the position is open.
  2. Box Breakout Block
    • Rebuilds breakout boxes three times per day (session-shifted hours 6, 12 and 20 by default).
    • Places bracketed stop orders above the session high and below the session low with a configurable buffer.
    • Clears any pending orders and positions at session resets (hours 0, 10 and 16), mimicking the original expert behaviour.
  3. Weekly Breakout Block
    • Tracks Monday price action and stores the running high/low of the first part of the session.
    • Places symmetrical stop orders within a limited activation window (StartHourWeeklySetupEndHour) so the week starts with an OCO breakout.
    • Forces a flat state on Friday evenings to avoid weekend exposure.
  4. Gap Block
    • Compares the new daily open with the previous day high/low (using the shifted calendar).
    • Buys strong gap-down openings and sells strong gap-up openings.
    • Sets a protective stop at a configurable distance and hands further management to the trailing engine.

Parameters

Name Description
CandleType Time frame used for indicator calculations and time-window checks.
TimeShiftHours Session shift applied to candle timestamps so the hourly schedule matches the original EA.
StartHour Base Monday start hour for the weekly module (before applying the shift).
EnableRsiModule, EnableBoxModule, EnableWeeklyModule, EnableGapModule Toggles for the four independent blocks.
RsiPeriod, RsiLowerLevel, RsiUpperLevel RSI calculation and trigger levels.
RsiMaxEntries, RsiPyramidPoints Pyramiding controls for the RSI block.
RsiStopLossPoints, RsiTrailingPoints Initial and trailing stop sizes (in points) for RSI-driven trades.
BoxEntryPoints, BoxTrailingPoints Breakout buffer and trailing distance for the box orders.
WeeklyEntryPoints, WeeklySetupEndHour, WeeklyTrailingPoints Weekly breakout configuration.
GapStopLossPoints, GapTrailingPoints Gap module protective stop and trailing distance.

All point-based parameters are multiplied by the instrument TickSize to obtain price offsets so that the strategy adapts to different symbols.

Trading Logic

  • Indicator Binding: A single RSI indicator is bound to the candle subscription. Every finished candle triggers ProcessCandle, which dispatches the values to the four module handlers.
  • Daily State Tracking: The strategy aggregates open/high/low for each shifted day to support the gap logic and to keep a historical reference for the weekly module.
  • Order Placement: Orders are submitted through BuyMarket, SellMarket, BuyStop, SellStop in line with the high-level API best practices. Scheduled modules always cancel active orders before re-arming to avoid duplicates.
  • Trailing Management: Once a position is active, _activeTrailingPoints stores the module-specific distance. The UpdateTrailing method moves stop orders only in the favourable direction.

Risk Management

  • Only market orders created by the RSI and gap modules are protected by an immediate stop calculated in points.
  • Breakout modules rely on the trailing engine after activation; they can be combined with external portfolio protection if required.
  • Calling ClosePosition() is the canonical way to flatten, preserving compatibility with StockSharp risk tools.

Usage Notes

  • The strategy operates on a single security and uses the global Volume value for sizing. Adjust portfolio protection separately if you need per-position risk limits.
  • Session times are evaluated after applying the TimeShiftHours. For example, with the default value 2, the box reset at hour 0 corresponds to 02:00 server time.
  • Because StockSharp strategies manage net positions, simultaneous long/short baskets (possible in hedging MetaTrader accounts) are consolidated. This is the main behavioural difference from the original EA and should be considered during validation.

Logging and Monitoring

  • Each module resets its internal flags once the position returns to flat, helping operators diagnose which block produced a trade.
  • Add optional charting or logging through StockSharp facilities if detailed analytics are required.
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>
/// Port of the Master MM Droid strategy with modular money management blocks.
/// Uses RSI crossover signals with pyramiding, daily gap detection, and
/// box/weekly breakout modules - all implemented via candle-based checks.
/// </summary>
public class MasterMmDroidStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiLowerLevel;
	private readonly StrategyParam<decimal> _rsiUpperLevel;
	private readonly StrategyParam<int> _rsiMaxEntries;
	private readonly StrategyParam<decimal> _rsiPyramidSteps;
	private readonly StrategyParam<decimal> _stopLossSteps;
	private readonly StrategyParam<decimal> _trailingSteps;
	private readonly StrategyParam<int> _boxLookback;
	private readonly StrategyParam<decimal> _boxEntrySteps;

	private RelativeStrengthIndex _rsi = null!;

	private decimal _previousRsi;
	private bool _hasPreviousRsi;
	private decimal? _lastEntryPrice;
	private int _entryCount;

	private decimal? _activeStopPrice;
	private decimal _bestPrice;

	private decimal _boxHigh;
	private decimal _boxLow;
	private int _boxBarsCount;

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

	/// <summary>
	/// RSI period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI oversold level.
	/// </summary>
	public decimal RsiLowerLevel
	{
		get => _rsiLowerLevel.Value;
		set => _rsiLowerLevel.Value = value;
	}

	/// <summary>
	/// RSI overbought level.
	/// </summary>
	public decimal RsiUpperLevel
	{
		get => _rsiUpperLevel.Value;
		set => _rsiUpperLevel.Value = value;
	}

	/// <summary>
	/// Maximum pyramiding entries.
	/// </summary>
	public int RsiMaxEntries
	{
		get => _rsiMaxEntries.Value;
		set => _rsiMaxEntries.Value = value;
	}

	/// <summary>
	/// Price steps between pyramid entries.
	/// </summary>
	public decimal RsiPyramidSteps
	{
		get => _rsiPyramidSteps.Value;
		set => _rsiPyramidSteps.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public decimal StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingSteps
	{
		get => _trailingSteps.Value;
		set => _trailingSteps.Value = value;
	}

	/// <summary>
	/// Number of candles for box high/low calculation.
	/// </summary>
	public int BoxLookback
	{
		get => _boxLookback.Value;
		set => _boxLookback.Value = value;
	}

	/// <summary>
	/// Breakout distance above/below the box in price steps.
	/// </summary>
	public decimal BoxEntrySteps
	{
		get => _boxEntrySteps.Value;
		set => _boxEntrySteps.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MasterMmDroidStrategy"/>.
	/// </summary>
	public MasterMmDroidStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation period", "RSI")
			.SetOptimize(7, 21, 7);

		_rsiLowerLevel = Param(nameof(RsiLowerLevel), 25m)
			.SetDisplay("RSI Oversold", "RSI oversold threshold", "RSI");

		_rsiUpperLevel = Param(nameof(RsiUpperLevel), 75m)
			.SetDisplay("RSI Overbought", "RSI overbought threshold", "RSI");

		_rsiMaxEntries = Param(nameof(RsiMaxEntries), 2)
			.SetGreaterThanZero()
			.SetDisplay("Max Entries", "Maximum pyramiding steps", "RSI");

		_rsiPyramidSteps = Param(nameof(RsiPyramidSteps), 250m)
			.SetGreaterThanZero()
			.SetDisplay("Pyramid Steps", "Price steps between entries", "RSI");

		_stopLossSteps = Param(nameof(StopLossSteps), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Steps", "Stop-loss distance in price steps", "Risk");

		_trailingSteps = Param(nameof(TrailingSteps), 700m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Steps", "Trailing distance in price steps", "Risk");

		_boxLookback = Param(nameof(BoxLookback), 16)
			.SetGreaterThanZero()
			.SetDisplay("Box Lookback", "Candles for box high/low", "Box");

		_boxEntrySteps = Param(nameof(BoxEntrySteps), 180m)
			.SetGreaterThanZero()
			.SetDisplay("Box Entry Steps", "Breakout distance in price steps", "Box");
	}

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

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

		_previousRsi = 0m;
		_hasPreviousRsi = false;
		_lastEntryPrice = null;
		_entryCount = 0;
		_activeStopPrice = null;
		_bestPrice = 0m;
		_boxHigh = 0m;
		_boxLow = decimal.MaxValue;
		_boxBarsCount = 0;
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

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

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

		var step = Security?.PriceStep ?? 1m;
		var enteredThisCandle = false;

		// Update box tracking
		UpdateBox(candle);

		// Manage trailing stop
		ManageTrailing(candle, step);

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousRsi = rsiValue;
			_hasPreviousRsi = true;
			return;
		}

		// Check box breakout entries
		if (Position == 0 && _boxBarsCount >= BoxLookback)
		{
			var boxOffset = BoxEntrySteps * step;
			if (candle.ClosePrice > _boxHigh + boxOffset)
			{
				BuyMarket(Volume);
				_lastEntryPrice = candle.ClosePrice;
				_entryCount = 1;
				_activeStopPrice = candle.ClosePrice - StopLossSteps * step;
				_bestPrice = candle.ClosePrice;
				enteredThisCandle = true;
			}
			else if (candle.ClosePrice < _boxLow - boxOffset)
			{
				SellMarket(Volume);
				_lastEntryPrice = candle.ClosePrice;
				_entryCount = 1;
				_activeStopPrice = candle.ClosePrice + StopLossSteps * step;
				_bestPrice = candle.ClosePrice;
				enteredThisCandle = true;
			}
		}

		// RSI crossover signals
		if (!enteredThisCandle && _hasPreviousRsi && _rsi.IsFormed)
		{
			var rsiCrossUp = _previousRsi <= RsiLowerLevel && rsiValue > RsiLowerLevel;
			var rsiCrossDown = _previousRsi >= RsiUpperLevel && rsiValue < RsiUpperLevel;

			if (rsiCrossUp && Position <= 0)
			{
				var vol = Volume + (Position < 0 ? Math.Abs(Position) : 0);
				BuyMarket(vol);
				_lastEntryPrice = candle.ClosePrice;
				_entryCount = 1;
				_activeStopPrice = candle.ClosePrice - StopLossSteps * step;
				_bestPrice = candle.ClosePrice;
			}
			else if (rsiCrossDown && Position >= 0)
			{
				var vol = Volume + (Position > 0 ? Position : 0);
				SellMarket(vol);
				_lastEntryPrice = candle.ClosePrice;
				_entryCount = 1;
				_activeStopPrice = candle.ClosePrice + StopLossSteps * step;
				_bestPrice = candle.ClosePrice;
			}

			// Pyramiding
			var pyramidDist = RsiPyramidSteps * step;
			if (Position > 0 && _entryCount < RsiMaxEntries && _lastEntryPrice.HasValue)
			{
				if (candle.ClosePrice >= _lastEntryPrice.Value + pyramidDist)
				{
					BuyMarket(Volume);
					_lastEntryPrice = candle.ClosePrice;
					_entryCount++;
				}
			}
			else if (Position < 0 && _entryCount < RsiMaxEntries && _lastEntryPrice.HasValue)
			{
				if (candle.ClosePrice <= _lastEntryPrice.Value - pyramidDist)
				{
					SellMarket(Volume);
					_lastEntryPrice = candle.ClosePrice;
					_entryCount++;
				}
			}
		}

		_previousRsi = rsiValue;
		_hasPreviousRsi = true;
	}

	private void UpdateBox(ICandleMessage candle)
	{
		_boxBarsCount++;
		if (_boxBarsCount <= BoxLookback)
		{
			_boxHigh = Math.Max(_boxHigh, candle.HighPrice);
			_boxLow = Math.Min(_boxLow, candle.LowPrice);
		}
		else
		{
			// Shift the window - approximate by using recent candle
			_boxHigh = Math.Max(_boxHigh, candle.HighPrice);
			_boxLow = Math.Min(_boxLow, candle.LowPrice);
		}
	}

	private void ManageTrailing(ICandleMessage candle, decimal step)
	{
		if (Position == 0)
		{
			_activeStopPrice = null;
			return;
		}

		if (!_activeStopPrice.HasValue)
			return;

		var trailDist = TrailingSteps * step;

		if (Position > 0)
		{
			if (candle.ClosePrice > _bestPrice)
				_bestPrice = candle.ClosePrice;

			var trailStop = _bestPrice - trailDist;
			if (trailStop > _activeStopPrice.Value)
				_activeStopPrice = trailStop;

			if (candle.LowPrice <= _activeStopPrice.Value)
			{
				SellMarket(Position);
				_activeStopPrice = null;
				_lastEntryPrice = null;
				_entryCount = 0;
			}
		}
		else
		{
			if (candle.ClosePrice < _bestPrice || _bestPrice == 0m)
				_bestPrice = candle.ClosePrice;

			var trailStop = _bestPrice + trailDist;
			if (trailStop < _activeStopPrice.Value)
				_activeStopPrice = trailStop;

			if (candle.HighPrice >= _activeStopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				_activeStopPrice = null;
				_lastEntryPrice = null;
				_entryCount = 0;
			}
		}
	}
}