LANZ Strategy 4.0 Backtest 策略
LANZ Strategy 4.0 Backtest 是一种利用摆动枢轴检测趋势的突破策略。当价格突破最后一个枢轴高点时做多,跌破最后一个枢轴低点时做空。仓位大小根据账户权益、风险百分比和点值计算,止损设在最近摆动点加缓冲,止盈按风险收益比计算。
细节
- 数据: 价格K线。
- 入场条件:
- 多头: 价格突破最后枢轴高点。
- 空头: 价格跌破最后枢轴低点。
- 出场条件: 止损或止盈。
- 止损: 最近摆动点加缓冲。
- 默认参数:
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()