LANZ Strategy 2.0 [Бэктест]
Стратегия отслеживает структуру рынка и тренд через последние свинги. Вход выполняется в 02:00 по Нью-Йорку после пробоя структуры в сторону тренда. Стоп-лосс определяется по свингам или по полной защите, тейк-профит рассчитывается множителем риск/прибыль. Открытые позиции закрываются вручную в 11:45 по Нью-Йорку.
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>
/// LANZ strategy based on swing structure and break of structure.
/// Enters at 02:00 New York time when BOS aligns with trend.
/// Calculates stop-loss from swing points and risk-reward multiplier for take-profit.
/// Positions are closed manually at 11:45 New York time.
/// </summary>
public class Lanz20BacktestStrategy : Strategy
{
/// <summary>
/// Modes of stop-loss protection.
/// </summary>
public enum SlProtectionModeOptions
{
/// <summary>Use last swing.</summary>
FirstSwing,
/// <summary>Use second swing.</summary>
SecondSwing,
/// <summary>Use full coverage of swings.</summary>
FullCoverage
}
private readonly StrategyParam<decimal> _accountSizeUsd;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<SlProtectionModeOptions> _slProtectionMode;
private readonly StrategyParam<int> _fullCoveragePips;
private readonly StrategyParam<decimal> _minBosBreakPips;
private readonly StrategyParam<decimal> _rrMultiplier;
private readonly StrategyParam<int> _cooldownDays;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
private decimal? _prevHigh;
private decimal? _prevLow;
private decimal? _lastSwingHigh;
private decimal? _lastSwingLow;
private decimal? _olderSwingHigh;
private decimal? _olderSwingLow;
private int? _trendDir;
private decimal? _bosLevel;
private int? _bosDir;
private int? _lastBosDir;
private int? _lastTrendDir;
private decimal _pipSize;
private decimal _minBosBreakDist;
private decimal _stopPrice;
private decimal _takeProfitPrice;
private DateTime _nextTradeDate;
private readonly TimeZoneInfo _nyZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
/// <summary>
/// Account size in USD.
/// </summary>
public decimal AccountSizeUsd
{
get => _accountSizeUsd.Value;
set => _accountSizeUsd.Value = value;
}
/// <summary>
/// Risk per trade in percent.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Stop-loss protection mode.
/// </summary>
public SlProtectionModeOptions SlProtectionMode
{
get => _slProtectionMode.Value;
set => _slProtectionMode.Value = value;
}
/// <summary>
/// Pips used in full coverage mode.
/// </summary>
public int FullCoveragePips
{
get => _fullCoveragePips.Value;
set => _fullCoveragePips.Value = value;
}
/// <summary>
/// Minimum break of structure distance in pips.
/// </summary>
public decimal MinBosBreakPips
{
get => _minBosBreakPips.Value;
set => _minBosBreakPips.Value = value;
}
/// <summary>
/// Risk reward multiplier.
/// </summary>
public decimal RrMultiplier
{
get => _rrMultiplier.Value;
set => _rrMultiplier.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Minimum number of days between entries.
/// </summary>
public int CooldownDays
{
get => _cooldownDays.Value;
set => _cooldownDays.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public Lanz20BacktestStrategy()
{
_accountSizeUsd = Param(nameof(AccountSizeUsd), 100000000m)
.SetDisplay("Account Size", "Account size in USD", "Risk");
_riskPercent = Param(nameof(RiskPercent), 1m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Risk percentage", "Risk");
_slProtectionMode = Param(nameof(SlProtectionMode), SlProtectionModeOptions.FullCoverage)
.SetDisplay("SL Mode", "Stop-loss protection mode", "Risk");
_fullCoveragePips = Param(nameof(FullCoveragePips), 12)
.SetDisplay("Full Coverage Pips", "Pips for full coverage", "Risk");
_minBosBreakPips = Param(nameof(MinBosBreakPips), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Min BOS Break", "Minimum break of structure in pips", "General");
_rrMultiplier = Param(nameof(RrMultiplier), 5.5m)
.SetGreaterThanZero()
.SetDisplay("RR Multiplier", "Risk reward multiplier", "Risk");
_cooldownDays = Param(nameof(CooldownDays), 1)
.SetGreaterThanZero()
.SetDisplay("Cooldown Days", "Minimum days between entries", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highs.Clear();
_lows.Clear();
_prevHigh = null;
_prevLow = null;
_lastSwingHigh = null;
_lastSwingLow = null;
_olderSwingHigh = null;
_olderSwingLow = null;
_trendDir = null;
_bosLevel = null;
_bosDir = null;
_lastBosDir = null;
_lastTrendDir = null;
_pipSize = 0m;
_minBosBreakDist = 0m;
_stopPrice = 0m;
_takeProfitPrice = 0m;
_nextTradeDate = DateTime.MinValue;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = (Security.PriceStep ?? 1m) * 10m;
_minBosBreakDist = MinBosBreakPips * _pipSize;
_nextTradeDate = DateTime.MinValue;
var dummyEma1 = new ExponentialMovingAverage { Length = 10 };
var dummyEma2 = new ExponentialMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(dummyEma1, dummyEma2, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal d1, decimal d2)
{
if (candle.State != CandleStates.Finished)
return;
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > 10)
{
_highs.RemoveAt(0);
_lows.RemoveAt(0);
}
if (_highs.Count >= 5)
{
var high2 = _highs[^3];
var high3 = _highs[^4];
var high4 = _highs[^5];
var low2 = _lows[^3];
var low3 = _lows[^4];
var low4 = _lows[^5];
var hh = high2 > high3 && high3 > high4;
var ll = low2 < low3 && low3 < low4;
if (hh)
{
_olderSwingHigh = _prevHigh;
_prevHigh = _lastSwingHigh;
_lastSwingHigh = high2;
}
if (ll)
{
_olderSwingLow = _prevLow;
_prevLow = _lastSwingLow;
_lastSwingLow = low2;
}
}
if (_prevHigh.HasValue && _lastSwingHigh.HasValue && _prevLow.HasValue && _lastSwingLow.HasValue)
{
var isBullish = _lastSwingHigh > _prevHigh && _lastSwingLow > _prevLow;
var isBearish = _lastSwingHigh < _prevHigh && _lastSwingLow < _prevLow;
_trendDir = isBullish ? 1 : isBearish ? -1 : _trendDir;
}
var newBosUp = _lastSwingHigh.HasValue && candle.ClosePrice > _lastSwingHigh + _minBosBreakDist;
var newBosDown = _lastSwingLow.HasValue && candle.ClosePrice < _lastSwingLow - _minBosBreakDist;
if (newBosUp)
{
_bosLevel = _lastSwingHigh;
_bosDir = 1;
}
else if (newBosDown)
{
_bosLevel = _lastSwingLow;
_bosDir = -1;
}
if (_bosDir.HasValue)
_lastBosDir = _bosDir;
if (_trendDir.HasValue)
_lastTrendDir = _trendDir;
var nyTime = candle.OpenTime;
var isAnalysisBar = nyTime.Hour == 10 && nyTime.Minute < 15;
var manualClose = nyTime.Hour == 15 && nyTime.Minute >= 30;
var alreadyInTrade = Position != 0;
if (alreadyInTrade)
{
if (Position > 0)
{
if (candle.LowPrice <= _stopPrice)
{
SellMarket();
return;
}
if (candle.HighPrice >= _takeProfitPrice)
{
SellMarket();
return;
}
}
else
{
if (candle.HighPrice >= _stopPrice)
{
BuyMarket();
return;
}
if (candle.LowPrice <= _takeProfitPrice)
{
BuyMarket();
return;
}
}
if (manualClose)
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
}
if (alreadyInTrade)
return;
if (candle.OpenTime.Date < _nextTradeDate)
return;
var enterLong = isAnalysisBar && _lastBosDir == 1;
var enterShort = isAnalysisBar && _lastBosDir == -1;
var entryPrice = candle.ClosePrice;
var riskUsd = AccountSizeUsd * (RiskPercent / 100m);
if (enterLong)
{
var fallbackSl = entryPrice - 5m * _pipSize;
var slBase = SlProtectionMode switch
{
SlProtectionModeOptions.FirstSwing => _lastSwingLow ?? fallbackSl,
SlProtectionModeOptions.SecondSwing => _prevLow ?? fallbackSl,
SlProtectionModeOptions.FullCoverage => (_olderSwingLow == null || _prevLow == null || _lastSwingLow == null)
? fallbackSl
: Math.Min((decimal)_olderSwingLow, Math.Min((decimal)_prevLow, (decimal)_lastSwingLow)) - FullCoveragePips * _pipSize,
_ => fallbackSl
};
var slPrice = (entryPrice - slBase) < (10m * _pipSize) ? entryPrice - 10m * _pipSize : slBase;
var tpPrice = entryPrice + RrMultiplier * (entryPrice - slPrice);
var slPips = Math.Abs(entryPrice - slPrice) / _pipSize;
var lotSize = slPips == 0 ? 0 : riskUsd / (slPips * 10m);
if (lotSize > 0)
{
BuyMarket();
_stopPrice = slPrice;
_takeProfitPrice = tpPrice;
_nextTradeDate = candle.OpenTime.Date.AddDays(CooldownDays);
}
}
else if (enterShort)
{
var fallbackSl = entryPrice + 5m * _pipSize;
var slBase = SlProtectionMode switch
{
SlProtectionModeOptions.FirstSwing => _lastSwingHigh ?? fallbackSl,
SlProtectionModeOptions.SecondSwing => _prevHigh ?? fallbackSl,
SlProtectionModeOptions.FullCoverage => (_olderSwingHigh == null || _prevHigh == null || _lastSwingHigh == null)
? fallbackSl
: Math.Max((decimal)_olderSwingHigh, Math.Max((decimal)_prevHigh, (decimal)_lastSwingHigh)) + FullCoveragePips * _pipSize,
_ => fallbackSl
};
var slPrice = (slBase - entryPrice) < (10m * _pipSize) ? entryPrice + 10m * _pipSize : slBase;
var tpPrice = entryPrice - RrMultiplier * (slPrice - entryPrice);
var slPips = Math.Abs(entryPrice - slPrice) / _pipSize;
var lotSize = slPips == 0 ? 0 : riskUsd / (slPips * 10m);
if (lotSize > 0)
{
SellMarket();
_stopPrice = slPrice;
_takeProfitPrice = tpPrice;
_nextTradeDate = candle.OpenTime.Date.AddDays(CooldownDays);
}
}
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class lanz20_backtest_strategy(Strategy):
"""
LANZ strategy: swing structure BOS with session time entry and SL/TP.
"""
def __init__(self):
super(lanz20_backtest_strategy, self).__init__()
self._rr_multiplier = self.Param("RrMultiplier", 5.5).SetDisplay("RR Multiplier", "Risk reward multiplier", "Risk")
self._cooldown_days = self.Param("CooldownDays", 1).SetDisplay("Cooldown Days", "Min days between entries", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._highs = []
self._lows = []
self._last_swing_high = 0.0
self._last_swing_low = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._bos_dir = 0
self._stop_price = 0.0
self._tp_price = 0.0
self._last_trade_day = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(lanz20_backtest_strategy, self).OnReseted()
self._highs = []
self._lows = []
self._last_swing_high = 0.0
self._last_swing_low = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._bos_dir = 0
self._stop_price = 0.0
self._tp_price = 0.0
self._last_trade_day = None
def OnStarted2(self, time):
super(lanz20_backtest_strategy, self).OnStarted2(time)
ema1 = ExponentialMovingAverage()
ema1.Length = 10
ema2 = ExponentialMovingAverage()
ema2.Length = 20
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema1, ema2, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, d1, d2):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
pip_size = step * 10
self._highs.append(high)
self._lows.append(low)
if len(self._highs) > 10:
self._highs.pop(0)
self._lows.pop(0)
if len(self._highs) >= 5:
h2, h3, h4 = self._highs[-3], self._highs[-4], self._highs[-5]
l2, l3, l4 = self._lows[-3], self._lows[-4], self._lows[-5]
if h2 > h3 and h3 > h4:
self._prev_high = self._last_swing_high
self._last_swing_high = h2
if l2 < l3 and l3 < l4:
self._prev_low = self._last_swing_low
self._last_swing_low = l2
if self._last_swing_high > 0 and close > self._last_swing_high + 0.5 * pip_size:
self._bos_dir = 1
elif self._last_swing_low > 0 and close < self._last_swing_low - 0.5 * pip_size:
self._bos_dir = -1
hour = candle.OpenTime.Hour
if self.Position != 0:
if self.Position > 0:
if low <= self._stop_price:
self.SellMarket()
return
if high >= self._tp_price:
self.SellMarket()
return
else:
if high >= self._stop_price:
self.BuyMarket()
return
if low <= self._tp_price:
self.BuyMarket()
return
if hour == 15:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
today = candle.OpenTime.Date
if self._last_trade_day is not None:
diff = (today - self._last_trade_day).Days
if diff < self._cooldown_days.Value:
return
if hour != 10:
return
if self._bos_dir == 1:
sl = self._last_swing_low if self._last_swing_low > 0 else close - 5 * pip_size
if close - sl < 10 * pip_size:
sl = close - 10 * pip_size
tp = close + self._rr_multiplier.Value * (close - sl)
self.BuyMarket()
self._stop_price = sl
self._tp_price = tp
self._last_trade_day = today
elif self._bos_dir == -1:
sl = self._last_swing_high if self._last_swing_high > 0 else close + 5 * pip_size
if sl - close < 10 * pip_size:
sl = close + 10 * pip_size
tp = close - self._rr_multiplier.Value * (sl - close)
self.SellMarket()
self._stop_price = sl
self._tp_price = tp
self._last_trade_day = today
def CreateClone(self):
return lanz20_backtest_strategy()