Ver no GitHub

MP Candlestick Strategy

Overview

The MP Candlestick Strategy is a conversion of the MetaTrader 5 Expert Advisor mp candlestick.mq5 into the StockSharp high-level strategy framework. The system evaluates the direction of completed candles and opens trades in the same direction while applying strict risk management. It supports both fixed stop-loss distances expressed in MetaTrader pips and adaptive stop-loss placement derived from the Average True Range (ATR).

Trading Logic

  1. The strategy subscribes to a single configurable candle series (default: 1-hour candles).
  2. On each finished candle:
    • Bullish candle (close above open) → consider a long position.
    • Bearish candle (close below open) → consider a short position.
    • Doji candles are ignored.
  3. Before any entry the strategy calculates a stop-loss price either from ATR or from the fixed pip distance. The take-profit price is computed using the configured risk-to-reward ratio.
  4. If margin usage stays within the allowed percentage and the calculated position size is valid, the trade is opened at market.
  5. While the position is active the strategy monitors each new candle for:
    • Stop-loss or take-profit hits using candle extremes.
    • Trailing adjustment that moves the stop toward breakeven when ATR stops are enabled.
  6. Once the position is flat the process restarts with the next finished candle.

Risk and Money Management

  • Risk Percent defines the equity fraction risked per trade. The position size is derived from the price distance between entry and stop-loss and the instrument price/step value.
  • Risk/Reward Ratio determines the distance between the entry price and take-profit target relative to the initial risk.
  • Max Margin Usage restricts how much estimated margin the new trade may consume compared to current portfolio equity.
  • Trailing Stop is activated automatically when ATR-based risk management is used. It moves the stop halfway toward the profit target without exceeding the latest candle close, attempting to lock profits while respecting exchange constraints.

Parameters

Parameter Default Description
RiskPercent 1 Percent of portfolio equity allocated as maximum loss for a single trade.
RiskRewardRatio 1.5 Multiplier applied to the initial risk distance to define the take-profit target.
MaxMarginUsage 30 Upper bound for margin consumption expressed as a percentage of equity.
StopLossPips 50 Fixed stop-loss size in MetaTrader pips when ATR is disabled.
UseAutoSl true Enables ATR (length 14) stop-loss sizing with multiplier 1.5.
CandleType 1-hour time frame Candle series used for signals and ATR calculation.

Implementation Notes

  • The strategy relies on StockSharp high-level subscriptions (SubscribeCandles) and indicator binding (AverageTrueRange).
  • Position sizing aligns with the instrument volume step, minimum and maximum volume constraints.
  • Margin checks reuse available instrument margin hints (MarginBuy/MarginSell) and fall back to a price-based estimate.
  • Stop-loss and take-profit levels are enforced internally by monitoring candle highs and lows, ensuring consistent behavior across brokers.
  • All code comments are in English as required by the conversion guidelines.

Files

  • CS/MpCandlestickStrategy.cs — main C# strategy implementation.
  • README.md — English documentation (this file).
  • README_zh.md — Chinese translation.
  • README_ru.md — Russian translation.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Candlestick-based risk managed strategy converted from the MetaTrader "mp candlestick" expert.
