Ver no GitHub

Up3x1 Dynamic Sizing Strategy

Overview

  • Conversion of the MetaTrader 5 expert advisor up3x1.mq5 into the StockSharp high-level API.
  • Trades a triple exponential moving average (EMA) crossover with stop loss, take profit and trailing stop management.
  • Processes only finished candles to emulate the original iTickVolume(0) > 1 guard that forced one decision per bar.
  • Default candle series is 1 hour, but the timeframe is configurable through the CandleType parameter.

Trading Logic

  1. Indicators
    • Fast EMA (FastPeriod, default 24).
    • Medium EMA (MediumPeriod, default 60).
    • Slow EMA (SlowPeriod, default 120).
  2. Long entry
    • Previous bar: fast EMA below medium EMA and medium below slow (EMAfast₍t-1₎ < EMAmedium₍t-1₎ < EMAslow₍t-1₎).
    • Current bar: medium EMA below fast EMA while fast stays below slow (EMAmedium₍t₎ < EMAfast₍t₎ < EMAslow₍t₎).
  3. Short entry
    • Previous bar: fast EMA above medium EMA and medium above slow (EMAfast₍t-1₎ > EMAmedium₍t-1₎ > EMAslow₍t-1₎).
    • Current bar: medium EMA crosses above fast EMA while both remain above the slow EMA (EMAmedium₍t₎ > EMAfast₍t₎ > EMAslow₍t₎).
  4. Exit logic for both directions
    • Take profit when price advances by TakeProfitOffset from the entry (using candle high for longs, low for shorts).
    • Stop loss when price retraces by StopLossOffset from the entry (using candle low for longs, high for shorts).
    • Trailing stop activates once the position moves in favor by more than TrailingStopOffset and then follows price at that fixed distance, evaluated on candle extremes.
    • Fallback exit when the fast EMA crosses back below the medium EMA while both stay above the slow EMA (mirrors the ma_one_1 > ma_two_1 > ma_three_1 check from the MQL version).

Position Sizing and Risk Management

  • RiskFraction (default 0.02) multiplies the current portfolio value to approximate the original FreeMargin * 0.02 / 1000 lot sizing.
  • BaseVolume (default 0.1) acts as a fallback whenever portfolio data is unavailable or the calculated size becomes non-positive.
  • After more than one losing exit the volume is reduced by volume * losses / 3, mimicking the cumulative losses counter from the script (the counter is not reset after profitable trades, as in the original code).
  • Volumes are rounded down to Security.VolumeStep, clamped by Security.MinVolume / Security.MaxVolume, and dropped to zero if the instrument minimum cannot be met.

Parameters

Parameter Default Description
FastPeriod 24 Length of the fastest EMA.
MediumPeriod 60 Length of the medium EMA.
SlowPeriod 120 Length of the slow EMA used as long-term trend filter.
TakeProfitOffset 0.015 Absolute price distance for the take profit order (adapt to instrument quoting).
StopLossOffset 0.01 Absolute price distance for the stop loss order.
TrailingStopOffset 0.004 Trailing distance that locks gains once price advances sufficiently; set to 0 to disable.
BaseVolume 0.1 Fallback trade size when dynamic sizing cannot be computed.
RiskFraction 0.02 Fraction of portfolio value applied to the dynamic sizing formula.
CandleType 1 hour time frame Candle series used for indicator calculations and decision making.

Conversion Notes

  • Trailing stop and protective exits use candle highs/lows instead of raw ticks because the high-level API processes completed candles; this keeps the behaviour deterministic across backtests and live runs.
  • Stop loss and take profit are executed via market flattening commands at the evaluated threshold rather than by placing separate protective orders, ensuring compatibility with the high-level strategy flow.
  • Dynamic position sizing relies on Portfolio.CurrentValue. When unavailable the strategy falls back to BaseVolume, similar to the original LotCheck fallback to the manual Lots input.
  • The losses counter is intentionally cumulative (never reset on winning trades) to follow the MQL implementation.
  • All comments are in English as required by project guidelines.

Usage Tips

  1. Attach the strategy to a security and portfolio, then configure CandleType to match the chart resolution you want to emulate from MT5.
  2. Review price offsets so they reflect your instrument tick size (e.g., for a 5-digit Forex pair 0.015 equals 150 points as in the source expert).
  3. Tune RiskFraction / BaseVolume to achieve realistic position sizes relative to your account.
  4. Optional: disable trailing by setting TrailingStopOffset to zero.
  5. Monitor logs for messages such as "Enter long" or "Exit short" which mirror the MetaTrader Print diagnostics.

Repository Structure

API/2512_Up3x1/
├── CS/Up3x1DynamicSizingStrategy.cs      # Converted C# strategy
├── README.md                # English documentation (this file)
├── README_zh.md             # Chinese translation
└── README_ru.md             # Russian translation

Disclaimer

