枢轴点 SuperTrend 趋势过滤 策略
该策略将基于枢轴点的 SuperTrend 与另一个 SuperTrend 趋势过滤器及移动平均线结合使用。当趋势翻转或枢轴 SuperTrend 在指定日期范围内发出信号时进行交易。
细节
- 入场条件:
- 多头:价格 SuperTrend 转为上行且收盘价高于收盘价 SMA,或枢轴 SuperTrend 在日期窗口内转为上行。
- 空头:价格 SuperTrend 转为下行且收盘价低于 SMA,或枢轴 SuperTrend 在日期窗口内转为下行。
- 出场条件:
- 任一下行切换全额平多仓,任一上行切换全额平空仓,不受 SMA 或日期限制。
- 只有反向入场条件满足时才反转。两个指标同时发出相反信号时只平仓,不新开仓。
- 止损:无
- 默认值:
PivotPeriod= 2Factor= 3AtrPeriod= 10TrendAtrPeriod= 10TrendMultiplier= 3MaPeriod= 20CandleType= 5 分钟StartDate/EndDate= UTCDateTime.MinValue/DateTime.MaxValue
- 过滤器:
- 类别:Trend
- 方向:双向
- 指标:Pivot, SuperTrend, SMA
- 止损:无
- 复杂度:中等
- 时间框架:任意
- 季节性:可选
- 神经网络:否
- 背离:否
- 风险等级:中等
计算与执行
仅使用已完成的蜡烛。枢轴高点/低点必须严格高于/低于左右各 PivotPeriod 根蜡烛,且右侧蜡烛全部完成后才确认。如果同一根蜡烛同时符合两个条件,高点优先。首个枢轴初始化 center,后续按 (2 * center + pivot) / 3 更新。
枢轴带使用 Wilder ATR:前 AtrPeriod 个真实波幅取平均,此后以 1 / AtrPeriod 权重平滑。基础支撑/阻力为 center ± Factor * ATR。前收盘价高于前支撑时,支撑取基础值与前值的最大值,否则重置;前收盘价低于前阻力时,阻力取最小值,否则重置。收盘价严格高于前阻力则转为上行,严格低于前支撑则转为下行,否则保留方向。初始方向为上行,初始化不产生信号。
独立价格过滤器使用 StockSharp 原生 SuperTrend(TrendAtrPeriod、TrendMultiplier)。SMA 包含当前收盘价。只有方向切换产生信号,两个 ATR、SMA 和枢轴中心都必须就绪。StartDate <= candle.OpenTime <= EndDate 只限制枢轴入场。日期必须为有序 UTC 值。入场目标仓位为 Volume,反转订单量为 Volume + abs(Position),平仓量为 abs(Position)。没有冷却时间或保护止损。
枢轴概念来源:LonesomeTheBlue 的 Pivot Point Supertrend。上述规则定义本示例的双向模型。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Confirmed pivot SuperTrend signals combined with a price SuperTrend and SMA entry filter.
/// </summary>
public class PivotPointSuperTrendTrendFilterStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _pivotPeriod;
private readonly StrategyParam<decimal> _factor;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _trendAtrPeriod;
private readonly StrategyParam<decimal> _trendMultiplier;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DateTime> _startDate;
private readonly StrategyParam<DateTime> _endDate;
private readonly List<(decimal High, decimal Low)> _pivots = [];
private decimal? _center;
private decimal? _support;
private decimal? _resistance;
private decimal? _previousClose;
private bool? _pivotUp;
private bool? _filterUp;
/// <summary>
/// Initializes the published indicator periods and an unrestricted UTC date window.
/// </summary>
public PivotPointSuperTrendTrendFilterStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of finished candles", "General");
_pivotPeriod = Param(nameof(PivotPeriod), 2).SetGreaterThanZero()
.SetDisplay("Pivot Period", "Finished candles on each side of a confirmed pivot", "Indicators");
_factor = Param(nameof(Factor), 3m).SetGreaterThanZero()
.SetDisplay("Factor", "ATR width of the pivot bands", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 10).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR period of the pivot bands", "Indicators");
_trendAtrPeriod = Param(nameof(TrendAtrPeriod), 10).SetGreaterThanZero()
.SetDisplay("Trend ATR Period", "Wilder ATR period of the price SuperTrend", "Indicators");
_trendMultiplier = Param(nameof(TrendMultiplier), 3m).SetGreaterThanZero()
.SetDisplay("Trend Multiplier", "ATR width of the price SuperTrend", "Indicators");
_maPeriod = Param(nameof(MaPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Close-price SMA period for trend-filter entries", "Indicators");
_startDate = Param(nameof(StartDate), DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc))
.SetDisplay("Start Date", "Inclusive UTC candle-open time for pivot entries", "General");
_endDate = Param(nameof(EndDate), DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc))
.SetDisplay("End Date", "Inclusive UTC candle-open time for pivot entries", "General");
}
/// <summary>
/// Candle type; five minutes by default.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>
/// Number of finished candles required on each side of a pivot.
/// </summary>
public int PivotPeriod { get => _pivotPeriod.Value; set => _pivotPeriod.Value = value; }
/// <summary>
/// ATR multiplier for the pivot bands.
/// </summary>
public decimal Factor { get => _factor.Value; set => _factor.Value = value; }
/// <summary>
/// Wilder ATR period for the pivot bands.
/// </summary>
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
/// <summary>
/// ATR period of the price SuperTrend filter.
/// </summary>
public int TrendAtrPeriod { get => _trendAtrPeriod.Value; set => _trendAtrPeriod.Value = value; }
/// <summary>
/// ATR multiplier of the price SuperTrend filter.
/// </summary>
public decimal TrendMultiplier { get => _trendMultiplier.Value; set => _trendMultiplier.Value = value; }
/// <summary>
/// Close-price SMA period for trend-filter entries.
/// </summary>
public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
/// <summary>
/// Inclusive lower UTC candle-open time for pivot entries.
/// </summary>
public DateTime StartDate { get => _startDate.Value; set => _startDate.Value = value; }
/// <summary>
/// Inclusive upper UTC candle-open time for pivot entries.
/// </summary>
public DateTime EndDate { get => _endDate.Value; set => _endDate.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetSignals();
}
private void ResetSignals()
{
_pivots.Clear();
_center = _support = _resistance = _previousClose = null;
_pivotUp = _filterUp = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (StartDate.Kind != DateTimeKind.Utc || EndDate.Kind != DateTimeKind.Utc || StartDate > EndDate)
throw new ArgumentException("The pivot entry window requires ordered UTC dates.");
ResetSignals();
var atr = new AverageTrueRange { Length = AtrPeriod };
var trend = new SuperTrend { Length = TrendAtrPeriod, Multiplier = TrendMultiplier };
var average = new SimpleMovingAverage { Length = MaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(atr, trend, average, ProcessCandle, allowEmpty: true).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, trend);
DrawIndicator(area, average);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue, IIndicatorValue trendValue, IIndicatorValue averageValue)
{
if (candle.State != CandleStates.Finished)
return;
var previousClose = _previousClose ?? candle.ClosePrice;
_previousClose = candle.ClosePrice;
_pivots.Add((candle.HighPrice, candle.LowPrice));
if (_pivots.Count > 2 * PivotPeriod + 1)
_pivots.RemoveAt(0);
if (_pivots.Count == 2 * PivotPeriod + 1)
{
var candidate = _pivots[PivotPeriod];
var isHigh = true;
var isLow = true;
for (var i = 0; i < _pivots.Count; i++)
{
if (i == PivotPeriod)
continue;
isHigh &= candidate.High > _pivots[i].High;
isLow &= candidate.Low < _pivots[i].Low;
}
if (isHigh || isLow)
{
var pivot = isHigh ? candidate.High : candidate.Low;
_center = _center is decimal center ? (2m * center + pivot) / 3m : pivot;
}
}
var previousPivotUp = _pivotUp;
if (_center is decimal pivotCenter && atrValue.IsFormed && !atrValue.IsEmpty)
{
var width = Factor * atrValue.ToDecimal();
var oldSupport = _support;
var oldResistance = _resistance;
_support = oldSupport is decimal support && previousClose > support ? Math.Max(pivotCenter - width, support) : pivotCenter - width;
_resistance = oldResistance is decimal resistance && previousClose < resistance ? Math.Min(pivotCenter + width, resistance) : pivotCenter + width;
_pivotUp = oldResistance is decimal upper && candle.ClosePrice > upper ? true
: oldSupport is decimal lower && candle.ClosePrice < lower ? false : _pivotUp ?? true;
}
var previousFilterUp = _filterUp;
if (trendValue is SuperTrendIndicatorValue { IsFormed: true, IsEmpty: false } filter)
_filterUp = filter.IsUpTrend;
if (_pivotUp is null || _filterUp is null || !averageValue.IsFormed || averageValue.IsEmpty || !IsFormedAndOnlineAndAllowTrading())
return;
var pivotBuy = previousPivotUp == false && _pivotUp == true;
var pivotSell = previousPivotUp == true && _pivotUp == false;
var filterBuy = previousFilterUp == false && _filterUp == true;
var filterSell = previousFilterUp == true && _filterUp == false;
var withinDates = candle.OpenTime >= StartDate && candle.OpenTime <= EndDate;
var upSignal = pivotBuy || filterBuy;
var downSignal = pivotSell || filterSell;
var averagePrice = averageValue.ToDecimal();
var enterLong = !downSignal && (filterBuy && candle.ClosePrice > averagePrice || pivotBuy && withinDates);
var enterShort = !upSignal && (filterSell && candle.ClosePrice < averagePrice || pivotSell && withinDates);
if (Position > 0m && downSignal)
SellMarket(Position + (enterShort ? Volume : 0m));
else if (Position < 0m && upSignal)
BuyMarket(-Position + (enterLong ? Volume : 0m));
else if (Position == 0m && enterLong)
BuyMarket(Volume);
else if (Position == 0m && enterShort)
SellMarket(Volume);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import ArgumentException, DateTime, DateTimeKind, Decimal, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage, SuperTrend
from StockSharp.Algo.Strategies import Strategy
class pivot_point_super_trend_trend_filter_strategy(Strategy):
def __init__(self):
super(pivot_point_super_trend_trend_filter_strategy, self).__init__()
self._pivot_period = self.Param("PivotPeriod", 2).SetGreaterThanZero() \
.SetDisplay("Pivot Period", "Finished candles on each side of a confirmed pivot", "Indicators")
self._factor = self.Param("Factor", Decimal.Parse("3")).SetGreaterThanZero() \
.SetDisplay("Factor", "ATR width of the pivot bands", "Indicators")
self._atr_period = self.Param("AtrPeriod", 10).SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR period of the pivot bands", "Indicators")
self._trend_atr_period = self.Param("TrendAtrPeriod", 10).SetGreaterThanZero() \
.SetDisplay("Trend ATR Period", "Wilder ATR period of the price SuperTrend", "Indicators")
self._trend_multiplier = self.Param("TrendMultiplier", Decimal.Parse("3")).SetGreaterThanZero() \
.SetDisplay("Trend Multiplier", "ATR width of the price SuperTrend", "Indicators")
self._ma_period = self.Param("MaPeriod", 20).SetGreaterThanZero() \
.SetDisplay("MA Period", "Close-price SMA period for trend-filter entries", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of finished candles", "General")
self._start_date = self.Param("StartDate", DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc)) \
.SetDisplay("Start Date", "Inclusive UTC candle-open time for pivot entries", "General")
self._end_date = self.Param("EndDate", DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) \
.SetDisplay("End Date", "Inclusive UTC candle-open time for pivot entries", "General")
self._reset_signals()
@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_signals(self):
self._pivots = []
self._center = None
self._support = None
self._resistance = None
self._previous_close = None
self._pivot_up = None
self._filter_up = None
def OnReseted(self):
super(pivot_point_super_trend_trend_filter_strategy, self).OnReseted()
self._reset_signals()
def OnStarted2(self, time):
super(pivot_point_super_trend_trend_filter_strategy, self).OnStarted2(time)
start = self._start_date.Value
end = self._end_date.Value
if start.Kind != DateTimeKind.Utc or end.Kind != DateTimeKind.Utc or start > end:
raise ArgumentException("The pivot entry window requires ordered UTC dates.")
self._reset_signals()
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
trend = SuperTrend()
trend.Length = self._trend_atr_period.Value
trend.Multiplier = self._trend_multiplier.Value
average = SimpleMovingAverage()
average.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, trend, average, self.OnProcess, True).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, trend)
self.DrawIndicator(area, average)
self.DrawOwnTrades(area)
def OnProcess(self, candle, atr_value, trend_value, average_value):
if candle.State != CandleStates.Finished:
return
previous_close = candle.ClosePrice if self._previous_close is None else self._previous_close
self._previous_close = candle.ClosePrice
self._pivots.append((candle.HighPrice, candle.LowPrice))
period = self._pivot_period.Value
if len(self._pivots) > 2 * period + 1:
self._pivots.pop(0)
if len(self._pivots) == 2 * period + 1:
candidate = self._pivots[period]
neighbors = self._pivots[:period] + self._pivots[period + 1:]
is_high = all(candidate[0] > bar[0] for bar in neighbors)
is_low = all(candidate[1] < bar[1] for bar in neighbors)
if is_high or is_low:
pivot = candidate[0] if is_high else candidate[1]
self._center = pivot if self._center is None else (Decimal(2) * self._center + pivot) / Decimal(3)
previous_pivot_up = self._pivot_up
if self._center is not None and atr_value.IsFormed and not atr_value.IsEmpty:
width = self._factor.Value * atr_value.GetValue[Decimal](None)
old_support = self._support
old_resistance = self._resistance
basic_support = self._center - width
basic_resistance = self._center + width
self._support = max(basic_support, old_support) if old_support is not None and previous_close > old_support else basic_support
self._resistance = min(basic_resistance, old_resistance) if old_resistance is not None and previous_close < old_resistance else basic_resistance
if old_resistance is not None and candle.ClosePrice > old_resistance:
self._pivot_up = True
elif old_support is not None and candle.ClosePrice < old_support:
self._pivot_up = False
elif self._pivot_up is None:
self._pivot_up = True
previous_filter_up = self._filter_up
if trend_value.IsFormed and not trend_value.IsEmpty:
self._filter_up = bool(trend_value.IsUpTrend)
if self._pivot_up is None or self._filter_up is None or not average_value.IsFormed \
or average_value.IsEmpty or not self.IsFormedAndOnlineAndAllowTrading():
return
pivot_buy = previous_pivot_up is False and self._pivot_up
pivot_sell = previous_pivot_up is True and not self._pivot_up
filter_buy = previous_filter_up is False and self._filter_up
filter_sell = previous_filter_up is True and not self._filter_up
within_dates = self._start_date.Value <= candle.OpenTime <= self._end_date.Value
up_signal = pivot_buy or filter_buy
down_signal = pivot_sell or filter_sell
average_price = average_value.GetValue[Decimal](None)
enter_long = not down_signal and (filter_buy and candle.ClosePrice > average_price or pivot_buy and within_dates)
enter_short = not up_signal and (filter_sell and candle.ClosePrice < average_price or pivot_sell and within_dates)
if self.Position > 0 and down_signal:
self.SellMarket(self.Position + (self.Volume if enter_short else Decimal.Zero))
elif self.Position < 0 and up_signal:
self.BuyMarket(-self.Position + (self.Volume if enter_long else Decimal.Zero))
elif self.Position == 0 and enter_long:
self.BuyMarket(self.Volume)
elif self.Position == 0 and enter_short:
self.SellMarket(self.Volume)
def CreateClone(self):
return pivot_point_super_trend_trend_filter_strategy()