Ver en GitHub

Estrategia de Margen Fijo de Dinero

Esta estrategia replica el ejemplo de MetaTrader "Money Fixed Margin" utilizando la API de alto nivel de StockSharp. Muestra cómo dimensionar posiciones arriesgando un porcentaje fijo del portafolio mientras convierte la distancia del stop-loss expresada en pips a un desplazamiento de precio absoluto. La estrategia solo opera posiciones largas y se enfoca en demostrar la lógica de gestión del dinero en lugar de una señal de entrada predictiva.

Detalles

  • Criterios de entrada:
    • Largo: ejecuta una compra a mercado después de cada recuento de velas completado especificado por Check Interval (por defecto cada 980ª barra). La orden utiliza el precio de cierre de la vela desencadenante como referencia para los cálculos de riesgo.
  • Largo/Corto: Solo largos.
  • Criterios de salida:
    • El stop-loss protector se adjunta automáticamente mediante StartProtection a una distancia derivada del parámetro Stop Loss (pips).
    • No se utiliza objetivo de beneficio; las posiciones cierran únicamente por el stop-loss o intervención manual.
  • Stops: Solo Stop Loss.
  • Valores predeterminados:
    • Stop Loss (pips) = 25
    • Risk Percent = 10
    • Check Interval = 980
    • Candle Type = marco temporal de 1 minuto
  • Filtros:
    • Categoría: Gestión de riesgos
    • Dirección: Largo
    • Indicadores: Ninguno
    • Stops: Sí (stop-loss)
    • Complejidad: Básico
    • Marco temporal: Intradía (configurable a través de Candle Type)
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio (escala con Risk Percent)

Lógica de Dimensionamiento de Posición

  1. La estrategia lee Security.PriceStep y Security.Decimals para inferir el tamaño del pip. Los pares con 3 o 5 decimales usan un multiplicador décuplo para coincidir con la definición de pip de MetaTrader.
  2. Stop Loss (pips) se multiplica por el tamaño del pip para obtener una distancia de precio absoluta (ExtStopLoss) idéntica al código MQL5.
  3. El valor actual del portafolio (prefiriendo Portfolio.CurrentValue luego Portfolio.BeginValue) se multiplica por Risk Percent / 100 para determinar el capital expuesto por operación.
  4. El riesgo por lote individual se calcula mediante el producto de la distancia del stop-loss, el número de pasos de precio dentro de esa distancia y Security.StepPrice cuando esté disponible. Si StepPrice es desconocido, la distancia de precio en sí se usa como respaldo.
  5. Dividir el monto de riesgo por el riesgo por lote produce el volumen deseado. El resultado se normaliza al VolumeStep del instrumento, se limita a los límites mínimos y máximos de volumen, y se registra para transparencia. También se registra un valor de comparación con distancia de stop-loss cero para ilustrar por qué el gestor de dinero rechaza operaciones sin un stop protector.

Flujo de Trabajo

  1. Al iniciar, la estrategia se suscribe a la serie de velas configurada, calcula el tamaño del pip y habilita StartProtection con la distancia de stop-loss absoluta calculada.
  2. Cada vela completada incrementa un contador interno. Cuando el contador alcanza el Check Interval elegido, la estrategia evalúa el tamaño de la posición, imprime información de diagnóstico y restablece el contador.
  3. Si el volumen calculado es positivo, se coloca una orden de compra a mercado. La protección incorporada adjunta el stop-loss en Close - ExtStopLoss. Cualquier error (por ejemplo, por datos insuficientes o instrumentos con precio cero) impide el envío de la orden.
  4. No se realizan más operaciones hasta que el contador complete otro intervalo, manteniendo el enfoque en la gestión del dinero en lugar de la frecuencia de señales.

Notas de Uso

  • Establezca Risk Percent en un valor conservador al conectarse a una cuenta en vivo; el riesgo predeterminado del 10% refleja el ejemplo MQL pero es agresivo para el trading real.
  • Asegúrese de que el instrumento proporcione metadatos significativos de PriceStep y StepPrice. Cuando no estén disponibles, la estrategia sigue operando pero interpreta el riesgo en unidades de precio bruto.
  • La estrategia evita intencionalmente las operaciones cortas para mantenerse fiel a la demostración original. Adapte las llamadas BuyMarket/SellMarket si se desea trading bilateral.
  • Combine este módulo de gestión del dinero con otros generadores de señales reutilizando el helper CalculateFixedMarginVolume del código de la estrategia.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Recreates the MetaTrader Money Fixed Margin sample using StockSharp.
