Backtest da Estratégia LANZ 4.0
O Backtest da Estratégia LANZ 4.0 é uma estratégia de rompimento que usa pivôs de swing para detectar mudanças de tendência. Quando o preço rompe acima do último máximo de pivô, entra comprado; quando o preço rompe abaixo do último mínimo de pivô, entra vendido. O tamanho da posição é calculado a partir do percentual de risco e do valor em pips, com stop-loss abaixo/acima do último swing mais buffer e take-profit pela relação risco-retorno.
Detalhes
- Dados: Velas de preço.
- Critérios de entrada:
- Comprado: O preço cruza acima do último máximo de pivô.
- Vendido: O preço cruza abaixo do último mínimo de pivô.
- Critérios de saída: Stop-loss ou take-profit.
- Stops: Máximo/mínimo de swing recente com buffer.
- Valores padrão:
SwingLength= 180SlBufferPoints= 50RiskReward= 1RiskPercent= 1PipValueUsd= 10
- Filtros:
- Categoria: Rompimento
- Direção: Comprado e Vendido
- Indicadores: Highest, Lowest
- Complexidade: Moderado
- Nível de risco: Médio
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// LANZ Strategy 4.0 Backtest.
/// A pivot high (low) is a candle whose high (low) is the extreme of the SwingLength candles on each side of it. A close crossing above
/// the last pivot high goes long and a close crossing below the last pivot low goes short, reversing an opposite position. The stop sits
/// SlBufferPoints price steps beyond the opposite pivot, the target RiskReward times the stop distance away, and the volume risks
/// RiskPercent of equity with PipValueUsd per price step and lot.
/// </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<DataType> _candleType;
private readonly List<decimal> _highs = [];
private readonly List<decimal> _lows = [];
private decimal? _pivotHigh;
private decimal? _pivotLow;
private decimal? _prevClose;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Candles on each side of a pivot.
/// </summary>
public int SwingLength
{
get => _swingLength.Value;
set => _swingLength.Value = value;
}
/// <summary>
/// Stop buffer beyond the pivot in price steps.
/// </summary>
public decimal SlBufferPoints
{
get => _slBufferPoints.Value;
set => _slBufferPoints.Value = value;
}
/// <summary>
/// Take profit as a multiple of the stop distance.
/// </summary>
public decimal RiskReward
{
get => _riskReward.Value;
set => _riskReward.Value = value;
}
/// <summary>
/// Percent of equity risked per trade.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Money value of one price step for one lot.
/// </summary>
public decimal PipValueUsd
{
get => _pipValueUsd.Value;
set => _pipValueUsd.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="Lanz40BacktestStrategy"/>.
/// </summary>
public Lanz40BacktestStrategy()
{
_swingLength = Param(nameof(SwingLength), 180)
.SetGreaterThanZero()
.SetDisplay("Swing Length", "Candles on each side of a pivot", "General");
_slBufferPoints = Param(nameof(SlBufferPoints), 50m)
.SetNotNegative()
.SetDisplay("SL Buffer", "Stop buffer beyond the pivot in price steps", "Risk");
_riskReward = Param(nameof(RiskReward), 1m)
.SetGreaterThanZero()
.SetDisplay("Risk Reward", "Take profit as a multiple of the stop distance", "Risk");
_riskPercent = Param(nameof(RiskPercent), 1m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Percent of equity risked per trade", "Risk");
_pipValueUsd = Param(nameof(PipValueUsd), 10m)
.SetGreaterThanZero()
.SetDisplay("Pip Value USD", "Money value of one price step for one lot", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_highs.Clear();
_lows.Clear();
_pivotHigh = _pivotLow = null;
_prevClose = null;
_stopPrice = _takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdatePivots(candle);
var prevClose = _prevClose;
var close = candle.ClosePrice;
_prevClose = close;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position > 0 && _stopPrice is decimal longStop && _takePrice is decimal longTake
&& (candle.LowPrice <= longStop || candle.HighPrice >= longTake))
{
SellMarket(Position);
_stopPrice = _takePrice = null;
return;
}
if (Position < 0 && _stopPrice is decimal shortStop && _takePrice is decimal shortTake
&& (candle.HighPrice >= shortStop || candle.LowPrice <= shortTake))
{
BuyMarket(-Position);
_stopPrice = _takePrice = null;
return;
}
if (prevClose is not decimal last || _pivotHigh is not decimal pivotHigh || _pivotLow is not decimal pivotLow)
return;
var step = Security.PriceStep ?? 1m;
var buffer = SlBufferPoints * step;
if (last <= pivotHigh && close > pivotHigh && Position <= 0)
{
var stop = pivotLow - buffer;
if (stop >= close)
return;
BuyMarket(CalculateVolume(close - stop, step) + Math.Abs(Position));
_stopPrice = stop;
_takePrice = close + (close - stop) * RiskReward;
}
else if (last >= pivotLow && close < pivotLow && Position >= 0)
{
var stop = pivotHigh + buffer;
if (stop <= close)
return;
SellMarket(CalculateVolume(stop - close, step) + Math.Abs(Position));
_stopPrice = stop;
_takePrice = close - (stop - close) * RiskReward;
}
}
private void UpdatePivots(ICandleMessage candle)
{
var window = SwingLength * 2 + 1;
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > window)
{
_highs.RemoveAt(0);
_lows.RemoveAt(0);
}
if (_highs.Count < window)
return;
// The candle in the middle of the window is a pivot once SwingLength candles have closed after it.
var high = _highs[SwingLength];
if (high == _highs.Max())
_pivotHigh = high;
var low = _lows[SwingLength];
if (low == _lows.Min())
_pivotLow = low;
}
private decimal CalculateVolume(decimal stopDistance, decimal step)
{
var equity = Portfolio?.CurrentValue ?? 0m;
var steps = stopDistance / step;
var volume = steps > 0m ? equity * RiskPercent / 100m / (steps * PipValueUsd) : 0m;
if (Security.VolumeStep is decimal volumeStep && volumeStep > 0m)
volume = Math.Floor(volume / volumeStep) * volumeStep;
return volume > 0m ? volume : Volume;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class lanz_4_0_backtest_strategy(Strategy):
"""
LANZ Strategy 4.0 Backtest.
A pivot high (low) is a candle whose high (low) is the extreme of the SwingLength candles on each side of it. A close crossing above
the last pivot high goes long and a close crossing below the last pivot low goes short, reversing an opposite position. The stop sits
SlBufferPoints price steps beyond the opposite pivot, the target RiskReward times the stop distance away, and the volume risks
RiskPercent of equity with PipValueUsd per price step and lot.
"""
def __init__(self):
super(lanz_4_0_backtest_strategy, self).__init__()
self._swing_length = self.Param("SwingLength", 180).SetGreaterThanZero().SetDisplay("Swing Length", "Candles on each side of a pivot", "General")
self._sl_buffer_points = self.Param("SlBufferPoints", 50.0).SetNotNegative().SetDisplay("SL Buffer", "Stop buffer beyond the pivot in price steps", "Risk")
self._risk_reward = self.Param("RiskReward", 1.0).SetGreaterThanZero().SetDisplay("Risk Reward", "Take profit as a multiple of the stop distance", "Risk")
self._risk_percent = self.Param("RiskPercent", 1.0).SetGreaterThanZero().SetDisplay("Risk %", "Percent of equity risked per trade", "Risk")
self._pip_value_usd = self.Param("PipValueUsd", 10.0).SetGreaterThanZero().SetDisplay("Pip Value USD", "Money value of one price step for one lot", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._highs = []
self._lows = []
self._pivot_high = None
self._pivot_low = None
self._prev_close = None
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(lanz_4_0_backtest_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(lanz_4_0_backtest_strategy, self).OnStarted2(time)
self._reset_state()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._update_pivots(high, low)
prev_close = self._prev_close
self._prev_close = close
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position > 0 and self._stop_price is not None and self._take_price is not None:
if low <= self._stop_price or high >= self._take_price:
self.SellMarket(self.Position)
self._stop_price = None
self._take_price = None
return
if self.Position < 0 and self._stop_price is not None and self._take_price is not None:
if high >= self._stop_price or low <= self._take_price:
self.BuyMarket(-self.Position)
self._stop_price = None
self._take_price = None
return
if prev_close is None or self._pivot_high is None or self._pivot_low is None:
return
step = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 1.0
buffer = float(self._sl_buffer_points.Value) * step
rr = float(self._risk_reward.Value)
pivot_high = self._pivot_high
pivot_low = self._pivot_low
if prev_close <= pivot_high and close > pivot_high and self.Position <= 0:
stop = pivot_low - buffer
if stop >= close:
return
self.BuyMarket(self._calculate_volume(close - stop, step) + abs(self.Position))
self._stop_price = stop
self._take_price = close + (close - stop) * rr
elif prev_close >= pivot_low and close < pivot_low and self.Position >= 0:
stop = pivot_high + buffer
if stop <= close:
return
self.SellMarket(self._calculate_volume(stop - close, step) + abs(self.Position))
self._stop_price = stop
self._take_price = close - (stop - close) * rr
def _update_pivots(self, high, low):
length = self._swing_length.Value
window = length * 2 + 1
self._highs.append(high)
self._lows.append(low)
if len(self._highs) > window:
self._highs.pop(0)
self._lows.pop(0)
if len(self._highs) < window:
return
# The candle in the middle of the window is a pivot once SwingLength candles have closed after it.
mid_high = self._highs[length]
if mid_high == max(self._highs):
self._pivot_high = mid_high
mid_low = self._lows[length]
if mid_low == min(self._lows):
self._pivot_low = mid_low
def _calculate_volume(self, stop_distance, step):
portfolio = self.Portfolio
equity = float(portfolio.CurrentValue) if portfolio is not None and portfolio.CurrentValue is not None else 0.0
steps = stop_distance / step
volume = equity * float(self._risk_percent.Value) / 100.0 / (steps * float(self._pip_value_usd.Value)) if steps > 0.0 else 0.0
volume_step = self.Security.VolumeStep
if volume_step is not None and float(volume_step) > 0.0:
vs = float(volume_step)
volume = math.floor(volume / vs) * vs
return Decimal(volume) if volume > 0.0 else self.Volume
def CreateClone(self):
return lanz_4_0_backtest_strategy()