Ver en GitHub

MartingailExpert v1.0 Stochastic Strategy (C#)

Overview

The MartingailExpert v1.0 Stochastic Strategy is a direct conversion of the MetaTrader 4 expert advisor MartingailExpert_v1_0_Stochastic.mq4. The strategy watches the %K/%D lines of the Stochastic Oscillator and opens a position when the previous completed bar produces a momentum confirmation above (for longs) or below (for shorts) configurable threshold zones. Once the first trade is live, the algorithm builds a martingale ladder of additional market orders whose volume grows geometrically and whose shared take-profit remains anchored to the price of the most recent addition.

The conversion relies entirely on StockSharp's high-level API: candle subscriptions, indicator binding, and built-in BuyMarket/SellMarket helpers. All code comments were rewritten in English and the implementation follows the tab-based indentation style required by the project guidelines.

Trading Logic

1. Entry signal

  1. The Stochastic Oscillator (Length = KPeriod, %K smoothing = Slowing, %D smoothing = DPeriod) is bound to the main candle subscription. Only finished candles are processed.
  2. The strategy mimics the original MQL call iStochastic(..., shift = 1) by storing the previous bar values of %K and %D. A long entry is triggered when K_prev > D_prev and D_prev > ZoneBuy. A short entry is triggered when K_prev < D_prev and D_prev < ZoneSell.
  3. The very first trade uses BuyVolume or SellVolume and resets any opposite direction state to avoid mixing long and short ladders.

2. Martingale averaging

  1. Whenever there is an open cluster (_buyOrderCount or _sellOrderCount greater than zero) the strategy monitors the candle's low (for longs) or high (for shorts).
  2. Step calculation
    • StepMode = 0: the next addition waits for the price to move by exactly StepPoints × PointSize against the latest filled order.
    • StepMode = 1: the distance becomes StepPoints + max(0, 2 × ordersCount − 2) points, matching the MQL expression step + OrdersTotal*2 - 2. The expression is multiplied by the instrument's point size (derived from Security.PriceStep and adjusted for 3/5 decimal FX quotes).
  3. If the candle violates the trigger level, the strategy sends an immediate market order whose volume equals previousVolume × Multiplier. Volumes are normalized to the instrument's VolumeStep, capped by VolumeMax (when available) and rounded down to zero if they fall below VolumeMin.
  4. After each addition, the shared target price is updated to lastEntryPrice ± ProfitFactorPoints × PointSize × orderCount depending on the direction.

3. Take-profit management

  1. The cluster is closed once the candle touches the shared target price (High >= target for longs, Low <= target for shorts). An additional check estimates the price-distance profit using the weighted average entry price to mirror the original OrderProfit() safeguard from MQL.
  2. All open orders are flattened with a single SellMarket(Math.Abs(Position)) or BuyMarket(Math.Abs(Position)) call. After a successful exit the internal martingale state is reset.
  3. If the external environment closes positions (manual intervention, stop-outs) the next candle with Position == 0 automatically clears the cached martingale state, keeping the strategy consistent.

4. Additional implementation notes

  • The point size is derived from Security.PriceStep. For 3- or 5-decimal FX symbols the value is multiplied by ten to emulate the MetaTrader concept of a pip (Point).
  • StartProtection() is invoked once in OnStarted so the platform can attach common protective behaviours (timeouts, heartbeat, etc.).
  • The strategy draws candles, the stochastic indicator, and own trades on a dedicated chart area for easier visual inspection during backtests.

Parameters

Name Type Default Description
StepPoints decimal 25 Distance in points before another martingale order is placed.
StepMode int 0 0 – fixed distance, 1 – fixed plus 2 × ordersCount − 2 points.
ProfitFactorPoints decimal 10 Points added (or subtracted) per open order to compute the cluster take profit.
Multiplier decimal 1.5 Multiplier applied to the last order volume for the next addition.
BuyVolume decimal 0.01 Volume of the initial long order.
SellVolume decimal 0.01 Volume of the initial short order.
KPeriod int 200 Lookback period of the stochastic oscillator.
DPeriod int 20 Smoothing period for the %D signal line.
Slowing int 20 Additional smoothing applied to %K (MetaTrader's slowing).
ZoneBuy decimal 50 Minimum %D value required to allow long entries.
ZoneSell decimal 50 Maximum %D value required to allow short entries.
CandleType DataType 5m time frame Candle type used for all indicator calculations.

Folder Structure

API/3991/
├── CS/
│   └── MartingailExpertV10StochasticStrategy.cs
├── README.md
├── README_zh.md
└── README_ru.md

Python implementation is intentionally omitted in accordance with the task requirements.

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>
/// Conversion of the "MartingailExpert v1.0 Stochastic" MetaTrader expert advisor.
/// Implements stochastic based entries with martingale averaging and cluster take profits.
/// </summary>
public class MartingailExpertV10StochasticStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stepPoints;
	private readonly StrategyParam<int> _stepMode;
	private readonly StrategyParam<decimal> _profitFactorPoints;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<decimal> _zoneBuy;
	private readonly StrategyParam<decimal> _zoneSell;
	private readonly StrategyParam<DataType> _candleType;

	private StochasticOscillator _stochastic;

	private decimal _pointSize;
	private decimal? _prevK;
	private decimal? _prevD;

	private decimal _buyLastPrice;
	private decimal _buyLastVolume;
	private decimal _buyTotalVolume;
	private decimal _buyWeightedSum;
	private int _buyOrderCount;
	private decimal _buyTakeProfit;

	private decimal _sellLastPrice;
	private decimal _sellLastVolume;
	private decimal _sellTotalVolume;
	private decimal _sellWeightedSum;
	private int _sellOrderCount;
	private decimal _sellTakeProfit;

	/// <summary>
	/// Distance in points that price has to travel against the latest entry before adding.
	/// </summary>
	public decimal StepPoints
	{
		get => _stepPoints.Value;
		set => _stepPoints.Value = value;
	}

	/// <summary>
	/// Step mode: 0 - fixed, 1 - fixed plus extra points per filled order.
	/// </summary>
	public int StepMode
	{
		get => _stepMode.Value;
		set => _stepMode.Value = value;
	}

	/// <summary>
	/// Profit target in points applied to every open order.
	/// </summary>
	public decimal ProfitFactorPoints
	{
		get => _profitFactorPoints.Value;
		set => _profitFactorPoints.Value = value;
	}

	/// <summary>
	/// Martingale multiplier for the next averaging order.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Stochastic %K lookback period.
	/// </summary>
	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic %D smoothing length.
	/// </summary>
	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	/// <summary>
	/// Minimum stochastic level that confirms long setups.
	/// </summary>
	public decimal ZoneBuy
	{
		get => _zoneBuy.Value;
		set => _zoneBuy.Value = value;
	}

	/// <summary>
	/// Maximum stochastic level that confirms short setups.
	/// </summary>
	public decimal ZoneSell
	{
		get => _zoneSell.Value;
		set => _zoneSell.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="MartingailExpertV10StochasticStrategy"/>.
	/// </summary>
	public MartingailExpertV10StochasticStrategy()
	{
		_stepPoints = Param(nameof(StepPoints), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Step", "Price step in points before averaging", "Martingale");

		_stepMode = Param(nameof(StepMode), 0)
			.SetDisplay("Step Mode", "0 - fixed step, 1 - step plus extra points per order", "Martingale");

		_profitFactorPoints = Param(nameof(ProfitFactorPoints), 300m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Factor", "Points multiplied by order count for take profit", "Martingale");

		_multiplier = Param(nameof(Multiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Multiplier", "Martingale multiplier for averaging", "Martingale");

		_kPeriod = Param(nameof(KPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Stochastic %K lookback", "Indicators");

		_dPeriod = Param(nameof(DPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Stochastic %D smoothing", "Indicators");

		_zoneBuy = Param(nameof(ZoneBuy), 50m)
			.SetDisplay("Zone Buy", "%D lower bound to allow buys", "Indicators");

		_zoneSell = Param(nameof(ZoneSell), 50m)
			.SetDisplay("Zone Sell", "%D upper bound to allow sells", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(10).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for processing", "General");

		Volume = 1;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_stochastic = null;
		_pointSize = 0m;
		_prevK = null;
		_prevD = null;
		ResetLongState();
		ResetShortState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_pointSize = Security?.PriceStep ?? 1m;
		if (_pointSize <= 0m) _pointSize = 1m;

		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = KPeriod;
		_stochastic.D.Length = DPeriod;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_stochastic, ProcessCandle)
			.Start();

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

			var indArea = CreateChartArea();
			if (indArea != null)
				DrawIndicator(indArea, _stochastic);
		}

		base.OnStarted2(time);
	}

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

		if (stochasticValue is not StochasticOscillatorValue stoch)
			return;

		if (stoch.K is not decimal currentK || stoch.D is not decimal currentD)
			return;

		if (!_stochastic.IsFormed)
		{
			_prevK = currentK;
			_prevD = currentD;
			return;
		}

		var tradingAllowed = IsFormedAndOnlineAndAllowTrading();

		ManageClusters(candle, tradingAllowed);

		if (!tradingAllowed)
		{
			_prevK = currentK;
			_prevD = currentD;
			return;
		}

		// Entry logic: stochastic crossover in oversold/overbought zones
		if (Position == 0m && _buyOrderCount == 0 && _sellOrderCount == 0
			&& _prevK is decimal prevK && _prevD is decimal prevD)
		{
			if (prevK > prevD && prevD > ZoneBuy)
			{
				OpenLong(candle.ClosePrice);
			}
			else if (prevK < prevD && prevD < ZoneSell)
			{
				OpenShort(candle.ClosePrice);
			}
		}

		_prevK = currentK;
		_prevD = currentD;
	}

	private void ManageClusters(ICandleMessage candle, bool tradingAllowed)
	{
		if (Position > 0m && _buyOrderCount > 0)
		{
			HandleLongCluster(candle, tradingAllowed);
		}
		else if (Position < 0m && _sellOrderCount > 0)
		{
			HandleShortCluster(candle, tradingAllowed);
		}
		else if (Position == 0m)
		{
			if (_buyOrderCount > 0 || _sellOrderCount > 0)
			{
				ResetLongState();
				ResetShortState();
			}
		}
	}

	private void HandleLongCluster(ICandleMessage candle, bool tradingAllowed)
	{
		if (!tradingAllowed || _pointSize <= 0m)
			return;

		// Check take profit first
		if (_buyTakeProfit > 0m && candle.HighPrice >= _buyTakeProfit)
		{
			SellMarket(Math.Abs(Position));
			ResetLongState();
			return;
		}

		// Average down
		var currentCount = Math.Max(1, _buyOrderCount);
		var stepPts = StepMode == 0
			? StepPoints
			: StepPoints + Math.Max(0m, currentCount * 2m - 2m);
		var addTrigger = _buyLastPrice - stepPts * _pointSize;

		if (_buyLastVolume > 0m && candle.LowPrice <= addTrigger)
		{
			var nextVolume = Math.Max(1m, Math.Round(_buyLastVolume * Multiplier));
			BuyMarket(nextVolume);

			var executionPrice = candle.ClosePrice;
			_buyLastVolume = nextVolume;
			_buyLastPrice = executionPrice;
			_buyTotalVolume += nextVolume;
			_buyWeightedSum += executionPrice * nextVolume;
			_buyOrderCount++;
			RecalcLongTp();
		}
	}

	private void HandleShortCluster(ICandleMessage candle, bool tradingAllowed)
	{
		if (!tradingAllowed || _pointSize <= 0m)
			return;

		// Check take profit first
		if (_sellTakeProfit > 0m && candle.LowPrice <= _sellTakeProfit)
		{
			BuyMarket(Math.Abs(Position));
			ResetShortState();
			return;
		}

		// Average up
		var currentCount = Math.Max(1, _sellOrderCount);
		var stepPts = StepMode == 0
			? StepPoints
			: StepPoints + Math.Max(0m, currentCount * 2m - 2m);
		var addTrigger = _sellLastPrice + stepPts * _pointSize;

		if (_sellLastVolume > 0m && candle.HighPrice >= addTrigger)
		{
			var nextVolume = Math.Max(1m, Math.Round(_sellLastVolume * Multiplier));
			SellMarket(nextVolume);

			var executionPrice = candle.ClosePrice;
			_sellLastVolume = nextVolume;
			_sellLastPrice = executionPrice;
			_sellTotalVolume += nextVolume;
			_sellWeightedSum += executionPrice * nextVolume;
			_sellOrderCount++;
			RecalcShortTp();
		}
	}

	private void OpenLong(decimal price)
	{
		BuyMarket(Volume);

		_buyLastPrice = price;
		_buyLastVolume = Volume;
		_buyTotalVolume = Volume;
		_buyWeightedSum = price * Volume;
		_buyOrderCount = 1;
		RecalcLongTp();

		ResetShortState();
	}

	private void OpenShort(decimal price)
	{
		SellMarket(Volume);

		_sellLastPrice = price;
		_sellLastVolume = Volume;
		_sellTotalVolume = Volume;
		_sellWeightedSum = price * Volume;
		_sellOrderCount = 1;
		RecalcShortTp();

		ResetLongState();
	}

	private void RecalcLongTp()
	{
		var avg = _buyTotalVolume > 0 ? _buyWeightedSum / _buyTotalVolume : _buyLastPrice;
		_buyTakeProfit = avg + ProfitFactorPoints * _pointSize;
	}

	private void RecalcShortTp()
	{
		var avg = _sellTotalVolume > 0 ? _sellWeightedSum / _sellTotalVolume : _sellLastPrice;
		_sellTakeProfit = avg - ProfitFactorPoints * _pointSize;
	}

	private void ResetLongState()
	{
		_buyLastPrice = 0m;
		_buyLastVolume = 0m;
		_buyTotalVolume = 0m;
		_buyWeightedSum = 0m;
		_buyOrderCount = 0;
		_buyTakeProfit = 0m;
	}

	private void ResetShortState()
	{
		_sellLastPrice = 0m;
		_sellLastVolume = 0m;
		_sellTotalVolume = 0m;
		_sellWeightedSum = 0m;
		_sellOrderCount = 0;
		_sellTakeProfit = 0m;
	}
}