/// Uses candle direction to decide trade side, applies ATR-based or fixed stop-loss distance,
/// and enforces a configurable risk-to-reward profile with margin awareness.
/// </summary>
public class MpCandlestickStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _riskRewardRatio;
	private readonly StrategyParam<decimal> _maxMarginUsage;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<bool> _useAutoSl;
	private readonly StrategyParam<DataType> _candleType;

	private AverageTrueRange _atr;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _isLongPosition;

	/// <summary>
	/// Percentage of portfolio equity risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Desired risk to reward ratio.
	/// </summary>
	public decimal RiskRewardRatio
	{
		get => _riskRewardRatio.Value;
		set => _riskRewardRatio.Value = value;
	}

	/// <summary>
	/// Maximum allowed margin usage percentage.
	/// </summary>
	public decimal MaxMarginUsage
	{
		get => _maxMarginUsage.Value;
		set => _maxMarginUsage.Value = value;
	}

	/// <summary>
	/// Fixed stop-loss distance in MetaTrader pips when dynamic stop is disabled.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Enables ATR based stop-loss sizing.
	/// </summary>
	public bool UseAutoSl
	{
		get => _useAutoSl.Value;
		set => _useAutoSl.Value = value;
	}

	/// <summary>
	/// Candle type used for signal generation and ATR calculation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public MpCandlestickStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
		.SetNotNegative()
		.SetDisplay("Risk Percent", "Percentage of portfolio equity risked per trade", "Risk")
		
		.SetOptimize(0.5m, 10m, 0.5m);

		_riskRewardRatio = Param(nameof(RiskRewardRatio), 1.5m)
		.SetGreaterThanZero()
		.SetDisplay("Risk/Reward Ratio", "Target reward multiple relative to the initial risk", "Risk")
		
		.SetOptimize(1m, 4m, 0.25m);

		_maxMarginUsage = Param(nameof(MaxMarginUsage), 30m)
		.SetNotNegative()
		.SetDisplay("Max Margin Usage", "Upper bound for margin consumption as percent of equity", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50)
		.SetGreaterThanZero()
		.SetDisplay("Stop-Loss Pips", "Fixed stop-loss size in MetaTrader pips", "Risk")
		
		.SetOptimize(10, 200, 5);

		_useAutoSl = Param(nameof(UseAutoSl), true)
		.SetDisplay("Use ATR Stop", "If enabled the stop-loss uses ATR * 1.5 distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Primary candle series for signals", "Data");
	}

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

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

		_atr = null;
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}

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

		_atr = new AverageTrueRange { Length = 14 };

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

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

	/// <inheritdoc />
	// Reset risk levels handled via OnReseted

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
			ResetRiskLevels();
	}

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

		CheckRiskLevels(candle);

		// indicators are bound via Bind

		if (Position != 0m)
		{
			UpdateTrailingStop(candle.ClosePrice);
			return;
		}

		var isBullish = candle.ClosePrice > candle.OpenPrice;
		var isBearish = candle.ClosePrice < candle.OpenPrice;

		if (!isBullish && !isBearish)
			return;

		if (!TryCreateRiskTargets(isBullish, candle.ClosePrice, atrValue,
		out var stopPrice, out var takeProfit, out var stopDistance))
		{
			return;
		}

		var volume = CalculateTradeVolume(stopDistance);
		if (volume <= 0m)
			return;

		// margin validation skipped for backtest

		if (isBullish)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_entryPrice = candle.ClosePrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfit;
		_isLongPosition = isBullish;
		UpdateTrailingStop(candle.ClosePrice);
	}

	private bool TryCreateRiskTargets(bool isLong, decimal entryPrice, decimal atrValue,
	out decimal stopPrice, out decimal takeProfitPrice, out decimal stopDistance)
	{
		stopPrice = 0m;
		takeProfitPrice = 0m;
		stopDistance = 0m;

		var security = Security;
		if (security == null)
			return false;

		if (RiskRewardRatio <= 0m)
			return false;

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		decimal distance;
		if (UseAutoSl)
		{
			distance = atrValue * 1.5m;
		}
		else
		{
			distance = StopLossPips * priceStep;
		}

		if (distance <= 0m)
			return false;

		stopDistance = distance;
		stopPrice = isLong ? entryPrice - distance : entryPrice + distance;
		takeProfitPrice = isLong ? entryPrice + distance * RiskRewardRatio : entryPrice - distance * RiskRewardRatio;

		return stopPrice > 0m && takeProfitPrice > 0m;
	}

	private decimal CalculateTradeVolume(decimal stopDistance)
	{
		var security = Security;
		var portfolio = Portfolio;

		if (security == null || portfolio == null)
			return 0m;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

		if (RiskPercent <= 0m)
			return AlignVolume(volumeStep);

		var equity = portfolio.CurrentValue ?? portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return 0m;

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? priceStep;
		if (stepPrice <= 0m)
			stepPrice = priceStep;

		if (stopDistance <= 0m)
			return 0m;

		var steps = stopDistance / priceStep;
		if (steps <= 0m)
			return 0m;

		var lossPerVolumeStep = steps * stepPrice;
		if (lossPerVolumeStep <= 0m)
			return 0m;

		var riskAmount = equity * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		var rawVolume = riskAmount / lossPerVolumeStep * volumeStep;
		return AlignVolume(rawVolume);
	}

	private decimal AlignVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
			return 0m;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

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

		var steps = Math.Floor(volume / volumeStep);
		if (steps <= 0m)
			steps = 1m;

		var normalized = steps * volumeStep;

		var minVolume = security.MinVolume ?? volumeStep;
		if (normalized < minVolume)
			normalized = minVolume;

		var maxVolume = security.MaxVolume;
		if (maxVolume.HasValue && normalized > maxVolume.Value)
			normalized = maxVolume.Value;

		return normalized;
	}

	private bool ValidateMargin(decimal price, decimal volume, bool isLong)
	{
		if (MaxMarginUsage <= 0m)
			return true;

		var security = Security;
		var portfolio = Portfolio;
		if (security == null || portfolio == null)
			return false;

		var equity = portfolio.CurrentValue ?? portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return false;

		var volumeStep = security.VolumeStep ?? 1m;
		if (volumeStep <= 0m)
			volumeStep = 1m;

		var marginPerVolume = isLong ? GetSecurityValue<decimal?>(Level1Fields.MarginBuy) : GetSecurityValue<decimal?>(Level1Fields.MarginSell);

		decimal margin;
		if (marginPerVolume is decimal direct && direct > 0m)
		{
			margin = direct * (volume / volumeStep);
		}
		else
		{
			margin = price * volume;
		}

		var maxMargin = equity * (MaxMarginUsage / 100m);
		return margin <= maxMargin;
	}

	private void CheckRiskLevels(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket(Position);
				ResetRiskLevels();
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
			}
		}
	}

	private void UpdateTrailingStop(decimal currentPrice)
	{
		if (!UseAutoSl)
			return;

		if (_entryPrice is not decimal entry || _takeProfitPrice is not decimal take || _stopPrice is not decimal currentStop)
			return;

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

		var priceStep = security.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 1m;

		if (_isLongPosition)
		{
			var candidate = entry + (take - entry) * 0.5m;
			var limit = currentPrice - priceStep;
			if (limit <= entry)
				limit = entry;

			if (candidate > limit)
				candidate = limit;

			if (candidate > currentStop && candidate < currentPrice)
				_stopPrice = candidate;
		}
		else
		{
			var candidate = entry - (entry - take) * 0.5m;
			var limit = currentPrice + priceStep;
			if (limit >= entry)
				limit = entry;

			if (candidate < limit)
				candidate = limit;

			if (candidate < currentStop && candidate > currentPrice)
				_stopPrice = candidate;
		}
	}

	private void ResetRiskLevels()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}
}