LANZ Strategy 4.0 Backtest
LANZ Strategy 4.0 Backtest — пробойная стратегия, определяющая развороты по свинговым максимумам и минимумам. Вход выполняется при пробое последнего пивота, размер позиции рассчитывается от капитала, процента риска и стоимости пункта. Стоп-лосс ставится за последний свинг с буфером, тейк-профит рассчитывается по коэффициенту риск/прибыль.
Подробности
- Данные: ценовые свечи.
- Условия входа:
- Лонг: цена пробивает последний максимум.
- Шорт: цена пробивает последний минимум.
- Условия выхода: стоп-лосс или тейк-профит.
- Стопы: последний свинг плюс буфер.
- Параметры по умолчанию:
SwingLength= 180SlBufferPoints= 50RiskReward= 1RiskPercent= 1PipValueUsd= 10
- Фильтры:
- Категория: пробойная
- Направление: лонг и шорт
- Индикаторы: Highest, Lowest
- Сложность: средняя
- Уровень риска: средний
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()