Ver en GitHub

Martin For Small Deposits

Overview

This strategy reproduces the "Martin for small deposits" averaging expert in StockSharp. It looks at 15 completed candles and opens a position only when the newest close is below (for longs) or above (for shorts) the close recorded 14 bars earlier. All trades are executed at market using the high-level strategy API, and the logic is applied once per finished candle.

Entry Logic

  • A rolling buffer keeps the last 15 completed candle closes.
  • When there are no open or pending positions, the strategy compares the most recent close with the close 14 bars ago.
  • If the latest close is lower, a long grid is started; if it is higher, a short grid is started.
  • Trade volume for the first order equals Initial Volume. Subsequent entries on the same side use the martingale multiplier before being normalized to the instrument's volume step.

Position Management

  • While a position exists, the strategy waits for Bars To Skip finished candles before considering another averaging trade.
  • Additional orders are sent only if price moves against the current direction by at least Step (pips), converted to price units using the detected pip size.
  • Each execution updates internal statistics: aggregated volume, average entry price, lowest (for longs) or highest (for shorts) entry price, and the price of the most recent fill.
  • Volume never exceeds Max Volume or the exchange-defined maximum volume. If the normalized size falls below the minimum allowed volume, the order is skipped.

Exit Conditions

  • When the unrealized net profit (difference between the current close and the average entry price, multiplied by position volume) exceeds Min Profit, all open orders are flattened.
  • If Take Profit (pips) is greater than zero and price reaches that distance from the latest entry in the favorable direction, the entire grid is closed.
  • Closing requests are tracked; no new orders are sent until exit orders are fully filled. After a flat state is reached, all internal counters reset so the next signal starts a fresh grid.

Parameters

Name Default Description
Initial Volume 0.01 Base lot size for the first trade.
Take Profit (pips) 65 Distance in pips from the latest fill that triggers a full exit. Use 0 to disable this check.
Step (pips) 15 Adverse movement in pips required before averaging into the position.
Bars To Skip 45 Minimum number of finished candles to wait between averaging orders.
Increase Factor 1.7 Multiplier applied to the trade volume each time a new order is added on the same side.
Max Volume 6 Upper bound for aggregated volume (before normalization by market limits).
Min Profit 10 Profit target used to close the entire grid when the net profit exceeds this amount.
Candle Type 1 hour Timeframe used for candle subscription and signal calculations.

