LANZ戦略 4.0 バックテスト
LANZ戦略4.0バックテストは、スウィングピボットを使ってトレンド転換を検出するブレイクアウト戦略です。価格が最後のピボット高値を上抜けるとロングエントリー、最後のピボット安値を下抜けるとショートエントリーします。ポジションサイズはリスクパーセントとpips値から計算され、ストップロスは最後のスウィングにバッファを加えた位置、テイクプロフィットはリスクリワード比で設定されます。
詳細
- データ: 価格ローソク足。
- エントリー条件:
- ロング: 価格が最後のピボット高値を上抜ける。
- ショート: 価格が最後のピボット安値を下抜ける。
- エグジット条件: ストップロスまたはテイクプロフィット。
- ストップ: 直近スウィング高値/安値にバッファを追加。
- デフォルト値:
SwingLength= 180SlBufferPoints= 50RiskReward= 1RiskPercent= 1PipValueUsd= 10
- フィルター:
- カテゴリ: ブレイクアウト
- 方向: ロング&ショート
- インジケーター: Highest, Lowest
- 複雑さ: 中程度
- リスクレベル: 中
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()