量能与波动率比率指标 WODI 策略
基于 TradingView 脚本 "Volume and Volatility Ratio Indicator - WODI" 的简化策略。策略监控成交量与价格波动的乘积,当波动率指数超过动态阈值且最近的K线出现方向变化时,按斐波那契系数设置止损和止盈并开仓。
详情
- 入场:高成交量、强波动并伴随K线反转形态。
- 出场:根据K线范围和斐波那契倍数计算止损与止盈。
- 多空:双向。
- 时间框架:任意。
- 指标:SMA。
此策略为教学用途的简化版本,已精简原始 TradingView 逻辑。
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 and volatility ratio indicator strategy (WODI).
/// Detects increased volume and volatility to enter reversal trades.
/// Uses volume MA and volatility index with short/long MA crossover.
/// </summary>
public class VolumeAndVolatilityRatioIndicatorWodiStrategy : Strategy
{
private readonly StrategyParam<int> _volLength;
private readonly StrategyParam<int> _indexLength;
private readonly StrategyParam<decimal> _stopPct;
private readonly StrategyParam<decimal> _tpPct;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = new();
private readonly List<decimal> _volIndices = new();
private decimal _entryPrice;
private decimal _stopDist;
private ICandleMessage _prevCandle;
private ICandleMessage _prevPrevCandle;
private int _cooldownRemaining;
public int VolLength { get => _volLength.Value; set => _volLength.Value = value; }
public int IndexLength { get => _indexLength.Value; set => _indexLength.Value = value; }
public decimal StopPct { get => _stopPct.Value; set => _stopPct.Value = value; }
public decimal TpPct { get => _tpPct.Value; set => _tpPct.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VolumeAndVolatilityRatioIndicatorWodiStrategy()
{
_volLength = Param(nameof(VolLength), 20)
.SetGreaterThanZero()
.SetDisplay("Volume MA Length", "Volume average period", "Parameters");
_indexLength = Param(nameof(IndexLength), 20)
.SetGreaterThanZero()
.SetDisplay("Index Length", "Volatility index average period", "Parameters");
_stopPct = Param(nameof(StopPct), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Stop %", "Stop loss percent", "Risk");
_tpPct = Param(nameof(TpPct), 1m)
.SetGreaterThanZero()
.SetDisplay("TP %", "Take profit percent", "Risk");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 24)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_volumes.Clear();
_volIndices.Clear();
_entryPrice = 0;
_stopDist = 0;
_prevCandle = null;
_prevPrevCandle = null;
_cooldownRemaining = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = 2 };
_volumes.Clear();
_volIndices.Clear();
_entryPrice = 0;
_stopDist = 0;
_prevCandle = null;
_prevPrevCandle = null;
_cooldownRemaining = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(sma, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal _dummy)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var vol = candle.TotalVolume;
var volatility = candle.ClosePrice > 0 ? (candle.HighPrice - candle.LowPrice) / candle.ClosePrice * 100m : 0;
var volIndex = vol * volatility;
_volumes.Add(vol);
_volIndices.Add(volIndex);
while (_volumes.Count > VolLength + 1)
_volumes.RemoveAt(0);
while (_volIndices.Count > IndexLength + 1)
_volIndices.RemoveAt(0);
// TP/SL management
if (Position > 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice <= _entryPrice - _stopDist || candle.ClosePrice >= _entryPrice + _stopDist * (TpPct / StopPct))
{
SellMarket();
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = SignalCooldownBars;
}
}
else if (Position < 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice >= _entryPrice + _stopDist || candle.ClosePrice <= _entryPrice - _stopDist * (TpPct / StopPct))
{
BuyMarket();
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = SignalCooldownBars;
}
}
if (_volumes.Count < VolLength || _volIndices.Count < IndexLength || _prevCandle == null || _prevPrevCandle == null)
{
_prevPrevCandle = _prevCandle;
_prevCandle = candle;
return;
}
// Calculate averages
var volAvg = _volumes.Take(VolLength).Sum() / VolLength;
var indexAvg = _volIndices.Take(IndexLength).Sum() / IndexLength;
// Entry conditions
var highVol = vol > volAvg;
var highVolIndex = volIndex > indexAvg * 2.5m;
var isLongPattern = highVol && highVolIndex
&& _prevCandle.ClosePrice < _prevPrevCandle.ClosePrice
&& candle.ClosePrice > _prevCandle.ClosePrice;
var isShortPattern = highVol && highVolIndex
&& _prevCandle.ClosePrice > _prevPrevCandle.ClosePrice
&& candle.ClosePrice < _prevCandle.ClosePrice;
if (_cooldownRemaining == 0 && isLongPattern && Position == 0)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && isShortPattern && Position == 0)
{
SellMarket();
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
_prevPrevCandle = _prevCandle;
_prevCandle = candle;
}
}
import clr
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 SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class volume_and_volatility_ratio_indicator_wodi_strategy(Strategy):
def __init__(self):
super(volume_and_volatility_ratio_indicator_wodi_strategy, self).__init__()
self._vol_length = self.Param("VolLength", 20) \
.SetDisplay("Volume MA Length", "Volume average period", "Parameters")
self._index_length = self.Param("IndexLength", 20) \
.SetDisplay("Index Length", "Volatility index average period", "Parameters")
self._stop_pct = self.Param("StopPct", 0.5) \
.SetDisplay("Stop %", "Stop loss percent", "Risk")
self._tp_pct = self.Param("TpPct", 1) \
.SetDisplay("TP %", "Take profit percent", "Risk")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 24) \
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._volumes = []
self._vol_indices = []
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_candle = None
self._prev_prev_candle = None
self._cooldown_remaining = 0
@property
def vol_length(self):
return self._vol_length.Value
@property
def index_length(self):
return self._index_length.Value
@property
def stop_pct(self):
return self._stop_pct.Value
@property
def tp_pct(self):
return self._tp_pct.Value
@property
def signal_cooldown_bars(self):
return self._signal_cooldown_bars.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(volume_and_volatility_ratio_indicator_wodi_strategy, self).OnReseted()
self._volumes = []
self._vol_indices = []
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_candle = None
self._prev_prev_candle = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(volume_and_volatility_ratio_indicator_wodi_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = 2
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle, _dummy):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
close = float(candle.ClosePrice)
vol = float(candle.TotalVolume)
volatility = (float(candle.HighPrice) - float(candle.LowPrice)) / close * 100.0 if close > 0 else 0.0
vol_index = vol * volatility
self._volumes.append(vol)
self._vol_indices.append(vol_index)
while len(self._volumes) > self.vol_length + 1:
self._volumes.pop(0)
while len(self._vol_indices) > self.index_length + 1:
self._vol_indices.pop(0)
# TP/SL management
stop_pct = float(self.stop_pct)
tp_pct = float(self.tp_pct)
if self.Position > 0 and self._entry_price > 0 and self._stop_dist > 0:
if close <= self._entry_price - self._stop_dist or close >= self._entry_price + self._stop_dist * (tp_pct / stop_pct):
self.SellMarket()
self._entry_price = 0
self._stop_dist = 0
self._cooldown_remaining = self.signal_cooldown_bars
elif self.Position < 0 and self._entry_price > 0 and self._stop_dist > 0:
if close >= self._entry_price + self._stop_dist or close <= self._entry_price - self._stop_dist * (tp_pct / stop_pct):
self.BuyMarket()
self._entry_price = 0
self._stop_dist = 0
self._cooldown_remaining = self.signal_cooldown_bars
if len(self._volumes) < self.vol_length or len(self._vol_indices) < self.index_length or self._prev_candle is None or self._prev_prev_candle is None:
self._prev_prev_candle = self._prev_candle
self._prev_candle = candle
return
# Calculate averages
vol_avg = sum(self._volumes[:self.vol_length]) / self.vol_length
index_avg = sum(self._vol_indices[:self.index_length]) / self.index_length
# Entry conditions
high_vol = vol > vol_avg
high_vol_index = vol_index > index_avg * 2.5
is_long_pattern = high_vol and high_vol_index \
and float(self._prev_candle.ClosePrice) < float(self._prev_prev_candle.ClosePrice) \
and close > float(self._prev_candle.ClosePrice)
is_short_pattern = high_vol and high_vol_index \
and float(self._prev_candle.ClosePrice) > float(self._prev_prev_candle.ClosePrice) \
and close < float(self._prev_candle.ClosePrice)
if self._cooldown_remaining == 0 and is_long_pattern and self.Position == 0:
self.BuyMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
elif self._cooldown_remaining == 0 and is_short_pattern and self.Position == 0:
self.SellMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
self._prev_prev_candle = self._prev_candle
self._prev_candle = candle
def CreateClone(self):
return volume_and_volatility_ratio_indicator_wodi_strategy()