Fourier平滑化ボリュームゾーンオシレーター WFSVZ0戦略
Fourier平滑化されたVolume Zone Oscillatorを使用した戦略です。オシレーターが閾値を上回ると買い、負の閾値を下回ると売りエントリーします。シグナルがない場合にオプションでポジションをクローズします。
詳細
- エントリー条件: オシレーターが閾値を上回る / 負の閾値を下回る。
- ロング/ショート: 両方。
- エグジット条件: 反対のシグナルまたはオプションの全決済。
- ストップ: なし。
- デフォルト値:
VzoLength= 2SmoothLength= 2Threshold= 0mCloseAllPositions= trueCandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: 出来高
- 方向: 両方
- インジケーター: Volume Zone Oscillator
- ストップ: なし
- 複雑さ: 中級
- 時間軸: イントラデイ (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>
/// Fourier Smoothed Volume Zone Oscillator strategy.
/// The Volume Zone Oscillator is 100 times the EMA of volume signed by the close-to-close direction divided by the EMA of volume,
/// both over VzoLength candles. It is smoothed by rebuilding its latest value from the mean and the fundamental harmonic of a
/// discrete Fourier transform over the last SmoothLength values. Above Threshold the strategy is long, below -Threshold short,
/// reversing an opposite position; between them the position is closed when CloseAllPositions is set.
/// </summary>
public class FourierSmoothedVzoStrategy : Strategy
{
private readonly StrategyParam<int> _vzoLength;
private readonly StrategyParam<int> _smoothLength;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<bool> _closeAllPositions;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _signedVolumeEma;
private ExponentialMovingAverage _volumeEma;
private readonly List<decimal> _vzoValues = [];
private decimal? _prevClose;
/// <summary>
/// EMA length of the oscillator.
/// </summary>
public int VzoLength
{
get => _vzoLength.Value;
set => _vzoLength.Value = value;
}
/// <summary>
/// Window of the Fourier smoothing.
/// </summary>
public int SmoothLength
{
get => _smoothLength.Value;
set => _smoothLength.Value = value;
}
/// <summary>
/// Oscillator level for signals; its negative is the short level.
/// </summary>
public decimal Threshold
{
get => _threshold.Value;
set => _threshold.Value = value;
}
/// <summary>
/// Close the position when there is no signal.
/// </summary>
public bool CloseAllPositions
{
get => _closeAllPositions.Value;
set => _closeAllPositions.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public FourierSmoothedVzoStrategy()
{
_vzoLength = Param(nameof(VzoLength), 2)
.SetGreaterThanZero()
.SetDisplay("VZO Length", "EMA length of the oscillator", "Indicators");
_smoothLength = Param(nameof(SmoothLength), 2)
.SetGreaterThanZero()
.SetDisplay("Smooth Length", "Window of the Fourier smoothing", "Indicators");
_threshold = Param(nameof(Threshold), 0m)
.SetNotNegative()
.SetDisplay("Threshold", "Oscillator level for signals", "Signals");
_closeAllPositions = Param(nameof(CloseAllPositions), true)
.SetDisplay("Close All Positions", "Close the position when there is no signal", "Signals");
_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();
_signedVolumeEma = null;
_volumeEma = null;
_vzoValues.Clear();
_prevClose = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_signedVolumeEma = new ExponentialMovingAverage { Length = VzoLength };
_volumeEma = new ExponentialMovingAverage { Length = VzoLength };
_vzoValues.Clear();
_prevClose = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var prevClose = _prevClose;
_prevClose = candle.ClosePrice;
if (prevClose is not decimal lastClose)
return;
var volume = candle.TotalVolume;
var direction = Math.Sign(candle.ClosePrice - lastClose);
var signedEma = _signedVolumeEma.Process(direction * volume, candle.OpenTime, true);
var volumeEma = _volumeEma.Process(volume, candle.OpenTime, true);
if (!_signedVolumeEma.IsFormed || !_volumeEma.IsFormed)
return;
var total = volumeEma.ToDecimal();
if (total == 0)
return;
_vzoValues.Add(100m * signedEma.ToDecimal() / total);
if (_vzoValues.Count > SmoothLength)
_vzoValues.RemoveAt(0);
if (_vzoValues.Count < SmoothLength)
return;
var oscillator = FourierSmooth(_vzoValues);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (oscillator > Threshold)
{
if (Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
}
else if (oscillator < -Threshold)
{
if (Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
else if (CloseAllPositions)
{
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(-Position);
}
}
private static decimal FourierSmooth(List<decimal> values)
{
var n = values.Count;
var mean = 0.0;
var cosSum = 0.0;
var sinSum = 0.0;
for (var i = 0; i < n; i++)
{
var x = (double)values[i];
var angle = 2 * Math.PI * i / n;
mean += x;
cosSum += x * Math.Cos(angle);
sinSum += x * Math.Sin(angle);
}
mean /= n;
if (n < 2)
return (decimal)mean;
// The Nyquist harmonic of an even window has no sine part and counts once, not twice.
var scale = n == 2 ? 1.0 / n : 2.0 / n;
var last = 2 * Math.PI * (n - 1) / n;
return (decimal)(mean + scale * (cosSum * Math.Cos(last) + sinSum * Math.Sin(last)));
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class fourier_smoothed_vzo_strategy(Strategy):
"""
Fourier Smoothed Volume Zone Oscillator strategy.
The Volume Zone Oscillator is 100 times the EMA of volume signed by the close-to-close direction divided by the EMA of volume,
both over VzoLength candles. It is smoothed by rebuilding its latest value from the mean and the fundamental harmonic of a
discrete Fourier transform over the last SmoothLength values. Above Threshold the strategy is long, below -Threshold short,
reversing an opposite position; between them the position is closed when CloseAllPositions is set.
"""
def __init__(self):
super(fourier_smoothed_vzo_strategy, self).__init__()
self._vzo_length = self.Param("VzoLength", 2).SetGreaterThanZero().SetDisplay("VZO Length", "EMA length of the oscillator", "Indicators")
self._smooth_length = self.Param("SmoothLength", 2).SetGreaterThanZero().SetDisplay("Smooth Length", "Window of the Fourier smoothing", "Indicators")
self._threshold = self.Param("Threshold", 0.0).SetNotNegative().SetDisplay("Threshold", "Oscillator level for signals", "Signals")
self._close_all_positions = self.Param("CloseAllPositions", True).SetDisplay("Close All Positions", "Close the position when there is no signal", "Signals")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._signed_volume_ema = None
self._volume_ema = None
self._vzo_values = []
self._prev_close = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(fourier_smoothed_vzo_strategy, self).OnReseted()
self._signed_volume_ema = None
self._volume_ema = None
self._vzo_values = []
self._prev_close = None
def OnStarted2(self, time):
super(fourier_smoothed_vzo_strategy, self).OnStarted2(time)
self._signed_volume_ema = ExponentialMovingAverage()
self._signed_volume_ema.Length = self._vzo_length.Value
self._volume_ema = ExponentialMovingAverage()
self._volume_ema.Length = self._vzo_length.Value
self._vzo_values = []
self._prev_close = None
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
last_close = self._prev_close
self._prev_close = close
if last_close is None:
return
volume = float(candle.TotalVolume)
direction = (close > last_close) - (close < last_close)
signed_ema = process_float(self._signed_volume_ema, Decimal(direction * volume), candle.OpenTime, True)
volume_ema = process_float(self._volume_ema, Decimal(volume), candle.OpenTime, True)
if not self._signed_volume_ema.IsFormed or not self._volume_ema.IsFormed:
return
total = float(volume_ema)
if total == 0:
return
smooth_length = self._smooth_length.Value
self._vzo_values.append(100.0 * float(signed_ema) / total)
if len(self._vzo_values) > smooth_length:
del self._vzo_values[0]
if len(self._vzo_values) < smooth_length:
return
oscillator = self._fourier_smooth(self._vzo_values)
if not self.IsFormedAndOnlineAndAllowTrading():
return
threshold = float(self._threshold.Value)
if oscillator > threshold:
if self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif oscillator < -threshold:
if self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self._close_all_positions.Value:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(-self.Position)
def _fourier_smooth(self, values):
n = len(values)
mean = 0.0
cos_sum = 0.0
sin_sum = 0.0
for i, x in enumerate(values):
angle = 2.0 * math.pi * i / n
mean += x
cos_sum += x * math.cos(angle)
sin_sum += x * math.sin(angle)
mean /= n
if n < 2:
return mean
# The Nyquist harmonic of an even window has no sine part and counts once, not twice.
scale = 1.0 / n if n == 2 else 2.0 / n
last = 2.0 * math.pi * (n - 1) / n
return mean + scale * (cos_sum * math.cos(last) + sin_sum * math.sin(last))
def CreateClone(self):
return fourier_smoothed_vzo_strategy()