Volume-Weighted Supertrend Strategy
该策略基于成交量加权均线与 ATR 生成超级趋势,并对成交量应用第二个超级趋势以确认方向。当价格和成交量趋势同时向上时开多,在条件反转时平仓。
参数
- ATR Period – 价格趋势的 ATR 周期。
- Volume Period – VWAP 与成交量趋势的周期。
- Factor – ATR 倍数。
- Candle Type – 使用的蜡烛类型。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volume-weighted supertrend strategy combining price and volume trend signals.
/// Uses StdDev-based supertrend on price and a volume supertrend.
/// </summary>
public class VolumeWeightedSupertrendStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _factor;
private readonly StrategyParam<DataType> _candleType;
private decimal? _prevUpperBand;
private decimal? _prevLowerBand;
private decimal? _prevSupertrend;
private int _prevDirection = 1;
private decimal? _prevClose;
private readonly List<decimal> _volumes = new();
private decimal? _prevVolUpperBand;
private decimal? _prevVolLowerBand;
private decimal? _prevVolSupertrend;
private int _prevVolDirection = 1;
private decimal? _prevVolume;
public int Period { get => _period.Value; set => _period.Value = value; }
public decimal Factor { get => _factor.Value; set => _factor.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VolumeWeightedSupertrendStrategy()
{
_period = Param(nameof(Period), 10)
.SetGreaterThanZero()
.SetDisplay("Period", "Supertrend period", "General");
_factor = Param(nameof(Factor), 3m)
.SetGreaterThanZero()
.SetDisplay("Factor", "Multiplier", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_prevUpperBand = null;
_prevLowerBand = null;
_prevSupertrend = null;
_prevDirection = 1;
_prevClose = null;
_volumes.Clear();
_prevVolUpperBand = null;
_prevVolLowerBand = null;
_prevVolSupertrend = null;
_prevVolDirection = 1;
_prevVolume = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var stdDev = new StandardDeviation { Length = Period };
var sma = new SimpleMovingAverage { Length = Period };
_prevUpperBand = null;
_prevLowerBand = null;
_prevSupertrend = null;
_prevDirection = 1;
_prevClose = null;
_volumes.Clear();
_prevVolUpperBand = null;
_prevVolLowerBand = null;
_prevVolSupertrend = null;
_prevVolDirection = 1;
_prevVolume = null;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(stdDev, sma, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal stdVal, decimal smaVal)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var hl2 = (candle.HighPrice + candle.LowPrice) / 2m;
var volume = candle.TotalVolume;
_volumes.Add(volume);
while (_volumes.Count > Period + 1)
_volumes.RemoveAt(0);
var volumeWindow = _volumes.ToArray();
if (stdVal <= 0 || volumeWindow.Length < Period)
{
_prevClose = close;
_prevVolume = volume;
return;
}
// Price supertrend
var upperBand = hl2 + Factor * stdVal;
var lowerBand = hl2 - Factor * stdVal;
if (_prevLowerBand != null)
lowerBand = lowerBand > _prevLowerBand.Value || (_prevClose.HasValue && _prevClose.Value < _prevLowerBand.Value) ? lowerBand : _prevLowerBand.Value;
if (_prevUpperBand != null)
upperBand = upperBand < _prevUpperBand.Value || (_prevClose.HasValue && _prevClose.Value > _prevUpperBand.Value) ? upperBand : _prevUpperBand.Value;
int direction;
if (_prevSupertrend == _prevUpperBand)
direction = close > upperBand ? -1 : 1;
else
direction = close < lowerBand ? 1 : -1;
var supertrend = direction == -1 ? lowerBand : upperBand;
var supertrendUpStart = direction == -1 && _prevDirection == 1;
var supertrendDnStart = direction == 1 && _prevDirection == -1;
var inRisingTrend = supertrend < close;
// Volume supertrend (manual ATR-like calc on volume)
var trVolume = _prevVolume.HasValue ? Math.Abs(volume - _prevVolume.Value) : 0m;
var volAvg = volumeWindow.Average();
var volStd = 0m;
if (volumeWindow.Length > 1)
{
var sumSq = volumeWindow.Sum(v => (v - volAvg) * (v - volAvg));
volStd = (decimal)Math.Sqrt((double)(sumSq / volumeWindow.Length));
}
var volUpperBand = volume + Factor * volStd;
var volLowerBand = volume - Factor * volStd;
if (_prevVolLowerBand != null)
volLowerBand = volLowerBand > _prevVolLowerBand.Value || (_prevVolume.HasValue && _prevVolume.Value < _prevVolLowerBand.Value) ? volLowerBand : _prevVolLowerBand.Value;
if (_prevVolUpperBand != null)
volUpperBand = volUpperBand < _prevVolUpperBand.Value || (_prevVolume.HasValue && _prevVolume.Value > _prevVolUpperBand.Value) ? volUpperBand : _prevVolUpperBand.Value;
int volDirection;
if (_prevVolSupertrend == _prevVolUpperBand)
volDirection = volume > volUpperBand ? -1 : 1;
else
volDirection = volume < volLowerBand ? 1 : -1;
var volumeSupertrend = volDirection == -1 ? volLowerBand : volUpperBand;
var volumeChangeUp = _prevVolDirection == 1 && volDirection == -1;
var volumeChangeDn = _prevVolDirection == -1 && volDirection == 1;
var inRisingVolume = volumeSupertrend < volume;
var buy = (inRisingVolume && supertrendUpStart) || (volumeChangeUp && inRisingTrend);
var sell = (!inRisingVolume && supertrendDnStart) || (volumeChangeDn && !inRisingTrend);
if (buy && Position <= 0)
BuyMarket();
else if (sell && Position >= 0)
SellMarket();
_prevUpperBand = upperBand;
_prevLowerBand = lowerBand;
_prevSupertrend = supertrend;
_prevDirection = direction;
_prevVolUpperBand = volUpperBand;
_prevVolLowerBand = volLowerBand;
_prevVolSupertrend = volumeSupertrend;
_prevVolDirection = volDirection;
_prevClose = close;
_prevVolume = volume;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import StandardDeviation, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class volume_weighted_supertrend_strategy(Strategy):
"""Volume-weighted supertrend: price StdDev-based supertrend combined with volume supertrend."""
def __init__(self):
super(volume_weighted_supertrend_strategy, self).__init__()
self._period = self.Param("Period", 10).SetGreaterThanZero().SetDisplay("Period", "Supertrend period", "General")
self._factor = self.Param("Factor", 3).SetGreaterThanZero().SetDisplay("Factor", "Multiplier", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(volume_weighted_supertrend_strategy, self).OnReseted()
self._prev_upper = None
self._prev_lower = None
self._prev_st = None
self._prev_dir = 1
self._prev_close = None
self._volumes = []
self._prev_vol_upper = None
self._prev_vol_lower = None
self._prev_vol_st = None
self._prev_vol_dir = 1
self._prev_volume = None
def OnStarted2(self, time):
super(volume_weighted_supertrend_strategy, self).OnStarted2(time)
self._prev_upper = None
self._prev_lower = None
self._prev_st = None
self._prev_dir = 1
self._prev_close = None
self._volumes = []
self._prev_vol_upper = None
self._prev_vol_lower = None
self._prev_vol_st = None
self._prev_vol_dir = 1
self._prev_volume = None
std = StandardDeviation()
std.Length = self._period.Value
sma = SimpleMovingAverage()
sma.Length = self._period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(std, sma, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, std_val, sma_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
hl2 = (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
volume = float(candle.TotalVolume)
sv = float(std_val)
period = self._period.Value
factor = self._factor.Value
self._volumes.append(volume)
while len(self._volumes) > period + 1:
self._volumes.pop(0)
if sv <= 0 or len(self._volumes) < period:
self._prev_close = close
self._prev_volume = volume
return
# Price supertrend
upper = hl2 + factor * sv
lower = hl2 - factor * sv
if self._prev_lower is not None:
lower = lower if (lower > self._prev_lower or (self._prev_close is not None and self._prev_close < self._prev_lower)) else self._prev_lower
if self._prev_upper is not None:
upper = upper if (upper < self._prev_upper or (self._prev_close is not None and self._prev_close > self._prev_upper)) else self._prev_upper
if self._prev_st == self._prev_upper:
direction = -1 if close > upper else 1
else:
direction = 1 if close < lower else -1
supertrend = lower if direction == -1 else upper
st_up_start = direction == -1 and self._prev_dir == 1
st_dn_start = direction == 1 and self._prev_dir == -1
in_rising = supertrend < close
# Volume supertrend
vol_avg = sum(self._volumes) / len(self._volumes)
vol_std = 0
if len(self._volumes) > 1:
sum_sq = sum((v - vol_avg) ** 2 for v in self._volumes)
vol_std = math.sqrt(sum_sq / len(self._volumes))
vol_upper = volume + factor * vol_std
vol_lower = volume - factor * vol_std
if self._prev_vol_lower is not None:
vol_lower = vol_lower if (vol_lower > self._prev_vol_lower or (self._prev_volume is not None and self._prev_volume < self._prev_vol_lower)) else self._prev_vol_lower
if self._prev_vol_upper is not None:
vol_upper = vol_upper if (vol_upper < self._prev_vol_upper or (self._prev_volume is not None and self._prev_volume > self._prev_vol_upper)) else self._prev_vol_upper
if self._prev_vol_st == self._prev_vol_upper:
vol_dir = -1 if volume > vol_upper else 1
else:
vol_dir = 1 if volume < vol_lower else -1
vol_st = vol_lower if vol_dir == -1 else vol_upper
vol_change_up = self._prev_vol_dir == 1 and vol_dir == -1
vol_change_dn = self._prev_vol_dir == -1 and vol_dir == 1
in_rising_vol = vol_st < volume
buy = (in_rising_vol and st_up_start) or (vol_change_up and in_rising)
sell = (not in_rising_vol and st_dn_start) or (vol_change_dn and not in_rising)
if buy and self.Position <= 0:
self.BuyMarket()
elif sell and self.Position >= 0:
self.SellMarket()
self._prev_upper = upper
self._prev_lower = lower
self._prev_st = supertrend
self._prev_dir = direction
self._prev_vol_upper = vol_upper
self._prev_vol_lower = vol_lower
self._prev_vol_st = vol_st
self._prev_vol_dir = vol_dir
self._prev_close = close
self._prev_volume = volume
def CreateClone(self):
return volume_weighted_supertrend_strategy()