Ver en GitHub

Estrategia de cruce de niveles de precios de aplicaciones

Descripción general

  • Conversión del MetaTrader 4 asesor experto BT_v4 (MQL/8543/BT_v4.mq4).
  • Reimplementado con la estrategia API de alto nivel de StockSharp (suscripciones de velas, procesamiento sin indicadores, protecciones integradas).
  • Enfocado en reaccionar ante el precio de cierre cruzando un nivel horizontal definido por el usuario (AppPrice).

Lógica de trading

  1. Cada vela terminada actualiza un búfer interno con el último precio de cierre.
  2. Cuando el cierre se mueve por encima de AppPrice mientras que el cierre anterior estaba en el nivel o por debajo del mismo, la estrategia
    • Se comercializa solo si BuyOnly = true (refleja el valor predeterminado original EA).
    • Cancela cualquier orden pendiente, compensa una venta corta existente mediante el mismo volumen de orden de mercado y establece una posición larga del tamaño de lote calculado.
  3. Cuando el cierre se mueve por debajo de AppPrice mientras que el cierre anterior estaba en o por encima del nivel, la estrategia
    • Se negocia solo si BuyOnly = false (modo de solo venta del EA).
    • Cancela órdenes pendientes, compensa cualquier posición larga existente y establece una posición corta del tamaño de lote calculado.
  4. Las señales se evalúan estrictamente en velas completas; las velas parcialmente formadas se ignoran al igual que en el script MQL.

