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= 180SlBufferPoints= 50RiskReward= 1RiskPercent= 1PipValueUsd= 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class lanz_4_0_backtest_strategy(Strategy):
def __init__(self):
super(lanz_4_0_backtest_strategy, self).__init__()
self._swing_length = self.Param("SwingLength", 180) \
.SetGreaterThanZero() \
.SetDisplay("Swing Length", "Pivot swing length", "General")
self._sl_buffer_points = self.Param("SlBufferPoints", 50.0) \
.SetGreaterThanZero() \
.SetDisplay("SL Buffer", "Stop loss buffer points", "Risk")
self._risk_reward = self.Param("RiskReward", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("TP RR", "Take profit risk-reward", "Risk")
self._max_entries = self.Param("MaxEntries", 45) \
.SetGreaterThanZero() \
.SetDisplay("Max Entries", "Maximum entries per run", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._last_top = None
self._last_bottom = None
self._prev_high = None
self._prev_low = None
self._trend_dir = 0
self._top_crossed = False
self._bottom_crossed = False
self._signal_buy = False
self._signal_sell = False
self._stop_price = 0.0
self._take_profit_price = 0.0
self._entries_executed = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(lanz_4_0_backtest_strategy, self).OnReseted()
self._last_top = None
self._last_bottom = None
self._prev_high = None
self._prev_low = None
self._trend_dir = 0
self._top_crossed = False
self._bottom_crossed = False
self._signal_buy = False
self._signal_sell = False
self._stop_price = 0.0
self._take_profit_price = 0.0
self._entries_executed = 0
def OnStarted2(self, time):
super(lanz_4_0_backtest_strategy, self).OnStarted2(time)
highest = Highest()
highest.Length = self._swing_length.Value
lowest = Lowest()
lowest.Length = self._swing_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(highest, lowest, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, highest)
self.DrawIndicator(area, lowest)
self.DrawOwnTrades(area)
def OnProcess(self, candle, high_val, low_val):
if candle.State != CandleStates.Finished:
return
hv = float(high_val)
lv = float(low_val)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._last_top is None or hv != self._last_top:
self._prev_high = self._last_top
self._last_top = hv
if self._prev_high is not None and hv < self._prev_high:
self._trend_dir = -1
self._top_crossed = False
if self._last_bottom is None or lv != self._last_bottom:
self._prev_low = self._last_bottom
self._last_bottom = lv
if self._prev_low is not None and lv > self._prev_low:
self._trend_dir = 1
self._bottom_crossed = False
buy_signal = not self._top_crossed and self._prev_high is not None and close > self._prev_high
sell_signal = not self._bottom_crossed and self._prev_low is not None and close < self._prev_low
if self.Position == 0:
self._signal_buy = False
self._signal_sell = False
self._stop_price = 0.0
self._take_profit_price = 0.0
if buy_signal and not self._signal_buy and self.Position == 0:
self._signal_buy = True
if sell_signal and not self._signal_sell and self.Position == 0:
self._signal_sell = True
buf = float(self._sl_buffer_points.Value) * 0.0001
rr = float(self._risk_reward.Value)
if self._signal_buy and self.Position == 0 and self._entries_executed < self._max_entries.Value:
sl = low - buf
tp = close + (close - sl) * rr
self.BuyMarket()
self._stop_price = sl
self._take_profit_price = tp
self._top_crossed = True
self._entries_executed += 1
elif self._signal_sell and self.Position == 0 and self._entries_executed < self._max_entries.Value:
sl = high + buf
tp = close - (sl - close) * rr
self.SellMarket()
self._stop_price = sl
self._take_profit_price = tp
self._bottom_crossed = True
self._entries_executed += 1
if self.Position > 0 and (self._stop_price > 0.0 or self._take_profit_price > 0.0):
if low <= self._stop_price or high >= self._take_profit_price:
self.SellMarket()
elif self.Position < 0 and (self._stop_price > 0.0 or self._take_profit_price > 0.0):
if high >= self._stop_price or low <= self._take_profit_price:
self.BuyMarket()
def CreateClone(self):
return lanz_4_0_backtest_strategy()