Implementation Notes

  • Pip size is derived from Security.PriceStep and decimal precision. For instruments quoted with 3 or 5 decimals, the code multiplies the price step by 10 to match the MQL concept of a pip.
  • Unrealized profit is approximated from price differences and does not include swap or commission adjustments that were present in the original expert.
  • Additional averaging trades are skipped while exit orders are active, preserving the sequential execution flow of the original MQL logic.
  • When Step (pips) is zero the strategy never averages; when Take Profit (pips) is zero only the Min Profit condition closes the grid.
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>
/// Martingale averaging strategy for small deposits.
/// </summary>
public class MartinForSmallDepositsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<int> _barsToSkip;
	private readonly StrategyParam<decimal> _increaseFactor;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _positionVolume;
	private decimal _avgPrice;
	private decimal _extremePrice;
	private decimal _lastEntryPrice;
	private int _currentTradeCount;
	private int _currentDirection;
	private int _barsSinceLastEntry;
	private decimal _pendingOpenVolume;
	private int _pendingOpenDirection;
	private decimal _pendingCloseVolume;
	private int _pendingCloseDirection;
	private decimal _pipSize;
	private readonly decimal[] _closeHistory = new decimal[15];
	private int _closeHistoryCount;
	private int _latestIndex = -1;

	/// <summary>
	/// Initializes a new instance of the <see cref="MartinForSmallDepositsStrategy"/> class.
	/// </summary>
	public MartinForSmallDepositsStrategy()
	{
		_initialVolume = Param(nameof(InitialVolume), 0.01m)
			.SetDisplay("Initial Volume", "Base lot size for the first order", "Position Sizing")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 200)
			.SetDisplay("Take Profit (pips)", "Take profit distance from the latest entry", "Risk")
			;

		_stepPips = Param(nameof(StepPips), 100)
			.SetDisplay("Step (pips)", "Adverse price move required to add a new trade", "Position Sizing")
			;

		_barsToSkip = Param(nameof(BarsToSkip), 100)
			.SetDisplay("Bars To Skip", "Number of finished candles to wait before averaging", "Timing")
			;

		_increaseFactor = Param(nameof(IncreaseFactor), 1.7m)
			.SetDisplay("Increase Factor", "Multiplier applied to the volume of each new order", "Position Sizing")
			;

		_maxVolume = Param(nameof(MaxVolume), 6m)
			.SetDisplay("Max Volume", "Maximum allowed aggregated volume", "Risk")
			;

		_minProfit = Param(nameof(MinProfit), 10m)
			.SetDisplay("Min Profit", "Net profit threshold to close all positions", "Risk")
			;

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

	/// <summary>
	/// Base lot size for the first trade in the sequence.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Price move in pips that triggers an averaging order.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Number of candles to wait between additional averaging trades.
	/// </summary>
	public int BarsToSkip
	{
		get => _barsToSkip.Value;
		set => _barsToSkip.Value = value;
	}

	/// <summary>
	/// Multiplier for the martingale position sizing.
	/// </summary>
	public decimal IncreaseFactor
	{
		get => _increaseFactor.Value;
		set => _increaseFactor.Value = value;
	}

	/// <summary>
	/// Maximum allowed aggregated volume across all open trades.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Profit target that closes the whole grid.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

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

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

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

		_positionVolume = 0m;
		_avgPrice = 0m;
		_extremePrice = 0m;
		_lastEntryPrice = 0m;
		_currentTradeCount = 0;
		_currentDirection = 0;
		_barsSinceLastEntry = 0;
		_pendingOpenVolume = 0m;
		_pendingOpenDirection = 0;
		_pendingCloseVolume = 0m;
		_pendingCloseDirection = 0;
		_pipSize = 0m;
		Array.Clear(_closeHistory, 0, _closeHistory.Length);
		_closeHistoryCount = 0;
		_latestIndex = -1;
	}

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

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

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

		// No bound indicators - skip formation check

		UpdateCloseHistory(candle.ClosePrice);

		var pipSize = EnsurePipSize();
		if (pipSize <= 0m)
			return;

		var stepDistance = StepPips > 0 ? StepPips * pipSize : 0m;
		var takeProfitDistance = TakeProfitPips > 0 ? TakeProfitPips * pipSize : 0m;

		var hasPosition = _positionVolume > 0m || Position != 0m || _pendingOpenDirection != 0 || _pendingCloseDirection != 0;

		if (!hasPosition)
		{
			if (!IsHistoryReady())
				return;

			var referenceClose = GetReferenceClose();
			if (candle.ClosePrice < referenceClose)
			{
				TryOpenBuy(candle.ClosePrice);
			}
			else if (candle.ClosePrice > referenceClose)
			{
				TryOpenSell(candle.ClosePrice);
			}

			return;
		}

		if (_pendingCloseDirection != 0)
			return;

		if (_positionVolume <= 0m || _currentDirection == 0)
			return;

		_barsSinceLastEntry++;

		var price = candle.ClosePrice;
		var openPnL = CalculateOpenProfit(price);

		if (openPnL > MinProfit)
		{
			CloseAllPositions();
			return;
		}

		if (_currentDirection > 0)
		{
			if (takeProfitDistance > 0m && price >= _lastEntryPrice + takeProfitDistance)
			{
				CloseAllPositions();
				return;
			}

			if (_barsSinceLastEntry <= BarsToSkip)
				return;

			if (stepDistance > 0m && _extremePrice - price > stepDistance)
				TryOpenBuy(price);
		}
		else if (_currentDirection < 0)
		{
			if (takeProfitDistance > 0m && price <= _lastEntryPrice - takeProfitDistance)
			{
				CloseAllPositions();
				return;
			}

			if (_barsSinceLastEntry <= BarsToSkip)
				return;

			if (stepDistance > 0m && price - _extremePrice > stepDistance)
				TryOpenSell(price);
		}
	}

	private void TryOpenBuy(decimal price)
	{
		if (_pendingOpenDirection != 0 && _pendingOpenDirection != 1)
			return;

		var volume = GetNextVolume(1);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_pendingOpenDirection = 1;
		_pendingOpenVolume += volume;
	}

	private void TryOpenSell(decimal price)
	{
		if (_pendingOpenDirection != 0 && _pendingOpenDirection != -1)
			return;

		var volume = GetNextVolume(-1);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_pendingOpenDirection = -1;
		_pendingOpenVolume += volume;
	}

	private void CloseAllPositions()
	{
		if (_pendingCloseDirection != 0)
			return;

		var volume = Position;

		if (volume > 0m)
		{
			SellMarket(volume);
			_pendingCloseDirection = -1;
			_pendingCloseVolume += volume;
		}
		else if (volume < 0m)
		{
			var closeVolume = -volume;
			BuyMarket(closeVolume);
			_pendingCloseDirection = 1;
			_pendingCloseVolume += closeVolume;
		}
		else if (_positionVolume > 0m)
		{
			if (_currentDirection > 0)
			{
				SellMarket(_positionVolume);
				_pendingCloseDirection = -1;
				_pendingCloseVolume += _positionVolume;
			}
			else if (_currentDirection < 0)
			{
				BuyMarket(_positionVolume);
				_pendingCloseDirection = 1;
				_pendingCloseVolume += _positionVolume;
			}
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		if (trade.Order == null)
			return;

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_pendingCloseDirection == 1)
			{
				ApplyClose(volume);
				_pendingCloseVolume -= volume;
				if (_pendingCloseVolume <= 0m)
					_pendingCloseDirection = 0;
				return;
			}

			if (_pendingOpenDirection == 1)
			{
				ApplyLongOpen(price, volume);
				_pendingOpenVolume -= volume;
				if (_pendingOpenVolume <= 0m)
					_pendingOpenDirection = 0;
				return;
			}

			if (_currentDirection < 0)
				ApplyClose(volume);
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_pendingCloseDirection == -1)
			{
				ApplyClose(volume);
				_pendingCloseVolume -= volume;
				if (_pendingCloseVolume <= 0m)
					_pendingCloseDirection = 0;
				return;
			}

			if (_pendingOpenDirection == -1)
			{
				ApplyShortOpen(price, volume);
				_pendingOpenVolume -= volume;
				if (_pendingOpenVolume <= 0m)
					_pendingOpenDirection = 0;
				return;
			}

			if (_currentDirection > 0)
				ApplyClose(volume);
		}
	}

	private void ApplyLongOpen(decimal price, decimal volume)
	{
		var previousVolume = _positionVolume;
		_positionVolume += volume;
		_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
		_extremePrice = previousVolume == 0m ? price : Math.Min(_extremePrice, price);
		_lastEntryPrice = price;
		_currentDirection = 1;
		_currentTradeCount++;
		_barsSinceLastEntry = 0;
	}

	private void ApplyShortOpen(decimal price, decimal volume)
	{
		var previousVolume = _positionVolume;
		_positionVolume += volume;
		_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
		_extremePrice = previousVolume == 0m ? price : Math.Max(_extremePrice, price);
		_lastEntryPrice = price;
		_currentDirection = -1;
		_currentTradeCount++;
		_barsSinceLastEntry = 0;
	}

	private void ApplyClose(decimal volume)
	{
		_positionVolume -= volume;
		if (_positionVolume <= 0m)
		{
			ResetPositionState();
		}
	}

	private void ResetPositionState()
	{
		_positionVolume = 0m;
		_avgPrice = 0m;
		_extremePrice = 0m;
		_lastEntryPrice = 0m;
		_currentTradeCount = 0;
		_currentDirection = 0;
		_barsSinceLastEntry = 0;
		_pendingOpenDirection = 0;
		_pendingOpenVolume = 0m;
		_pendingCloseDirection = 0;
		_pendingCloseVolume = 0m;
	}

	private decimal CalculateOpenProfit(decimal price)
	{
		if (_currentDirection > 0)
			return (price - _avgPrice) * _positionVolume;

		if (_currentDirection < 0)
			return (_avgPrice - price) * _positionVolume;

		return 0m;
	}

	private decimal GetNextVolume(int direction)
	{
		var baseVolume = InitialVolume;
		if (baseVolume <= 0m)
			return 0m;

		var depth = _currentDirection == direction ? _currentTradeCount : 0;
		decimal factor;
		if (IncreaseFactor <= 0m || depth == 0)
		{
			factor = 1m;
		}
		else
		{
			var raw = Math.Pow((double)IncreaseFactor, depth);
			if (double.IsInfinity(raw) || double.IsNaN(raw) || raw > (double)decimal.MaxValue)
				return 0m;
			factor = (decimal)raw;
		}
		var volume = baseVolume * factor;

		if (MaxVolume > 0m && volume > MaxVolume)
			volume = MaxVolume;

		volume = NormalizeVolume(volume);

		return volume;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var security = Security;
		if (security == null)
			return 0m;

		if (security.VolumeStep is decimal step && step > 0m)
		{
			var steps = decimal.Truncate(volume / step);
			volume = steps * step;
		}

		if (security.MinVolume is decimal min && volume < min)
			return 0m;

		if (security.MaxVolume is decimal max && volume > max)
			volume = max;

		return volume;
	}

	private decimal EnsurePipSize()
	{
		if (_pipSize > 0m)
			return _pipSize;

		var security = Security;
		if (security == null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step == 0m)
		{
			var decimals = security.Decimals;
			if (decimals != null)
			{
				step = (decimal)Math.Pow(10, -decimals.Value);
			}
		}

		if (step == 0m)
			step = 0.01m;

		var decimalsCount = security.Decimals ?? 0;
		_pipSize = (decimalsCount == 3 || decimalsCount == 5) ? step * 10m : step;

		if (_pipSize == 0m)
			_pipSize = step > 0m ? step : 0.01m;

		return _pipSize;
	}

	private void UpdateCloseHistory(decimal closePrice)
	{
		if (_closeHistory.Length == 0)
			return;

		_latestIndex = (_latestIndex + 1) % _closeHistory.Length;
		_closeHistory[_latestIndex] = closePrice;

		if (_closeHistoryCount < _closeHistory.Length)
			_closeHistoryCount++;
	}

	private bool IsHistoryReady()
	{
		return _closeHistoryCount >= _closeHistory.Length;
	}

	private decimal GetReferenceClose()
	{
		var index = (_latestIndex + 1) % _closeHistory.Length;
		return _closeHistory[index];
	}
}