Dimensionamiento de posiciones

  • EnableMoneyManagement = false → use FixedVolume (equivalente a la entrada MQL Lots.
  • EnableMoneyManagement = true → calcula el lote usando la fórmula original:

[ \text = \text_{\text} \left( \frac{\text}{100} \times \frac{\text}{\text} \right) ]

  • divisor = 1000 para lotes de un decimal y 100 para lotes de dos decimales (misma regla que LotPrec en MQL).
  • El resultado se fija en [MinLot, MaxLot] y luego se alinea con las restricciones de seguridad VolumeStep, VolumeMin y VolumeMax.
  • Si los datos del saldo de la cartera no están disponibles, la estrategia vuelve a FixedVolume.

Gestión del riesgo

  • StopLossPoints y TakeProfitPoints se miden en puntos de precio de instrumento (ticks).
  • Si cualquiera de los valores es positivo, StartProtection se activa con las compensaciones convertidas a través de Security.PriceStep.
  • Establecer una distancia en 0 desactiva esa pata protectora en particular, de acuerdo con el comportamiento original de EA.

Parámetros

Nombre Descripción Predeterminado
AppPrice Nivel que activa las operaciones cuando el cierre lo cruza. 0
BuyOnly true = modo solo largo (valor predeterminado original), false = solo corto. true
FixedVolume Tamaño del lote cuando MM está deshabilitado. 0.1
EnableMoneyManagement Permite dimensionar el porcentaje de equilibrio. false
LotBalancePercent Porcentaje del saldo utilizado cuando MM está activado. 10
MinLot / MaxLot Límites para el tamaño de lote calculado. 0.1 / 5
LotPrecision Número de decimales para redondear el lote calculado. 1
StopLossPoints Distancia de stop-loss en puntos de precio (0 = deshabilitado). 140
TakeProfitPoints Distancia de obtención de beneficios en puntos de precio (0 = deshabilitado). 180
CandleType Plazo de vela utilizado para la detección cruzada. 1 Minute

Notas de implementación

  • Utiliza SubscribeCandles(...).Bind(...) por lo que los indicadores son innecesarios; Los precios de cierre llegan directamente en la devolución de llamada.
  • Las órdenes de mercado (BuyMarket/SellMarket) se dimensionan para aplanar la posición opuesta antes de abrir una nueva, reflejando la lógica EA de cerrar órdenes opuestas antes de ingresar.
  • CancelActiveOrders() se invoca antes de cada orden de mercado para evitar órdenes pendientes no deseadas.
  • Parámetros como Magic, Slippage y configuraciones de color del archivo MQL se omiten porque no tienen un equivalente directo en StockSharp.
  • Asegúrese de que los metadatos Security (PriceStep, VolumeStep, VolumeMin, VolumeMax) estén completos para que los ajustes de precio/volumen coincidan con las reglas del corredor.

Consejos de uso

  • Establezca AppPrice en el nivel horizontal que desea monitorear (por ejemplo, precio psicológico, pivote diario, etc.).
  • Apague BuyOnly para replicar el modo original de "solo venta"; déjelo activado para ejecutar el comportamiento de solo largo predeterminado proporcionado.
  • Al habilitar la administración del dinero, verifique que la conexión de la cartera proporcione actualizaciones del saldo; de lo contrario, la estrategia vuelve al volumen fijo.
  • No se proporciona ningún puerto Python por solicitud; solo se genera la estrategia C#.
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>
/// Strategy that opens trades when the close price crosses a configured level.
/// Converted from the MQL4 expert advisor BT_v4.
/// </summary>
public class AppPriceLevelCrossStrategy : Strategy
{
	private readonly StrategyParam<decimal> _appPrice;
	private readonly StrategyParam<bool> _buyOnly;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _enableMoneyManagement;
	private readonly StrategyParam<decimal> _lotBalancePercent;
	private readonly StrategyParam<decimal> _minLot;
	private readonly StrategyParam<decimal> _maxLot;
	private readonly StrategyParam<int> _lotPrecision;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousClose;

	/// <summary>
	/// Initializes strategy parameters with defaults mirroring the MQL version.
	/// </summary>
	public AppPriceLevelCrossStrategy()
	{
		_appPrice = Param(nameof(AppPrice), 65000m)
			.SetDisplay("App Price", "Reference level that generates trades when the close crosses it", "Trading");

		_buyOnly = Param(nameof(BuyOnly), true)
			.SetDisplay("Buy Only", "Enable to trade only long entries (set to false for sell-only mode)", "Trading");

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Fixed Volume", "Lot size used when money management is disabled", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 140)
			.SetDisplay("Stop Loss (points)", "Distance in price points for the protective stop (0 disables)", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 180)
			.SetDisplay("Take Profit (points)", "Distance in price points for the profit target (0 disables)", "Risk");

		_enableMoneyManagement = Param(nameof(EnableMoneyManagement), false)
			.SetDisplay("Enable MM", "Toggle balance-based position sizing", "Risk");

		_lotBalancePercent = Param(nameof(LotBalancePercent), 10m)
			.SetDisplay("Balance %", "Percentage of balance used to compute the lot when MM is enabled", "Risk");

		_minLot = Param(nameof(MinLot), 0.1m)
			.SetDisplay("Minimum Lot", "Lower bound for the calculated lot size", "Risk");

		_maxLot = Param(nameof(MaxLot), 5m)
			.SetDisplay("Maximum Lot", "Upper bound for the calculated lot size", "Risk");

		_lotPrecision = Param(nameof(LotPrecision), 1)
			.SetDisplay("Lot Precision", "Number of decimal places to round the calculated lot size", "Risk");

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

	/// <summary>
	/// Target price level for the close-cross rule.
	/// </summary>
	public decimal AppPrice
	{
		get => _appPrice.Value;
		set => _appPrice.Value = value;
	}

	/// <summary>
	/// When true only long trades are allowed; set to false to trade only shorts.
	/// </summary>
	public bool BuyOnly
	{
		get => _buyOnly.Value;
		set => _buyOnly.Value = value;
	}

	/// <summary>
	/// Fixed lot size used when money management is disabled.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Enables the balance-percentage position sizing block.
	/// </summary>
	public bool EnableMoneyManagement
	{
		get => _enableMoneyManagement.Value;
		set => _enableMoneyManagement.Value = value;
	}

	/// <summary>
	/// Percentage of account balance used for lot calculation when MM is active.
	/// </summary>
	public decimal LotBalancePercent
	{
		get => _lotBalancePercent.Value;
		set => _lotBalancePercent.Value = value;
	}

	/// <summary>
	/// Minimum allowed lot for the calculated value.
	/// </summary>
	public decimal MinLot
	{
		get => _minLot.Value;
		set => _minLot.Value = value;
	}

	/// <summary>
	/// Maximum allowed lot for the calculated value.
	/// </summary>
	public decimal MaxLot
	{
		get => _maxLot.Value;
		set => _maxLot.Value = value;
	}

	/// <summary>
	/// Decimal precision applied to the calculated lot size.
	/// </summary>
	public int LotPrecision
	{
		get => _lotPrecision.Value;
		set => _lotPrecision.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate the cross conditions.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		// Reset stored close value so the next formed candle rebuilds the history.
		_previousClose = null;
	}

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

		var dummySma = new SimpleMovingAverage { Length = 2 };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(dummySma, ProcessCandle).Start();

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

		var step = Security?.PriceStep ?? 1m;
		if (TakeProfitPoints > 0 || StopLossPoints > 0)
		{
			var takeDistance = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : new Unit(0m);
			var stopDistance = StopLossPoints > 0 ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : new Unit(0m);

			StartProtection(takeProfit: takeDistance, stopLoss: stopDistance);
		}
	}

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

		var previousClose = _previousClose;
		_previousClose = candle.ClosePrice;

		// Need at least one completed candle to compare against the configured level.
		if (previousClose is null)
			return;

		var crossedAbove = candle.ClosePrice > AppPrice && previousClose <= AppPrice;
		var crossedBelow = candle.ClosePrice < AppPrice && previousClose >= AppPrice;

		if (crossedAbove)
		{
			ExecuteBuy();
		}
		else if (crossedBelow)
		{
			ExecuteSell();
		}
	}

	private void ExecuteBuy()
	{
		// Skip if we already hold a long position.
		if (Position > 0)
			return;

		var baseVolume = CalculateBaseVolume();
		if (baseVolume <= 0m)
			return;

		var volume = baseVolume;
		if (Position < 0)
			volume += Math.Abs(Position);

		volume = AlignVolume(volume);
		if (volume <= 0m)
			return;

BuyMarket(volume);
	}

	private void ExecuteSell()
	{
		// Skip if we already hold a short position.
		if (Position < 0)
			return;

		var baseVolume = CalculateBaseVolume();
		if (baseVolume <= 0m)
			return;

		var volume = baseVolume;
		if (Position > 0)
			volume += Math.Abs(Position);

		volume = AlignVolume(volume);
		if (volume <= 0m)
			return;

SellMarket(volume);
	}

	private decimal CalculateBaseVolume()
	{
		if (!EnableMoneyManagement)
			return FixedVolume;

		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
		if (balance is null || balance <= 0m)
			return FixedVolume;

		var divisor = LotPrecision == 2 ? 100m : 1000m;
		if (LotPrecision <= 0)
			divisor = 1m;

		var precision = LotPrecision;
		if (precision < 0)
			precision = 0;

		var volume = LotBalancePercent / 100m * balance.Value / divisor;
		volume = Math.Round(volume, precision, MidpointRounding.AwayFromZero);

		if (volume < MinLot)
			volume = MinLot;
		if (volume > MaxLot)
			volume = MaxLot;

		return volume;
	}

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

		var minVolume = security.MinVolume ?? 0m;
		var maxVolume = security.MaxVolume ?? decimal.MaxValue;
		var step = security.VolumeStep ?? 0m;

		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		if (step > 0m)
		{
			var steps = Math.Round(volume / step, MidpointRounding.AwayFromZero);
			volume = steps * step;
		}

		return volume;
	}
}