Ver en GitHub

Estrategia de velas MP

Descripción general

La Estrategia MP Candlestick es una conversión del MetaTrader 5 Expert Advisor mp candlestick.mq5 al marco de estrategia de alto nivel StockSharp. El sistema evalúa la dirección de las velas completadas y abre operaciones en la misma dirección mientras aplica una estricta gestión de riesgos. Admite distancias de stop-loss fijas expresadas en MetaTrader pips y colocación de stop-loss adaptativa derivada del rango verdadero promedio (ATR).

Lógica de trading

  1. La estrategia se suscribe a una única serie de velas configurables (predeterminado: velas de 1 hora).
  2. En cada vela terminada:
    • Vela alcista (cierre por encima de apertura) → considere una posición larga.
    • Vela bajista (cierre por debajo de apertura) → considere una posición corta.
    • Se ignoran las velas Doji.
  3. Antes de cualquier entrada, la estrategia calcula un precio de límite de pérdidas desde ATR o desde la distancia de pip fija. El precio de obtención de beneficios se calcula utilizando la relación riesgo-recompensa configurada.
  4. Si el uso del margen se mantiene dentro del porcentaje permitido y el tamaño de la posición calculado es válido, la operación se abre en el mercado.
  5. Mientras la posición está activa, la estrategia monitorea cada nueva vela para detectar:
    • El stop-loss o la toma de ganancias utilizan los extremos de las velas.
    • Ajuste final que mueve el stop hacia el punto de equilibrio cuando ATR stop están habilitados.
  6. Una vez que la posición es plana, el proceso se reinicia con la siguiente vela terminada.

Gestión de riesgos y dinero

  • Porcentaje de riesgo define la fracción de capital arriesgada por operación. El tamaño de la posición se deriva de la distancia del precio entre la entrada y el stop-loss y el precio/valor del paso del instrumento.
  • Relación riesgo/recompensa determina la distancia entre el precio de entrada y el objetivo de obtención de beneficios en relación con el riesgo inicial.
  • Uso de margen máximo restringe la cantidad de margen estimado que puede consumir la nueva operación en comparación con el capital de la cartera actual.
  • Trailing Stop se activa automáticamente cuando se utiliza la gestión de riesgos basada en ATR. Mueve el stop a mitad de camino hacia el objetivo de ganancias sin exceder el último cierre de vela, intentando bloquear las ganancias respetando las restricciones cambiarias.

Parámetros

Parámetro Predeterminado Descripción
RiskPercent 1 Porcentaje del capital de la cartera asignado como pérdida máxima para una sola operación.
RiskRewardRatio 1.5 Multiplicador aplicado a la distancia de riesgo inicial para definir el objetivo de obtención de beneficios.
MaxMarginUsage 30 Límite superior para el consumo de margen expresado como porcentaje del capital.
StopLossPips 50 Se corrigió el tamaño del stop-loss en MetaTrader pips cuando ATR está deshabilitado.
UseAutoSl cierto Habilita el tamaño de stop-loss de ATR (longitud 14) con multiplicador 1,5.
CandleType plazo de 1 hora Serie de velas utilizadas para señales y cálculo ATR.

Notas de implementación

  • La estrategia se basa en StockSharp suscripciones de alto nivel (SubscribeCandles) y vinculación de indicadores (AverageTrueRange).
  • El tamaño de la posición se alinea con el paso de volumen del instrumento y las restricciones de volumen mínimo y máximo.
  • Las comprobaciones de márgenes reutilizan las sugerencias de margen de instrumentos disponibles (MarginBuy/MarginSell) y recurren a una estimación basada en el precio.
  • Los niveles de stop-loss y take-profit se aplican internamente mediante el seguimiento de los máximos y mínimos de las velas, lo que garantiza un comportamiento coherente entre los corredores.
  • Todos los comentarios del código están en inglés como lo exigen las pautas de conversión.

Archivos

  • CS/MpCandlestickStrategy.cs: implementación de la estrategia principal de C#.
  • README.md — Documentación en inglés (este archivo).
  • README_zh.md — Traducción al chino.
  • README_ru.md — Traducción al ruso.
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;
	}
}