Ver en GitHub

Backtest de Estrategia LANZ 4.0

El Backtest de la Estrategia LANZ 4.0 es una estrategia de ruptura que utiliza pivotes de swing para detectar cambios de tendencia. Cuando el precio rompe por encima del último máximo de pivote, entra en largo; cuando el precio rompe por debajo del último mínimo de pivote, entra en corto. El tamaño de posición se calcula a partir del porcentaje de riesgo y el valor en pips, con stop-loss por debajo/encima del último swing más un buffer y take-profit por ratio riesgo-recompensa.

Detalles

  • Datos: Velas de precio.
  • Criterios de entrada:
    • Largo: El precio cruza por encima del último máximo de pivote.
    • Corto: El precio cruza por debajo del último mínimo de pivote.
  • Criterios de salida: Stop-loss o take-profit.
  • Stops: Máximo/mínimo de swing reciente con buffer.
  • Valores predeterminados:
    • SwingLength = 180
    • SlBufferPoints = 50
    • RiskReward = 1
    • RiskPercent = 1
    • PipValueUsd = 10
  • Filtros:
    • Categoría: Ruptura
    • Dirección: Largo y corto
    • Indicadores: Highest, Lowest
    • Complejidad: Moderado
    • Nivel de riesgo: Medio

using System;
using System.Collections.Generic;

using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy based on pivot highs and lows with dynamic risk management.
/// </summary>
public class Lanz40BacktestStrategy : Strategy
{
	private readonly StrategyParam<int> _swingLength;
	private readonly StrategyParam<decimal> _slBufferPoints;
	private readonly StrategyParam<decimal> _riskReward;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _pipValueUsd;
	private readonly StrategyParam<int> _maxEntries;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest;
	private Lowest _lowest;

	private decimal? _lastTop;
	private decimal? _lastBottom;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private int _trendDir;
	private bool _topCrossed;
	private bool _bottomCrossed;
	private bool _topWasStrong;
	private bool _bottomWasStrong;

	private decimal? _entryPriceBuy;
	private decimal? _entryPriceSell;
	private bool _signalTriggeredBuy;
	private bool _signalTriggeredSell;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private int _entriesExecuted;

	/// <summary>
	/// Pivot swing length.
	/// </summary>
	public int SwingLength
	{
	    get => _swingLength.Value;
	    set => _swingLength.Value = value;
	}

	/// <summary>
	/// Stop loss buffer in points.
	/// </summary>
	public decimal SlBufferPoints
	{
	    get => _slBufferPoints.Value;
	    set => _slBufferPoints.Value = value;
	}

	/// <summary>
	/// Risk reward multiplier.
	/// </summary>
	public decimal RiskReward
	{
	    get => _riskReward.Value;
	    set => _riskReward.Value = value;
	}