/// It demonstrates fixed percentage risk sizing for long trades.
/// </summary>
public class MoneyFixedMarginStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _checkInterval;
	private readonly StrategyParam<DataType> _candleType;

	private int _barCount;
	private decimal _pipSize;

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Portfolio percentage risked on each trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Number of finished candles between trade attempts.
	/// </summary>
	public int CheckInterval
	{
		get => _checkInterval.Value;
		set => _checkInterval.Value = value;
	}

	/// <summary>
	/// Candle series used to time entries.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MoneyFixedMarginStrategy"/>.
	/// </summary>
	public MoneyFixedMarginStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 25m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Percent", "Percent of equity risked per trade", "Risk");

		_checkInterval = Param(nameof(CheckInterval), 150)
			.SetGreaterThanZero()
			.SetDisplay("Check Interval", "Completed candles between trades", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_barCount = 0;
		_pipSize = 0m;
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		_pipSize = priceStep * adjust;
		if (_pipSize <= 0m)
			_pipSize = priceStep > 0m ? priceStep : 1m;

		// Attach a protective stop using the pip-based distance converted to price units.
		StartProtection(
			new Unit(StopLossPips * _pipSize, UnitTypes.Absolute),
			new Unit(StopLossPips * _pipSize * 2, UnitTypes.Absolute));

		// Subscribe to the candle stream that emulates the tick counter from the MQL example.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

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

		// Count finished candles to mirror the tick counter from the original script.
		_barCount++;

		if (_barCount < CheckInterval)
			return;

		var entryPrice = candle.ClosePrice;

		if (entryPrice <= 0m)
		{
			LogWarning("Skip trade because entry price is not positive. Close={0}", entryPrice);
			return;
		}

		var riskAmount = CalculateRiskAmount();
		if (riskAmount <= 0m)
		{
			LogWarning("Skip trade because risk amount is not positive. Portfolio value={0}", riskAmount);
			return;
		}

		var stopDistance = StopLossPips * _pipSize;
		var stopPrice = entryPrice - stopDistance;

		var volumeWithoutStop = CalculateFixedMarginVolume(entryPrice, 0m, riskAmount);
		var volumeWithStop = CalculateFixedMarginVolume(entryPrice, stopPrice, riskAmount);

		this.LogInfo(
			"StopLoss=0 -> volume {0:0.####}; StopLoss={1:0.#####} -> volume {2:0.####}; Portfolio={3:0.##}",
			volumeWithoutStop,
			stopPrice,
			volumeWithStop,
			GetPortfolioValue());

		BuyMarket();

		// Reset the counter only after successfully sending an order.
		_barCount = 0;
	}

	private decimal CalculateRiskAmount()
	{
		var portfolioValue = GetPortfolioValue();
		return portfolioValue > 0m ? portfolioValue * RiskPercent / 100m : 0m;
	}

	private decimal GetPortfolioValue()
	{
		var current = Portfolio?.CurrentValue ?? 0m;
		if (current > 0m)
			return current;

		var begin = Portfolio?.BeginValue ?? 0m;
		return begin > 0m ? begin : current;
	}

	private decimal CalculateFixedMarginVolume(decimal entryPrice, decimal stopPrice, decimal riskAmount)
	{
		if (riskAmount <= 0m || entryPrice <= 0m || stopPrice <= 0m)
			return 0m;

		var stopDistance = entryPrice - stopPrice;
		if (stopDistance <= 0m)
			return 0m;

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

		var stepPrice = 0m;
		if (stepPrice <= 0m)
			stepPrice = priceStep;

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

		var riskPerVolume = stepsCount * stepPrice;
		if (riskPerVolume <= 0m)
			return 0m;

		var rawVolume = riskAmount / riskPerVolume;
		return NormalizeVolume(rawVolume);
	}

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

		if (Security?.VolumeStep is decimal step && step > 0m)
		{
			volume = Math.Ceiling(volume / step) * step;
		}

		return volume;
	}
}