Trading involves significant risk. This example is provided for educational purposes and should be validated on historical and simulated data before any live deployment.

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>
/// EMA triple crossover strategy converted from the MetaTrader 5 "up3x1" expert.
/// Uses three exponential moving averages with optional stop loss, take profit and trailing logic.
/// Position size is reduced after losing trades similar to the original lot optimization routine.
/// </summary>
public class Up3x1DynamicSizingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _mediumPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskFraction;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _mediumEma;
	private ExponentialMovingAverage _slowEma;

	private bool _hasPrevValues;
	private decimal _prevFast;
	private decimal _prevMedium;
	private decimal _prevSlow;

	private decimal _entryPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;

	private int _losses;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MediumPeriod
	{
		get => _mediumPeriod.Value;
		set => _mediumPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Absolute price distance for take profit.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Absolute price distance for stop loss.
	/// </summary>
	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	/// <summary>
	/// Absolute trailing stop distance.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	/// <summary>
	/// Base volume used when dynamic sizing cannot be calculated.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of portfolio value used for dynamic position sizing.
	/// </summary>
	public decimal RiskFraction
	{
		get => _riskFraction.Value;
		set => _riskFraction.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public Up3x1DynamicSizingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fastest EMA", "Indicators");

		_mediumPeriod = Param(nameof(MediumPeriod), 60)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA", "Period of the middle EMA", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 120)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slowest EMA", "Indicators");

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0.015m)
			.SetDisplay("Take Profit", "Absolute take profit distance in price units", "Risk");

		_stopLossOffset = Param(nameof(StopLossOffset), 0.01m)
			.SetDisplay("Stop Loss", "Absolute stop loss distance in price units", "Risk");

		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.004m)
			.SetDisplay("Trailing", "Trailing stop distance that follows price", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Fallback trade volume if dynamic sizing fails", "Money Management");

		_riskFraction = Param(nameof(RiskFraction), 0.02m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio value used for sizing", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for calculations", "General");
	}

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

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

		Volume = BaseVolume;
		ResetState();
		_losses = 0;
		_hasPrevValues = false;
		_prevFast = 0m;
		_prevMedium = 0m;
		_prevSlow = 0m;
	}

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

		Volume = BaseVolume;

		_fastEma = new EMA
		{
			Length = FastPeriod
		};

		_mediumEma = new EMA
		{
			Length = MediumPeriod
		};

		_slowEma = new EMA
		{
			Length = SlowPeriod
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_fastEma, _mediumEma, _slowEma, ProcessCandle)
			.Start();

		StartProtection(null, null);
	}

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

		if (!_fastEma.IsFormed || !_mediumEma.IsFormed || !_slowEma.IsFormed)
			return;

		if (!_hasPrevValues)
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			_hasPrevValues = true;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			return;
		}

		if (Position > 0)
		{
			if (TryHandleLongExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0)
		{
			if (TryHandleShortExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else
		{
			var bullishSetup = _prevFast < _prevMedium && _prevMedium < _prevSlow && mediumValue < fastValue && fastValue < slowValue;
			var bearishSetup = _prevFast > _prevMedium && _prevMedium > _prevSlow && mediumValue > fastValue && fastValue > slowValue;

			if (bullishSetup)
			{
				TryEnterLong(candle);
			}
			else if (bearishSetup)
			{
				TryEnterShort(candle);
			}
		}

		_prevFast = fastValue;
		_prevMedium = mediumValue;
		_prevSlow = slowValue;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped long entry because calculated volume is below minimum.");
			return;
		}

		BuyMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter long at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped short entry because calculated volume is below minimum.");
			return;
		}

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter short at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private bool TryHandleLongExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice + TakeProfitOffset;
			if (candle.HighPrice >= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice - StopLossOffset;
			if (candle.LowPrice <= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_highestPrice = candle.HighPrice > _highestPrice ? candle.HighPrice : _highestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _highestPrice - _entryPrice > TrailingStopOffset)
		{
			var trail = _highestPrice - TrailingStopOffset;
			if (candle.LowPrice <= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private bool TryHandleShortExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice - TakeProfitOffset;
			if (candle.LowPrice <= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice + StopLossOffset;
			if (candle.HighPrice >= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_lowestPrice = _lowestPrice == 0m || candle.LowPrice < _lowestPrice ? candle.LowPrice : _lowestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _entryPrice - _lowestPrice > TrailingStopOffset)
		{
			var trail = _lowestPrice + TrailingStopOffset;
			if (candle.HighPrice >= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private void ExitPosition(decimal exitPrice, string reason)
	{
		var isLong = Position > 0;
		var volume = Math.Abs(Position);

		if (volume <= 0m)
			return;

		var pnl = isLong
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (isLong)
		{
			SellMarket();
		}
		else
		{
			BuyMarket();
		}

		LogInfo($"Exit {(isLong ? "long" : "short")} at {exitPrice} because {reason}. Approx PnL: {pnl}.");

		if (pnl < 0m)
			_losses++;

		ResetState();
	}

	private decimal CalculateOrderVolume()
	{
		var volume = 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;

		if (portfolioValue > 0m && RiskFraction > 0m)
			volume = portfolioValue * RiskFraction / 1000m;

		if (volume <= 0m)
			volume = BaseVolume;

		if (_losses > 1)
		{
			var reduction = volume * _losses / 3m;
			volume -= reduction;

			if (volume <= 0m)
				volume = BaseVolume;
		}

		volume = AdjustVolumeToInstrument(volume);

		return volume;
	}

	private decimal AdjustVolumeToInstrument(decimal volume)
	{
		var security = Security;

		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		var minVolume = security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			return 0m;

		var maxVolume = security.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetState()
	{
		_entryPrice = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
	}
}