Ver en GitHub

Estrategia LBS

La Estrategia LBS es una conversión directa del asesor experto de MetaTrader 5 "LBS (barabashkakvn's edition)". El sistema original monitorea rupturas de la vela anterior durante una ventana de trading configurable y coloca órdenes stop en ambos extremos. El puerto de StockSharp mantiene las mismas reglas de gestión de operaciones mientras usa la API de alto nivel (SubscribeCandles, SubscribeLevel1, BuyStop/SellStop) para mayor claridad y fiabilidad.

Lógica de trading

  1. La estrategia monitorea velas completadas del marco temporal seleccionado (CandleType).
  2. Cuando el tiempo de cierre de la vela coincide con cualquiera de las horas de trading habilitadas (Hour1, Hour2, Hour3), el algoritmo calcula los niveles de ruptura:
    • El buy stop se coloca en el máximo más alto de la vela y el ask actual más un buffer de congelación.
    • El sell stop se coloca en el mínimo más bajo de la vela y el bid actual menos el mismo buffer.
    • El buffer reproduce el fallback SYMBOL_TRADE_FREEZE_LEVEL de MetaTrader (tres spreads, pero nunca menos de diez pips).
  3. Si se abre una posición, la orden pendiente opuesta se cancela inmediatamente, igual que la rutina DeleteAllPendingOrders del experto MQL.
  4. Los precios iniciales de stop-loss se adjuntan según StopLossPips. La lógica de trailing opcional (TrailingStopPips y TrailingStepPips) desplaza el stop una vez que el beneficio flotante supera los umbrales configurados.
  5. Las órdenes solo se envían cuando la estrategia está en línea, no hay ninguna posición abierta y hay cotizaciones válidas de Level1 disponibles.

Gestión del dinero

MoneyMode espeja el interruptor Lot/Risk del experto original:

  • FixedLot – el parámetro VolumeOrRisk se interpreta como un volumen de trading absoluto.
  • RiskPercent – la estrategia convierte VolumeOrRisk en una fracción del valor de la cartera. El importe del riesgo se divide por la distancia entre el precio de entrada y el stop de protección (en pasos de precio) para obtener el volumen de la orden. Cuando se usa este modo, el stop-loss debe estar habilitado; de lo contrario, la orden se omite.

Todos los volúmenes se normalizan a los límites de mínimo, máximo y paso del instrumento para evitar rechazos del broker.

Parámetros

Nombre Predeterminado Descripción
StopLossPips 50 Distancia al stop fijo en pips. Cero deshabilita tanto el stop inicial como el módulo de trailing.
TrailingStopPips 5 Distancia del trailing-stop en pips. Cero deshabilita el trailing.
TrailingStepPips 15 Beneficio adicional (en pips) requerido antes de mover el trailing stop. Debe ser positivo cuando el trailing está habilitado.
MoneyMode FixedLot Selecciona entre volumen fijo y dimensionamiento por porcentaje de riesgo.
VolumeOrRisk 1.0 Tamaño del lote en modo FixedLot o porcentaje de riesgo en modo RiskPercent.
Hour1 10 Primera hora de trading. Establezca en 0 para deshabilitar.
Hour2 11 Segunda hora de trading. Establezca en 0 para deshabilitar.
Hour3 12 Tercera hora de trading. Establezca en 0 para deshabilitar.
CandleType Marco temporal de 1 hora Serie de velas usada para detectar rupturas; ajuste para reflejar el marco temporal del gráfico de MetaTrader.

Notas

  • Las comparaciones de horas usan el tiempo de cierre de la vela, que corresponde al momento en que TimeCurrent() de MetaTrader es igual al inicio de la siguiente barra.
  • La aproximación del nivel de congelación/stop garantiza que las órdenes stop nunca estén más cerca de diez pips del bid/ask actual, evitando los errores más comunes de MetaTrader.
  • Los trailing stops se actualizan en cada tick de Level1, asegurando un comportamiento cercano al manejador OnTick basado en ticks del experto original.
  • El dimensionamiento basado en riesgo usa Portfolio.CurrentValue cuando está disponible y recurre a Portfolio.BeginValue en caso contrario.

