NVIを用いたチャネル戦略
この戦略はボリンジャーバンドまたはケルトナーチャネルをネガティブボリュームインデックス(NVI)と組み合わせています。価格が下限バンドを下回って終値を形成し、NVIがそのEMAを上回っている場合にロングポジションを建てます。NVIがそのEMAを下回るとポジションを決済します。オプションのストップロスおよびテイクプロフィットのパーセンテージも設定できます。
詳細
- エントリー条件:
- ロング: 終値 < 下限バンド かつ NVI > NVI EMA。
- ロング/ショート: ロングのみ。
- エグジット条件:
- ロング: NVI < NVI EMA。
- ストップ: オプション、エントリー価格のパーセンテージ。
- デフォルト値:
ChannelType= "BB"ChannelLength= 20ChannelMultiplier= 2NviEmaLength= 200EnableStopLoss= falseStopLossPercent= 0EnableTakeProfit= falseTakeProfitPercent= 0
- フィルター:
- カテゴリ: チャネル
- 方向: ロングのみ
- インジケーター: Bollinger Bands または Keltner Channels, EMA, NVI
- ストップ: オプション
- 複雑さ: 低
- 時間軸: 任意
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
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>
/// Channels with NVI strategy.
/// The channel is a Bollinger band (ChannelType "BB") or a Keltner channel (ChannelType "KC": EMA plus and minus ChannelMultiplier
/// ATRs) of ChannelLength. The Negative Volume Index starts at 1000 and moves with the close only on candles whose volume is below the
/// previous one. Long only: buys when the close is below the lower channel line and NVI is above EMA(NviEmaLength) of NVI, and closes the
/// long when NVI falls below that EMA. Optional percent stop loss and take profit protect the position.
/// </summary>
public class ChannelsWithNviStrategy : Strategy
{
private readonly StrategyParam<string> _channelType;
private readonly StrategyParam<int> _channelLength;
private readonly StrategyParam<decimal> _channelMultiplier;
private readonly StrategyParam<int> _nviEmaLength;
private readonly StrategyParam<bool> _enableStopLoss;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<bool> _enableTakeProfit;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _nviEma;
private decimal _nvi;
private decimal? _prevClose;
private decimal? _prevVolume;
/// <summary>
/// Channel kind: "BB" for Bollinger Bands, "KC" for Keltner Channels.
/// </summary>
public string ChannelType
{
get => _channelType.Value;
set => _channelType.Value = value;
}
/// <summary>
/// Channel period.
/// </summary>
public int ChannelLength
{
get => _channelLength.Value;
set => _channelLength.Value = value;
}
/// <summary>
/// Channel width multiplier.
/// </summary>
public decimal ChannelMultiplier
{
get => _channelMultiplier.Value;
set => _channelMultiplier.Value = value;
}
/// <summary>
/// EMA period of NVI.
/// </summary>
public int NviEmaLength
{
get => _nviEmaLength.Value;
set => _nviEmaLength.Value = value;
}
/// <summary>
/// Enable the stop loss.
/// </summary>
public bool EnableStopLoss
{
get => _enableStopLoss.Value;
set => _enableStopLoss.Value = value;
}
/// <summary>
/// Stop loss percentage from entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Enable the take profit.
/// </summary>
public bool EnableTakeProfit
{
get => _enableTakeProfit.Value;
set => _enableTakeProfit.Value = value;
}
/// <summary>
/// Take profit percentage from entry price.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ChannelsWithNviStrategy()
{
_channelType = Param(nameof(ChannelType), "BB")
.SetDisplay("Channel Type", "Channel kind: BB for Bollinger Bands, KC for Keltner Channels", "Channel");
_channelLength = Param(nameof(ChannelLength), 20)
.SetGreaterThanZero()
.SetDisplay("Channel Length", "Channel period", "Channel");
_channelMultiplier = Param(nameof(ChannelMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Channel Multiplier", "Channel width multiplier", "Channel");
_nviEmaLength = Param(nameof(NviEmaLength), 200)
.SetGreaterThanZero()
.SetDisplay("NVI EMA Length", "EMA period of NVI", "NVI");
_enableStopLoss = Param(nameof(EnableStopLoss), false)
.SetDisplay("Enable Stop Loss", "Enable the stop loss", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_enableTakeProfit = Param(nameof(EnableTakeProfit), false)
.SetDisplay("Enable Take Profit", "Enable the take profit", "Risk");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 0m)
.SetNotNegative()
.SetDisplay("Take Profit %", "Take profit percentage from entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).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();
_nviEma = null;
_nvi = 1000m;
_prevClose = null;
_prevVolume = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_nvi = 1000m;
_prevClose = null;
_prevVolume = null;
_nviEma = new ExponentialMovingAverage { Length = NviEmaLength };
var bollinger = new BollingerBands { Length = ChannelLength, Width = ChannelMultiplier };
var ema = new ExponentialMovingAverage { Length = ChannelLength };
var atr = new AverageTrueRange { Length = ChannelLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(bollinger, ema, atr, ProcessCandle)
.Start();
var takeProfit = EnableTakeProfit && TakeProfitPercent > 0 ? new Unit(TakeProfitPercent, UnitTypes.Percent) : new Unit();
var stopLoss = EnableStopLoss && StopLossPercent > 0 ? new Unit(StopLossPercent, UnitTypes.Percent) : new Unit();
StartProtection(takeProfit, stopLoss, useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, bollinger);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerValue, IIndicatorValue emaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var volume = candle.TotalVolume;
if (_prevClose is decimal pc && _prevVolume is decimal pv && pc != 0 && volume < pv)
_nvi += _nvi * (close - pc) / pc;
_prevClose = close;
_prevVolume = volume;
var nviEmaValue = _nviEma.Process(new DecimalIndicatorValue(_nviEma, _nvi, candle.OpenTime) { IsFinal = true });
if (!nviEmaValue.IsFormed)
return;
decimal lower;
if (ChannelType.EqualsIgnoreCase("KC"))
{
if (!emaValue.IsFormed || !atrValue.IsFormed)
return;
lower = emaValue.GetValue<decimal>() - ChannelMultiplier * atrValue.GetValue<decimal>();
}
else
{
if (!bollingerValue.IsFormed || ((BollingerBandsValue)bollingerValue).LowBand is not decimal low)
return;
lower = low;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var nviEma = nviEmaValue.GetValue<decimal>();
if (Position > 0)
{
if (_nvi < nviEma)
SellMarket(Position);
}
else if (Position == 0 && close < lower && _nvi > nviEma)
{
BuyMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import BollingerBands, ExponentialMovingAverage, AverageTrueRange, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class channels_with_nvi_strategy(Strategy):
"""
Channels with NVI strategy.
The channel is a Bollinger band (ChannelType "BB") or a Keltner channel (ChannelType "KC": EMA plus and minus ChannelMultiplier
ATRs) of ChannelLength. The Negative Volume Index starts at 1000 and moves with the close only on candles whose volume is below the
previous one. Long only: buys when the close is below the lower channel line and NVI is above EMA(NviEmaLength) of NVI, and closes the
long when NVI falls below that EMA. Optional percent stop loss and take profit protect the position.
"""
def __init__(self):
super(channels_with_nvi_strategy, self).__init__()
self._channel_type = self.Param("ChannelType", "BB").SetDisplay("Channel Type", "Channel kind: BB for Bollinger Bands, KC for Keltner Channels", "Channel")
self._channel_length = self.Param("ChannelLength", 20).SetGreaterThanZero().SetDisplay("Channel Length", "Channel period", "Channel")
self._channel_multiplier = self.Param("ChannelMultiplier", 2.0).SetGreaterThanZero().SetDisplay("Channel Multiplier", "Channel width multiplier", "Channel")
self._nvi_ema_length = self.Param("NviEmaLength", 200).SetGreaterThanZero().SetDisplay("NVI EMA Length", "EMA period of NVI", "NVI")
self._enable_stop_loss = self.Param("EnableStopLoss", False).SetDisplay("Enable Stop Loss", "Enable the stop loss", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 0.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._enable_take_profit = self.Param("EnableTakeProfit", False).SetDisplay("Enable Take Profit", "Enable the take profit", "Risk")
self._take_profit_percent = self.Param("TakeProfitPercent", 0.0).SetNotNegative().SetDisplay("Take Profit %", "Take profit percentage from entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._nvi_ema = None
self._nvi = Decimal(1000)
self._prev_close = None
self._prev_volume = None
def OnReseted(self):
super(channels_with_nvi_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(channels_with_nvi_strategy, self).OnStarted2(time)
self._reset_state()
self._nvi_ema = ExponentialMovingAverage()
self._nvi_ema.Length = self._nvi_ema_length.Value
length = self._channel_length.Value
bollinger = BollingerBands()
bollinger.Length = length
bollinger.Width = Decimal(self._channel_multiplier.Value)
ema = ExponentialMovingAverage()
ema.Length = length
atr = AverageTrueRange()
atr.Length = length
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(bollinger, ema, atr, self._process_candle).Start()
take_percent = Decimal(self._take_profit_percent.Value)
stop_percent = Decimal(self._stop_loss_percent.Value)
take_profit = Unit(take_percent, UnitTypes.Percent) if self._enable_take_profit.Value and take_percent > 0 else Unit()
stop_loss = Unit(stop_percent, UnitTypes.Percent) if self._enable_stop_loss.Value and stop_percent > 0 else Unit()
self.StartProtection(take_profit, stop_loss, useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, bollinger)
self.DrawOwnTrades(area)
def _process_candle(self, candle, bollinger_value, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
volume = candle.TotalVolume
pc = self._prev_close
pv = self._prev_volume
if pc is not None and pv is not None and pc != 0 and volume < pv:
self._nvi += self._nvi * (close - pc) / pc
self._prev_close = close
self._prev_volume = volume
nvi_input = DecimalIndicatorValue(self._nvi_ema, self._nvi, candle.OpenTime)
nvi_input.IsFinal = True
nvi_ema_value = self._nvi_ema.Process(nvi_input)
if not nvi_ema_value.IsFormed:
return
if str(self._channel_type.Value).upper() == "KC":
if not ema_value.IsFormed or not atr_value.IsFormed:
return
lower = ema_value.GetValue[Decimal](None) - Decimal(self._channel_multiplier.Value) * atr_value.GetValue[Decimal](None)
else:
if not bollinger_value.IsFormed or bollinger_value.LowBand is None:
return
lower = bollinger_value.LowBand
if not self.IsFormedAndOnlineAndAllowTrading():
return
nvi_ema = nvi_ema_value.GetValue[Decimal](None)
if self.Position > 0:
if self._nvi < nvi_ema:
self.SellMarket(self.Position)
elif self.Position == 0 and close < lower and self._nvi > nvi_ema:
self.BuyMarket(self.Volume)
def CreateClone(self):
return channels_with_nvi_strategy()