Powertrend - Volumen-Bereichsfilter-Strategie
Diese Strategie verwendet einen volumenangepassten Bereichsfilter, um dynamische Preisbänder zu definieren. Long-Einstiege erfolgen, wenn der Preis das obere Band während eines Aufwärtstrends durchbricht, und Positionen werden geschlossen, wenn der Preis unter das untere Band fällt. Optionale Filter umfassen ADX, Trendbestätigung durch Hochs/Tiefs und eine VWMA des Filterwerts.
Berechnung und Ausführung
C# und Python verwenden diese ausdrückliche kausale Definition. Auf abgeschlossenen Kerzen gelten d = abs(Close - vorheriger Close) und V = TotalVolume. Nach RangePeriod Änderungen wird im Fenster einschließlich der aktuellen Kerze r = RangeMultiplier * sum(d * V) / sum(V) berechnet; das Gesamtvolumen muss positiv sein. Der erste gebildete Filter f ist der aktuelle Schlusskurs. Danach wird das vorherige f auf [Close - r, Close + r] begrenzt. Entscheidungsbänder sind vorheriges f + r und vorheriges f - r, bevor sich das Zentrum ändert. Dadurch wird das Band nach der Filterbewegung nicht auf den Ausbruchspreis selbst gesetzt.
Nur aus einer Nullposition mit Volume einsteigen, wenn Close > oberes Band und der Filter steigt. Bei Close < unteres Band wird der gesamte Long per Marktorder geschlossen, unabhängig von Einstiegsfiltern. Gleichheit hält die Position. Ausstehende Orders sperren zusätzliche Orders. Keine Shorts, EMA-Kreuzungen oder Wartefrist.
Optionaler ADX muss gebildet sein und strikt über AdxThreshold liegen; deaktiviert wird er nicht registriert und verlangt keine Aufwärmung. Hoch-/Tiefbestätigung verlangt gleichzeitig aktuelles High > vorheriges High und aktuelles Low > vorheriges Low. Filterwert-VWMA ist sum(f * V) / sum(V) der letzten VwmaPeriod gebildeten Werte einschließlich des aktuellen. Erforderlich sind ein volles Fenster, positives Gesamtvolumen und Close > VWMA. Deaktivierte Filter stellen keine Aufwärmungsanforderung. Entscheidungen erfolgen zum Kerzenschluss per Marktorder, ohne Börsen-Stoporders oder angenommene Intrabar-Ausführung an Hochs/Tiefs.
Standardwerte: RangePeriod=40, RangeMultiplier=1.5, AdxPeriod=14, AdxThreshold=20, VwmaPeriod=20, CandleType=TimeSpan.FromMinutes(5).TimeFrame(). IsAdxFilterEnabled, IsHighLowFilterEnabled, IsVwmaFilterEnabled sind false. Perioden und Multiplikator positiv; ADX-Schwelle in [0, 100]. Diese Formel und Werte konkretisieren das Beispiel; die ursprüngliche Kurzbeschreibung enthielt keine Originalindikatorformel.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Long-only volume-weighted range breakouts with optional ADX, high/low and filter-value VWMA confirmation.
/// </summary>
public class PowertrendVolumeRangeFilterStrategy : Strategy
{
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<decimal> _rangeMultiplier;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<int> _vwmaPeriod;
private readonly StrategyParam<bool> _isAdxFilterEnabled;
private readonly StrategyParam<bool> _isHighLowFilterEnabled;
private readonly StrategyParam<bool> _isVwmaFilterEnabled;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<(decimal delta, decimal volume)> _changes = [];
private readonly Queue<(decimal price, decimal volume)> _filtered = [];
private decimal? _previousClose;
private decimal? _previousHigh;
private decimal? _previousLow;
private decimal? _filter;
private decimal _weightedRange;
private decimal _rangeVolume;
private decimal _weightedFilter;
private decimal _filterVolume;
private Order _pendingOrder;
/// <summary>Initializes a 40-change range multiplied by 1.5, disabled optional filters and five-minute candles.</summary>
public PowertrendVolumeRangeFilterStrategy()
{
_rangePeriod = Param(nameof(RangePeriod), 40).SetGreaterThanZero()
.SetDisplay("Range Period", "Volume-weighted absolute close changes", "Range");
_rangeMultiplier = Param(nameof(RangeMultiplier), 1.5m).SetGreaterThanZero()
.SetDisplay("Range Multiplier", "Multiplier of the volume-weighted price-change range", "Range");
_adxPeriod = Param(nameof(AdxPeriod), 14).SetGreaterThanZero()
.SetDisplay("ADX Period", "Native directional-index period", "Filters");
_adxThreshold = Param(nameof(AdxThreshold), 20m).SetRange(0m, 100m)
.SetDisplay("ADX Threshold", "Strict lower bound on formed ADX for entries", "Filters");
_vwmaPeriod = Param(nameof(VwmaPeriod), 20).SetGreaterThanZero()
.SetDisplay("VWMA Period", "Volume-weighted average of formed filter values", "Filters");
_isAdxFilterEnabled = Param(nameof(IsAdxFilterEnabled), false)
.SetDisplay("ADX Enabled", "Require formed ADX above its threshold", "Filters");
_isHighLowFilterEnabled = Param(nameof(IsHighLowFilterEnabled), false)
.SetDisplay("High/Low Enabled", "Require both a higher high and a higher low", "Filters");
_isVwmaFilterEnabled = Param(nameof(IsVwmaFilterEnabled), false)
.SetDisplay("VWMA Enabled", "Require close above the formed filter-value VWMA", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Finished candles supplying price and true total volume", "General");
OrderRegistering += order => _pendingOrder = order;
}
/// <summary>Number of close changes used to form a volume-weighted range.</summary>
public int RangePeriod { get => _rangePeriod.Value; set => _rangePeriod.Value = value; }
/// <summary>Positive multiplier applied to the weighted range.</summary>
public decimal RangeMultiplier { get => _rangeMultiplier.Value; set => _rangeMultiplier.Value = value; }
/// <summary>Native ADX length.</summary>
public int AdxPeriod { get => _adxPeriod.Value; set => _adxPeriod.Value = value; }
/// <summary>Strict ADX entry threshold in [0, 100].</summary>
public decimal AdxThreshold { get => _adxThreshold.Value; set => _adxThreshold.Value = value; }
/// <summary>Number of formed filter values in the optional VWMA.</summary>
public int VwmaPeriod { get => _vwmaPeriod.Value; set => _vwmaPeriod.Value = value; }
/// <summary>Require a formed native ADX above the threshold for entry.</summary>
public bool IsAdxFilterEnabled { get => _isAdxFilterEnabled.Value; set => _isAdxFilterEnabled.Value = value; }
/// <summary>Require current high and low to exceed the previous candle's high and low.</summary>
public bool IsHighLowFilterEnabled { get => _isHighLowFilterEnabled.Value; set => _isHighLowFilterEnabled.Value = value; }
/// <summary>Require current close above the formed filter-value VWMA.</summary>
public bool IsVwmaFilterEnabled { get => _isVwmaFilterEnabled.Value; set => _isVwmaFilterEnabled.Value = value; }
/// <summary>Base candle type; five minutes by default.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
private void ResetState()
{
_changes.Clear();
_filtered.Clear();
_previousClose = _previousHigh = _previousLow = _filter = null;
_weightedRange = _rangeVolume = _weightedFilter = _filterVolume = 0m;
_pendingOrder = null;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (RangePeriod <= 0 || RangeMultiplier <= 0m || AdxPeriod <= 0 || AdxThreshold < 0m
|| AdxThreshold > 100m || VwmaPeriod <= 0)
throw new ArgumentException("Range/ADX/VWMA periods and multiplier must be positive; ADX threshold must be within [0, 100].");
ResetState();
var adx = new AverageDirectionalIndex { Length = AdxPeriod };
var subscription = SubscribeCandles(CandleType);
if (IsAdxFilterEnabled)
subscription.BindEx([adx], ProcessCandle, allowEmpty: true).Start();
else
subscription.Bind(candle => ProcessCandle(candle, [])).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
if (IsAdxFilterEnabled)
DrawIndicator(area, adx);
DrawOwnTrades(area);
}
}
private void Submit(Sides side, decimal volume)
=> RegisterOrder(new Order { Security = Security, Portfolio = Portfolio, Type = OrderTypes.Market, Side = side, Volume = volume });
private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
{
if (candle.State != CandleStates.Finished)
return;
var higherHighLow = _previousHigh is decimal high && _previousLow is decimal low
&& candle.HighPrice > high && candle.LowPrice > low;
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
if (_previousClose is decimal previous)
{
var delta = Math.Abs(candle.ClosePrice - previous);
_changes.Enqueue((delta, candle.TotalVolume));
_weightedRange += delta * candle.TotalVolume;
_rangeVolume += candle.TotalVolume;
if (_changes.Count > RangePeriod)
{
var removed = _changes.Dequeue();
_weightedRange -= removed.delta * removed.volume;
_rangeVolume -= removed.volume;
}
}
_previousClose = candle.ClosePrice;
if (_changes.Count < RangePeriod || _rangeVolume <= 0m)
return;
var width = RangeMultiplier * _weightedRange / _rangeVolume;
var prior = _filter;
if (prior is null)
_filter = candle.ClosePrice;
else if (candle.ClosePrice > prior + width)
_filter = candle.ClosePrice - width;
else if (candle.ClosePrice < prior - width)
_filter = candle.ClosePrice + width;
_filtered.Enqueue((_filter.Value, candle.TotalVolume));
_weightedFilter += _filter.Value * candle.TotalVolume;
_filterVolume += candle.TotalVolume;
if (_filtered.Count > VwmaPeriod)
{
var removed = _filtered.Dequeue();
_weightedFilter -= removed.price * removed.volume;
_filterVolume -= removed.volume;
}
if (prior is not decimal center || !IsFormedAndOnlineAndAllowTrading()
|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.ClosePrice < center - width)
{
Submit(Sides.Sell, Position);
return;
}
if (Position != 0m || width <= 0m || candle.ClosePrice <= center + width || _filter <= center
|| IsHighLowFilterEnabled && !higherHighLow
|| IsVwmaFilterEnabled && (_filtered.Count < VwmaPeriod || _filterVolume <= 0m || candle.ClosePrice <= _weightedFilter / _filterVolume))
return;
if (IsAdxFilterEnabled && (!values[0].IsFormed
|| ((IAverageDirectionalIndexValue)values[0]).MovingAverage is not decimal strength || strength <= AdxThreshold))
return;
Submit(Sides.Buy, Volume);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from collections import deque
from System import Array, ArgumentException, Decimal, TimeSpan
from StockSharp.Messages import CandleStates, DataType, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order
from StockSharp.Algo.Indicators import AverageDirectionalIndex, IIndicator
from StockSharp.Algo.Strategies import Strategy
class powertrend_volume_range_filter_strategy(Strategy):
def __init__(self):
super(powertrend_volume_range_filter_strategy, self).__init__()
self._range_period = self.Param("RangePeriod", 40).SetGreaterThanZero() \
.SetDisplay("Range Period", "Volume-weighted absolute close changes", "Range")
self._range_multiplier = self.Param("RangeMultiplier", Decimal.Parse("1.5")).SetGreaterThanZero() \
.SetDisplay("Range Multiplier", "Multiplier of the volume-weighted price-change range", "Range")
self._adx_period = self.Param("AdxPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ADX Period", "Native directional-index period", "Filters")
self._adx_threshold = self.Param("AdxThreshold", Decimal(20)).SetRange(Decimal.Zero, Decimal(100)) \
.SetDisplay("ADX Threshold", "Strict lower bound on formed ADX for entries", "Filters")
self._vwma_period = self.Param("VwmaPeriod", 20).SetGreaterThanZero() \
.SetDisplay("VWMA Period", "Volume-weighted average of formed filter values", "Filters")
self._is_adx_filter_enabled = self.Param("IsAdxFilterEnabled", False) \
.SetDisplay("ADX Enabled", "Require formed ADX above its threshold", "Filters")
self._is_high_low_filter_enabled = self.Param("IsHighLowFilterEnabled", False) \
.SetDisplay("High/Low Enabled", "Require both a higher high and a higher low", "Filters")
self._is_vwma_filter_enabled = self.Param("IsVwmaFilterEnabled", False) \
.SetDisplay("VWMA Enabled", "Require close above the formed filter-value VWMA", "Filters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying price and true total volume", "General")
self._reset_state()
self.OrderRegistering += self._observe_order
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._changes = deque()
self._filtered = deque()
self._previous_close = self._previous_high = self._previous_low = self._filter = None
self._weighted_range = self._range_volume = self._weighted_filter = self._filter_volume = Decimal.Zero
self._pending_order = None
def OnReseted(self):
super(powertrend_volume_range_filter_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(powertrend_volume_range_filter_strategy, self).OnStarted2(time)
if self._range_period.Value <= 0 or self._range_multiplier.Value <= 0 or self._adx_period.Value <= 0 \
or self._adx_threshold.Value < 0 or self._adx_threshold.Value > 100 or self._vwma_period.Value <= 0:
raise ArgumentException("Range/ADX/VWMA periods and multiplier must be positive; ADX threshold must be within [0, 100].")
self._reset_state()
subscription = self.SubscribeCandles(self.candle_type)
if self._is_adx_filter_enabled.Value:
adx = AverageDirectionalIndex()
adx.Length = self._adx_period.Value
subscription.BindEx(Array[IIndicator]([adx]), self.OnProcess, True).Start()
else:
subscription.Bind(self._without_adx).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _without_adx(self, candle):
self.OnProcess(candle, [])
def _submit(self, side, volume):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
self.RegisterOrder(order)
def OnProcess(self, candle, values):
if candle.State != CandleStates.Finished:
return
higher_high_low = self._previous_high is not None and self._previous_low is not None \
and candle.HighPrice > self._previous_high and candle.LowPrice > self._previous_low
self._previous_high = candle.HighPrice
self._previous_low = candle.LowPrice
if self._previous_close is not None:
delta = abs(candle.ClosePrice - self._previous_close)
self._changes.append((delta, candle.TotalVolume))
self._weighted_range += delta * candle.TotalVolume
self._range_volume += candle.TotalVolume
if len(self._changes) > self._range_period.Value:
removed = self._changes.popleft()
self._weighted_range -= removed[0] * removed[1]
self._range_volume -= removed[1]
self._previous_close = candle.ClosePrice
if len(self._changes) < self._range_period.Value or self._range_volume <= 0:
return
width = self._range_multiplier.Value * self._weighted_range / self._range_volume
prior = self._filter
if prior is None:
self._filter = candle.ClosePrice
elif candle.ClosePrice > prior + width:
self._filter = candle.ClosePrice - width
elif candle.ClosePrice < prior - width:
self._filter = candle.ClosePrice + width
self._filtered.append((self._filter, candle.TotalVolume))
self._weighted_filter += self._filter * candle.TotalVolume
self._filter_volume += candle.TotalVolume
if len(self._filtered) > self._vwma_period.Value:
removed = self._filtered.popleft()
self._weighted_filter -= removed[0] * removed[1]
self._filter_volume -= removed[1]
pending = self._pending_order
if prior is None or not self.IsFormedAndOnlineAndAllowTrading() \
or pending is not None and pending.State != OrderStates.Done and pending.State != OrderStates.Failed:
return
if self.Position > 0 and candle.ClosePrice < prior - width:
self._submit(Sides.Sell, self.Position)
return
if self.Position != 0 or width <= 0 or candle.ClosePrice <= prior + width or self._filter <= prior \
or self._is_high_low_filter_enabled.Value and not higher_high_low \
or self._is_vwma_filter_enabled.Value and (len(self._filtered) < self._vwma_period.Value \
or self._filter_volume <= 0 or candle.ClosePrice <= self._weighted_filter / self._filter_volume):
return
if self._is_adx_filter_enabled.Value:
value = values[0]
strength = value.MovingAverage
if not value.IsFormed or strength is None or strength <= self._adx_threshold.Value:
return
self._submit(Sides.Buy, self.Volume)
def CreateClone(self):
return powertrend_volume_range_filter_strategy()