Consejos de uso

  1. Adjunte la estrategia a un instrumento y elija el mismo marco temporal que se usó en MetaTrader.
  2. Configure las horas de trading según la sesión que desea operar (establecerlas en 0 deshabilita ese slot).
  3. Seleccione el modo RiskPercent si desea escalado automático; asegúrese de que StopLossPips sea positivo.
  4. Para trading de lote fijo, mantenga MoneyMode en FixedLot y establezca VolumeOrRisk al tamaño deseado.
  5. Inicie la estrategia. Colocará dos órdenes pendientes en la próxima hora configurada y mantendrá el stop de protección automáticamente.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

public enum LbsMoneyMode
{
	FixedLot,
	RiskPercent,
}

/// <summary>
/// Previous-candle breakout strategy with paired conditional stops and Level1 trailing.
/// </summary>
public class LbsStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<LbsMoneyMode> _moneyMode;
	private readonly StrategyParam<decimal> _volumeOrRisk;
	private readonly StrategyParam<int> _hour1;
	private readonly StrategyParam<int> _hour2;
	private readonly StrategyParam<int> _hour3;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _bid;
	private decimal? _ask;
	private Order _buyPending;
	private Order _sellPending;
	private Order _protectiveStop;
	private decimal _entryPrice;

	public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public LbsMoneyMode MoneyMode { get => _moneyMode.Value; set => _moneyMode.Value = value; }
	public decimal VolumeOrRisk { get => _volumeOrRisk.Value; set => _volumeOrRisk.Value = value; }
	public int Hour1 { get => _hour1.Value; set => _hour1.Value = value; }
	public int Hour2 { get => _hour2.Value; set => _hour2.Value = value; }
	public int Hour3 { get => _hour3.Value; set => _hour3.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public LbsStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
		_trailingStopPips = Param(nameof(TrailingStopPips), 5).SetNotNegative();
		_trailingStepPips = Param(nameof(TrailingStepPips), 15).SetNotNegative();
		_moneyMode = Param(nameof(MoneyMode), LbsMoneyMode.FixedLot);
		_volumeOrRisk = Param(nameof(VolumeOrRisk), 1m).SetGreaterThanZero();
		_hour1 = Param(nameof(Hour1), 10).SetRange(0, 23);
		_hour2 = Param(nameof(Hour2), 11).SetRange(0, 23);
		_hour3 = Param(nameof(Hour3), 12).SetRange(0, 23);
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_bid = null;
		_ask = null;
		_buyPending = null;
		_sellPending = null;
		_protectiveStop = null;
		_entryPrice = 0m;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var quotes = new Subscription(DataType.Level1, Security);
		quotes.MarketData.BuildField = Level1Fields.BestAskPrice;
		SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

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

		var barEnd = CandleType.Arg is TimeSpan timeFrame ? candle.OpenTime + timeFrame : candle.CloseTime;
		if (!IsTradingHour(barEnd.Hour) || _bid is not decimal bid || _ask is not decimal ask)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		CancelEntryStops();

		var point = GetPoint();
		var (buyPrice, sellPrice) = CalculateBreakoutLevels(
			candle.HighPrice, candle.LowPrice, bid, ask, point);

		var volume = CalculateVolume(point);
		if (volume <= 0m)
			return;

		_buyPending = CreateStopOrder(Sides.Buy, buyPrice, volume, "LBS buy breakout");
		_sellPending = CreateStopOrder(Sides.Sell, sellPrice, volume, "LBS sell breakout");

		RegisterOrder(_buyPending);
		RegisterOrder(_sellPending);
	}

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
			_bid = bid;
		if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
			_ask = ask;

		if (Position == 0m || _protectiveStop is null || TrailingStopPips <= 0 || TrailingStepPips <= 0)
			return;

		var point = GetPoint();
		var trail = TrailingStopPips * point;
		var step = TrailingStepPips * point;

		if (Position > 0m && _bid is decimal longPrice)
		{
			if (longPrice - _entryPrice < trail + step)
				return;

			var candidate = longPrice - trail;
			var current = GetActivation(_protectiveStop);
			if (candidate >= current + step)
				ReplaceProtectiveStop(Sides.Sell, candidate);
		}
		else if (Position < 0m && _ask is decimal shortPrice)
		{
			if (_entryPrice - shortPrice < trail + step)
				return;

			var candidate = shortPrice + trail;
			var current = GetActivation(_protectiveStop);
			if (candidate <= current - step)
				ReplaceProtectiveStop(Sides.Buy, candidate);
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Order is null || trade.Trade is null)
			return;

		if (IsSameOrder(trade.Order, _buyPending) || IsSameOrder(trade.Order, _sellPending))
		{
			var filledSide = trade.Order.Side;
			_entryPrice = trade.Trade.Price;

			if (filledSide == Sides.Buy)
				CancelIfActive(_sellPending);
			else
				CancelIfActive(_buyPending);

			_buyPending = null;
			_sellPending = null;

			if (StopLossPips > 0 && Position != 0m)
			{
				var point = GetPoint();
				var stop = filledSide == Sides.Buy
					? _entryPrice - StopLossPips * point
					: _entryPrice + StopLossPips * point;

				ReplaceProtectiveStop(filledSide.Invert(), stop);
			}

			return;
		}

		if (IsSameOrder(trade.Order, _protectiveStop) && Position == 0m)
		{
			_protectiveStop = null;
			_entryPrice = 0m;
		}
	}

	private bool IsTradingHour(int hour)
		=> (Hour1 != 0 && hour == Hour1) ||
		   (Hour2 != 0 && hour == Hour2) ||
		   (Hour3 != 0 && hour == Hour3);

	private decimal CalculateVolume(decimal point)
	{
		if (MoneyMode == LbsMoneyMode.FixedLot)
			return NormalizeVolume(VolumeOrRisk);

		if (StopLossPips <= 0)
			return 0m;

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

		var riskMoney = balance * VolumeOrRisk / 100m;
		var stepPrice = Security?.StepPrice ?? 0m;
		var lossPerUnit = stepPrice > 0m
			? StopLossPips * stepPrice
			: StopLossPips * point * (Security?.Multiplier ?? 1m);

		return lossPerUnit > 0m ? NormalizeVolume(riskMoney / lossPerUnit) : 0m;
	}

	private Order CreateStopOrder(Sides side, decimal activationPrice, decimal volume, string comment)
		=> new()
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Conditional,
			Condition = new StopOrderCondition { ActivationPrice = Security.ShrinkPrice(activationPrice, ShrinkRules.Auto) },
			Side = side,
			Volume = volume,
			Comment = comment,
		};

	private void ReplaceProtectiveStop(Sides side, decimal activationPrice)
	{
		var old = _protectiveStop;
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		var replacement = CreateStopOrder(side, activationPrice, volume, "LBS protective stop");

		if (old is { State: OrderStates.Active })
			ReRegisterOrder(old, replacement);
		else
			RegisterOrder(replacement);

		_protectiveStop = replacement;
	}

	private void CancelEntryStops()
	{
		CancelIfActive(_buyPending);
		CancelIfActive(_sellPending);
		_buyPending = null;
		_sellPending = null;
	}

	private void CancelIfActive(Order order)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
	}

	private static bool IsSameOrder(Order left, Order right)
		=> left is not null && right is not null &&
		   (ReferenceEquals(left, right) || left.TransactionId == right.TransactionId);

	private static decimal GetActivation(Order order)
		=> order?.Condition is StopOrderCondition stop ? stop.ActivationPrice ?? 0m : 0m;

	private decimal NormalizeVolume(decimal volume)
	{
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = Math.Floor(volume / step) * step;

		return volume;
	}

	private decimal GetPoint()
	{
		var point = Security?.PriceStep ?? 0m;
		return point > 0m ? point : 0.0001m;
	}

	internal static (decimal buy, decimal sell) CalculateBreakoutLevels(
		decimal candleHigh,
		decimal candleLow,
		decimal bid,
		decimal ask,
		decimal priceStep)
	{
		var spread = Math.Max(0m, ask - bid);
		var buffer = Math.Max(3m * spread, 10m * priceStep);
		return (Math.Max(candleHigh, ask + buffer), Math.Min(candleLow, bid - buffer));
	}
}