	/// <summary>
	/// Risk percent of equity.
	/// </summary>
	public decimal RiskPercent
	{
	    get => _riskPercent.Value;
	    set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Pip value in USD for one lot.
	/// </summary>
	public decimal PipValueUsd
	{
	    get => _pipValueUsd.Value;
	    set => _pipValueUsd.Value = value;
	}

	/// <summary>
	/// Candle type for strategy calculation.
	/// </summary>
	public DataType CandleType
	{
	    get => _candleType.Value;
	    set => _candleType.Value = value;
	}

	/// <summary>
	/// Maximum entries per run.
	/// </summary>
	public int MaxEntries
	{
	    get => _maxEntries.Value;
	    set => _maxEntries.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="Lanz40BacktestStrategy"/>.
	/// </summary>
	public Lanz40BacktestStrategy()
	{
	    _swingLength = Param(nameof(SwingLength), 180)
	        .SetDisplay("Swing Length", "Pivot swing length", "General")
	        .SetGreaterThanZero();

	    _slBufferPoints = Param(nameof(SlBufferPoints), 50m)
	        .SetDisplay("SL Buffer", "Stop loss buffer (points)", "Risk")
	        .SetGreaterThanZero();

	    _riskReward = Param(nameof(RiskReward), 1m)
	        .SetDisplay("TP RR", "Take profit risk-reward", "Risk")
	        .SetGreaterThanZero();

	    _riskPercent = Param(nameof(RiskPercent), 1m)
	        .SetDisplay("Risk %", "Risk percent per trade", "Risk")
	        .SetGreaterThanZero();

	    _pipValueUsd = Param(nameof(PipValueUsd), 10m)
	        .SetDisplay("Pip Value USD", "Pip value for one lot", "Risk")
	        .SetGreaterThanZero();

	    _maxEntries = Param(nameof(MaxEntries), 45)
	        .SetDisplay("Max Entries", "Maximum entries per run", "Risk")
	        .SetGreaterThanZero();

	    _candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
	        .SetDisplay("Candle Type", "Type of candles", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
	    base.OnReseted();
	    _lastTop = null;
	    _lastBottom = null;
	    _prevHigh = null;
	    _prevLow = null;
	    _trendDir = 0;
	    _topCrossed = false;
	    _bottomCrossed = false;
	    _topWasStrong = false;
	    _bottomWasStrong = false;
	    _entryPriceBuy = null;
	    _entryPriceSell = null;
	    _signalTriggeredBuy = false;
	    _signalTriggeredSell = false;
	    _stopPrice = 0m;
	    _takeProfitPrice = 0m;
	    _entriesExecuted = 0;
	}

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

	    _highest = new Highest { Length = SwingLength };
	    _lowest = new Lowest { Length = SwingLength };

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

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

	    StartProtection(null, null);
	}

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

	    if (!IsFormedAndOnlineAndAllowTrading())
	        return;

	    if (!_highest.IsFormed || !_lowest.IsFormed)
	        return;

	    if (!_lastTop.HasValue || highValue != _lastTop.Value)
	    {
	        _prevHigh = _lastTop;
	        _lastTop = highValue;
	        if (_prevHigh.HasValue && highValue < _prevHigh.Value)
	            _trendDir = -1;
	        _topWasStrong = _trendDir == -1;
	        _topCrossed = false;
	    }

	    if (!_lastBottom.HasValue || lowValue != _lastBottom.Value)
	    {
	        _prevLow = _lastBottom;
	        _lastBottom = lowValue;
	        if (_prevLow.HasValue && lowValue > _prevLow.Value)
	            _trendDir = 1;
	        _bottomWasStrong = _trendDir == 1;
	        _bottomCrossed = false;
	    }

	    var buySignal = !_topCrossed && _prevHigh.HasValue && candle.ClosePrice > _prevHigh.Value;
	    var sellSignal = !_bottomCrossed && _prevLow.HasValue && candle.ClosePrice < _prevLow.Value;

	    if (Position == 0)
	    {
	        _signalTriggeredBuy = false;
	        _signalTriggeredSell = false;
	        _entryPriceBuy = null;
	        _entryPriceSell = null;
	        _stopPrice = 0m;
	        _takeProfitPrice = 0m;
	    }

	    if (buySignal && !_signalTriggeredBuy && Position == 0)
	    {
	        _entryPriceBuy = candle.ClosePrice;
	        _signalTriggeredBuy = true;
	    }

	    if (sellSignal && !_signalTriggeredSell && Position == 0)
	    {
	        _entryPriceSell = candle.ClosePrice;
	        _signalTriggeredSell = true;
	    }

	    var pip = (Security.PriceStep ?? 0m) * 10m;
	    var buffer = SlBufferPoints * pip;

	    if (_signalTriggeredBuy && Position == 0 && _entriesExecuted < MaxEntries && _entryPriceBuy.HasValue)
	    {
	        var sl = candle.LowPrice - buffer;
	        var tp = _entryPriceBuy.Value + (_entryPriceBuy.Value - sl) * RiskReward;
	        var qty = CalculateQty(_entryPriceBuy.Value, sl, pip);
	        if (qty > 0m)
	        {
	            BuyMarket(qty);
	            _stopPrice = sl;
	            _takeProfitPrice = tp;
	            _topCrossed = true;
	            _entriesExecuted++;
	        }
	    }
	    else if (_signalTriggeredSell && Position == 0 && _entriesExecuted < MaxEntries && _entryPriceSell.HasValue)
	    {
	        var sl = candle.HighPrice + buffer;
	        var tp = _entryPriceSell.Value - (sl - _entryPriceSell.Value) * RiskReward;
	        var qty = CalculateQty(_entryPriceSell.Value, sl, pip);
	        if (qty > 0m)
	        {
	            SellMarket(qty);
	            _stopPrice = sl;
	            _takeProfitPrice = tp;
	            _bottomCrossed = true;
	            _entriesExecuted++;
	        }
	    }

	    if (Position > 0 && (_stopPrice > 0m || _takeProfitPrice > 0m))
	    {
	        if (candle.LowPrice <= _stopPrice || candle.HighPrice >= _takeProfitPrice)
	            SellMarket(Math.Abs(Position));
	    }
	    else if (Position < 0 && (_stopPrice > 0m || _takeProfitPrice > 0m))
	    {
	        if (candle.HighPrice >= _stopPrice || candle.LowPrice <= _takeProfitPrice)
	            BuyMarket(Math.Abs(Position));
	    }
	}

	private decimal CalculateQty(decimal entry, decimal sl, decimal pip)
	{
	    var equity = Portfolio?.CurrentValue ?? 0m;
	    var riskUsd = equity * RiskPercent / 100m;
	    var slPips = Math.Abs(entry - sl) / (pip == 0m ? 1m : pip);
	    return slPips > 0m ? riskUsd / (slPips * PipValueUsd) : 0m;
	}
}