PercentXトレンドフォロー戦略
TrendoscopeによるPercentX Trend Followerを基にした戦略です。
この戦略は、選択したバンド(KeltnerまたはBollinger)からの価格距離を正規化し、このオシレーターが動的な極値レンジをクロスした際に取引します。ストップの配置にはATRを使用します。
詳細
- エントリー条件: オシレーターが上部レンジを上抜けでロング、下部レンジを下抜けでショート。
- ロング/ショート: 両方。
- エグジット条件: ATRベースのストップ。
- ストップ: 初期ATRストップ。
- デフォルト値:
BandType= KeltnerMaLength= 40LoopbackPeriod= 80OuterLoopback= 80UseInitialStop= trueAtrLength= 14TrendMultiplier= 1ReverseMultiplier= 3CandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: トレンド
- 方向: 両方
- インジケーター: BollingerBands, KeltnerChannels, ATR, Highest, Lowest
- ストップ: ATR
- 複雑さ: 中級
- 時間軸: イントラデイ (5m)
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
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>
/// PercentX trend follower.
/// The oscillator measures the distance of the close from the middle of a Keltner or Bollinger band (MaLength, width TrendMultiplier)
/// in percent of the half band width. Its highest and lowest values over LoopbackPeriod form the extremes, and the dynamic range is
/// the lowest of those highs and the highest of those lows over OuterLoopback. Crossing above the upper range goes long, crossing below
/// the lower range goes short, reversing an opposite position. With UseInitialStop an ATR stop of ReverseMultiplier * ATR is placed
/// from the entry close.
/// </summary>
public class PercentXTrendFollowerStrategy : Strategy
{
/// <summary>
/// Band used to normalize price.
/// </summary>
public enum BandTypes
{
/// <summary>
/// Keltner channel: EMA middle, ATR width.
/// </summary>
Keltner,
/// <summary>
/// Bollinger bands: SMA middle, standard deviation width.
/// </summary>
Bollinger
}
private readonly StrategyParam<BandTypes> _bandType;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _loopbackPeriod;
private readonly StrategyParam<int> _outerLoopback;
private readonly StrategyParam<bool> _useInitialStop;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _trendMultiplier;
private readonly StrategyParam<decimal> _reverseMultiplier;
private readonly StrategyParam<DataType> _candleType;
private Highest _oscHighest = null!;
private Lowest _oscLowest = null!;
private Lowest _upperRange = null!;
private Highest _lowerRange = null!;
private decimal? _prevOsc;
private decimal? _prevUpper;
private decimal? _prevLower;
private decimal? _stopPrice;
/// <summary>
/// Band used to normalize price.
/// </summary>
public BandTypes BandType
{
get => _bandType.Value;
set => _bandType.Value = value;
}
/// <summary>
/// Period of the band middle line and width.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Bars for the oscillator highest and lowest values.
/// </summary>
public int LoopbackPeriod
{
get => _loopbackPeriod.Value;
set => _loopbackPeriod.Value = value;
}
/// <summary>
/// Bars for the dynamic range built from the extremes.
/// </summary>
public int OuterLoopback
{
get => _outerLoopback.Value;
set => _outerLoopback.Value = value;
}
/// <summary>
/// Use the initial ATR stop.
/// </summary>
public bool UseInitialStop
{
get => _useInitialStop.Value;
set => _useInitialStop.Value = value;
}
/// <summary>
/// ATR period for the stop.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// Band width multiplier.
/// </summary>
public decimal TrendMultiplier
{
get => _trendMultiplier.Value;
set => _trendMultiplier.Value = value;
}
/// <summary>
/// ATR multiplier of the initial stop.
/// </summary>
public decimal ReverseMultiplier
{
get => _reverseMultiplier.Value;
set => _reverseMultiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public PercentXTrendFollowerStrategy()
{
_bandType = Param(nameof(BandType), BandTypes.Keltner)
.SetDisplay("Band Type", "Band used to normalize price", "Indicators");
_maLength = Param(nameof(MaLength), 40)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Period of the band middle line and width", "Indicators");
_loopbackPeriod = Param(nameof(LoopbackPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Loopback Period", "Bars for the oscillator extremes", "Indicators");
_outerLoopback = Param(nameof(OuterLoopback), 80)
.SetGreaterThanZero()
.SetDisplay("Outer Loopback", "Bars for the dynamic range", "Indicators");
_useInitialStop = Param(nameof(UseInitialStop), true)
.SetDisplay("Use Initial Stop", "Use the initial ATR stop", "Risk");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period for the stop", "Risk");
_trendMultiplier = Param(nameof(TrendMultiplier), 1m)
.SetGreaterThanZero()
.SetDisplay("Trend Multiplier", "Band width multiplier", "Indicators");
_reverseMultiplier = Param(nameof(ReverseMultiplier), 3m)
.SetGreaterThanZero()
.SetDisplay("Reverse Multiplier", "ATR multiplier of the initial stop", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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()
{
_prevOsc = null;
_prevUpper = null;
_prevLower = null;
_stopPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
IIndicator middle;
IIndicator width;
if (BandType == BandTypes.Keltner)
{
middle = new ExponentialMovingAverage { Length = MaLength };
width = new AverageTrueRange { Length = MaLength };
}
else
{
middle = new SimpleMovingAverage { Length = MaLength };
width = new StandardDeviation { Length = MaLength };
}
var atr = new AverageTrueRange { Length = AtrLength };
_oscHighest = new Highest { Length = LoopbackPeriod };
_oscLowest = new Lowest { Length = LoopbackPeriod };
_upperRange = new Lowest { Length = OuterLoopback };
_lowerRange = new Highest { Length = OuterLoopback };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(middle, width, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, middle);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal middle, decimal width, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
var halfWidth = width * TrendMultiplier;
if (halfWidth <= 0)
return;
var close = candle.ClosePrice;
var osc = (close - middle) / halfWidth * 100m;
var highestValue = _oscHighest.Process(new DecimalIndicatorValue(_oscHighest, osc, candle.OpenTime) { IsFinal = true });
var lowestValue = _oscLowest.Process(new DecimalIndicatorValue(_oscLowest, osc, candle.OpenTime) { IsFinal = true });
if (!_oscHighest.IsFormed || !_oscLowest.IsFormed)
return;
var upperValue = _upperRange.Process(new DecimalIndicatorValue(_upperRange, highestValue.ToDecimal(), candle.OpenTime) { IsFinal = true });
var lowerValue = _lowerRange.Process(new DecimalIndicatorValue(_lowerRange, lowestValue.ToDecimal(), candle.OpenTime) { IsFinal = true });
if (!_upperRange.IsFormed || !_lowerRange.IsFormed)
return;
var upper = upperValue.ToDecimal();
var lower = lowerValue.ToDecimal();
var prevOsc = _prevOsc;
var prevUpper = _prevUpper;
var prevLower = _prevLower;
_prevOsc = osc;
_prevUpper = upper;
_prevLower = lower;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_stopPrice is decimal stop)
{
if (Position > 0 && candle.LowPrice <= stop)
{
SellMarket(Position);
_stopPrice = null;
return;
}
if (Position < 0 && candle.HighPrice >= stop)
{
BuyMarket(-Position);
_stopPrice = null;
return;
}
}
if (prevOsc is not decimal po || prevUpper is not decimal pu || prevLower is not decimal pl)
return;
if (po <= pu && osc > upper && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = UseInitialStop ? close - atr * ReverseMultiplier : null;
}
else if (po >= pl && osc < lower && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = UseInitialStop ? close + atr * ReverseMultiplier : null;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage, AverageTrueRange, StandardDeviation, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class percent_x_trend_follower_strategy(Strategy):
"""
PercentX trend follower.
The oscillator measures the distance of the close from the middle of a Keltner or Bollinger band (MaLength, width TrendMultiplier)
in percent of the half band width. Its highest and lowest values over LoopbackPeriod form the extremes, and the dynamic range is
the lowest of those highs and the highest of those lows over OuterLoopback. Crossing above the upper range goes long, crossing below
the lower range goes short, reversing an opposite position. With UseInitialStop an ATR stop of ReverseMultiplier * ATR is placed
from the entry close.
"""
def __init__(self):
super(percent_x_trend_follower_strategy, self).__init__()
self._band_type = self.Param("BandType", "Keltner").SetDisplay("Band Type", "Band used to normalize price", "Indicators")
self._ma_length = self.Param("MaLength", 40).SetGreaterThanZero().SetDisplay("MA Length", "Period of the band middle line and width", "Indicators")
self._loopback_period = self.Param("LoopbackPeriod", 80).SetGreaterThanZero().SetDisplay("Loopback Period", "Bars for the oscillator extremes", "Indicators")
self._outer_loopback = self.Param("OuterLoopback", 80).SetGreaterThanZero().SetDisplay("Outer Loopback", "Bars for the dynamic range", "Indicators")
self._use_initial_stop = self.Param("UseInitialStop", True).SetDisplay("Use Initial Stop", "Use the initial ATR stop", "Risk")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period for the stop", "Risk")
self._trend_multiplier = self.Param("TrendMultiplier", 1.0).SetGreaterThanZero().SetDisplay("Trend Multiplier", "Band width multiplier", "Indicators")
self._reverse_multiplier = self.Param("ReverseMultiplier", 3.0).SetGreaterThanZero().SetDisplay("Reverse Multiplier", "ATR multiplier of the initial stop", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).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._prev_osc = None
self._prev_upper = None
self._prev_lower = None
self._stop_price = None
def OnReseted(self):
super(percent_x_trend_follower_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(percent_x_trend_follower_strategy, self).OnStarted2(time)
self._reset_state()
ma_length = self._ma_length.Value
if str(self._band_type.Value) == "Keltner":
middle = ExponentialMovingAverage()
width = AverageTrueRange()
else:
middle = SimpleMovingAverage()
width = StandardDeviation()
middle.Length = ma_length
width.Length = ma_length
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
self._osc_highest = Highest()
self._osc_highest.Length = self._loopback_period.Value
self._osc_lowest = Lowest()
self._osc_lowest.Length = self._loopback_period.Value
self._upper_range = Lowest()
self._upper_range.Length = self._outer_loopback.Value
self._lower_range = Highest()
self._lower_range.Length = self._outer_loopback.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(middle, width, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, middle)
self.DrawOwnTrades(area)
def _process_candle(self, candle, middle, width, atr):
if candle.State != CandleStates.Finished:
return
half_width = width * Decimal(self._trend_multiplier.Value)
if half_width <= 0:
return
close = candle.ClosePrice
osc = (close - middle) / half_width * Decimal(100)
highest_value = process_float(self._osc_highest, osc, candle.OpenTime, True)
lowest_value = process_float(self._osc_lowest, osc, candle.OpenTime, True)
if not self._osc_highest.IsFormed or not self._osc_lowest.IsFormed:
return
upper_value = process_float(self._upper_range, highest_value.GetValue[Decimal](None), candle.OpenTime, True)
lower_value = process_float(self._lower_range, lowest_value.GetValue[Decimal](None), candle.OpenTime, True)
if not self._upper_range.IsFormed or not self._lower_range.IsFormed:
return
upper = upper_value.GetValue[Decimal](None)
lower = lower_value.GetValue[Decimal](None)
prev_osc = self._prev_osc
prev_upper = self._prev_upper
prev_lower = self._prev_lower
self._prev_osc = osc
self._prev_upper = upper
self._prev_lower = lower
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._stop_price is not None:
if self.Position > 0 and candle.LowPrice <= self._stop_price:
self.SellMarket(self.Position)
self._stop_price = None
return
if self.Position < 0 and candle.HighPrice >= self._stop_price:
self.BuyMarket(-self.Position)
self._stop_price = None
return
if prev_osc is None or prev_upper is None or prev_lower is None:
return
reverse_mult = Decimal(self._reverse_multiplier.Value)
if prev_osc <= prev_upper and osc > upper and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - atr * reverse_mult if self._use_initial_stop.Value else None
elif prev_osc >= prev_lower and osc < lower and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + atr * reverse_mult if self._use_initial_stop.Value else None
def CreateClone(self):
return percent_x_trend_follower_strategy()