Дивергенция объёма
Стратегия "Дивергенция объёма" отслеживает расхождения между движением цены и торговым объёмом. Если цена падает, а объём растёт, это может указывать на накопление; если цена растёт при высоком объёме, это может свидетельствовать о распределении.
Тестирование показывает среднегодичную доходность около 43%. Стратегию лучше запускать на фондовом рынке.
Вход в лонг происходит, когда падающие цены сопровождаются увеличением объёма. Вход в шорт — когда растущие цены идут в паре с большим объёмом. Выход осуществляется по пересечению скользящей средней или стопу.
Подход рассчитан на торговлю против неустойчивых движений.
Детали
- Условия входа: Цена и объём движутся в противоположных направлениях.
- Длинные/короткие позиции: Оба направления.
- Условия выхода: Пересечение цены со скользящей средней или стоп.
- Стопы: Да.
- Значения по умолчанию:
MAPeriod= 20ATRPeriod= 14CandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Divergence
- Направление: Оба
- Индикаторы: Объём, MA
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Да
- Уровень риска: Средний
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>
/// Buys a lower close and sells a higher close when volume rises over the previous candle.
/// Fully exits when Close crosses the SMA in either direction or on actual-fill entry-ATR protection.
/// </summary>
public class VolumeDivergenceStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _ma;
private AverageTrueRange _atr;
private decimal? _previousClose;
private decimal? _previousVolume;
private decimal? _previousMa;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
public int MAPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
public int ATRPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal StopLossATRMultiplier { get => _stopLossAtrMultiplier.Value; set => _stopLossAtrMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VolumeDivergenceStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Current-inclusive Close SMA length", "Indicators")
.SetOptimize(10, 50, 10);
_atrPeriod = Param(nameof(ATRPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR length for actual-fill protection", "Indicators")
.SetOptimize(7, 28, 7);
_stopLossAtrMultiplier = Param(nameof(StopLossATRMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Stop Multiplier", "Frozen signal ATR distance from actual fills; zero disables it", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearSignalState();
}
private void ClearSignalState()
{
_ma = null;
_atr = null;
_previousClose = null;
_previousVolume = null;
_previousMa = null;
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearSignalState();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
_ma = new SimpleMovingAverage { Length = MAPeriod };
_atr = new AverageTrueRange { Length = ATRPeriod };
Indicators.Add(_ma);
Indicators.Add(_atr);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawIndicator(area, _atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var maValue = _ma.Process(candle);
var atrValue = _atr.Process(candle);
decimal? ma = !maValue.IsEmpty && _ma.IsFormed ? maValue.GetValue<decimal>() : null;
decimal? atr = !atrValue.IsEmpty && _atr.IsFormed ? atrValue.GetValue<decimal>() : null;
var close = candle.ClosePrice;
var volume = candle.TotalVolume;
var priceDown = _previousClose is decimal down && close < down;
var priceUp = _previousClose is decimal up && close > up;
var volumeUp = _previousVolume is decimal priorVolume && volume > priorVolume;
var maUp = _previousClose is decimal priorUpClose && _previousMa is decimal priorUpMa &&
ma is decimal currentUpMa && priorUpClose <= priorUpMa && close > currentUpMa;
var maDown = _previousClose is decimal priorDownClose && _previousMa is decimal priorDownMa &&
ma is decimal currentDownMa && priorDownClose >= priorDownMa && close < currentDownMa;
// Warmup and unavailable trading still advance raw price/volume and formed MA history.
_previousClose = close;
_previousVolume = volume;
_previousMa = ma;
if (ma is null || atr is not decimal currentAtr ||
!IsFormedAndOnlineAndAllowTrading() ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var maCross = maUp || maDown;
if (Position > 0m && maCross) SellMarket(Position);
else if (Position < 0m && maCross) BuyMarket(Math.Abs(Position));
else if (Position == 0m)
{
if (priceDown && volumeUp) Enter(Sides.Buy, currentAtr);
else if (priceUp && volumeUp) Enter(Sides.Sell, currentAtr);
}
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * StopLossATRMultiplier;
_stopDistance ??= new Unit(distance);
// The cached native controller keeps this same Unit reference across entries.
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume,
Comment = "Volume divergence entry",
});
}
}
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, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class volume_divergence_strategy(Strategy):
"""
Buys a lower close and sells a higher close when volume rises over the previous candle.
Fully exits when Close crosses the SMA in either direction or on actual-fill entry-ATR protection.
"""
def __init__(self):
super(volume_divergence_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Current-inclusive Close SMA length", "Indicators")
self._atr_period = self.Param("ATRPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Wilder ATR length for actual-fill protection", "Indicators")
self._stop_loss_atr_multiplier = self.Param("StopLossATRMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Stop Multiplier", "Frozen signal ATR distance from actual fills; zero disables it", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._ma = None
self._atr = None
self._previous_close = None
self._previous_volume = None
self._previous_ma = None
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _clear_signal_state(self):
self._ma = None
self._atr = None
self._previous_close = None
self._previous_volume = None
self._previous_ma = None
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnReseted(self):
super(volume_divergence_strategy, self).OnReseted()
self._clear_signal_state()
def OnStarted2(self, time):
super(volume_divergence_strategy, self).OnStarted2(time)
self._clear_signal_state()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
self._ma = SimpleMovingAverage()
self._ma.Length = self._ma_period.Value
self._atr = AverageTrueRange()
self._atr.Length = self._atr_period.Value
self.Indicators.Add(self._ma)
self.Indicators.Add(self._atr)
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.DrawIndicator(area, self._ma)
self.DrawIndicator(area, self._atr)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between finished candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
ma_value = self._ma.Process(CandleIndicatorValue(self._ma, candle))
atr_value = self._atr.Process(CandleIndicatorValue(self._atr, candle))
ma = ma_value.GetValue[Decimal](None) if not ma_value.IsEmpty and self._ma.IsFormed else None
atr = atr_value.GetValue[Decimal](None) if not atr_value.IsEmpty and self._atr.IsFormed else None
close = candle.ClosePrice
volume = candle.TotalVolume
price_down = self._previous_close is not None and close < self._previous_close
price_up = self._previous_close is not None and close > self._previous_close
volume_up = self._previous_volume is not None and volume > self._previous_volume
ma_up = self._previous_close is not None and self._previous_ma is not None and ma is not None and self._previous_close <= self._previous_ma and close > ma
ma_down = self._previous_close is not None and self._previous_ma is not None and ma is not None and self._previous_close >= self._previous_ma and close < ma
# Advance raw price/volume and formed MA history even during warmup or disabled trading.
self._previous_close = close
self._previous_volume = volume
self._previous_ma = ma
if ma is None or atr is None or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
ma_cross = ma_up or ma_down
if self.Position > 0 and ma_cross:
self.SellMarket(self.Position)
elif self.Position < 0 and ma_cross:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0:
if price_down and volume_up:
self._enter(Sides.Buy, atr)
elif price_up and volume_up:
self._enter(Sides.Sell, atr)
def _enter(self, side, atr):
distance = atr * Decimal(self._stop_loss_atr_multiplier.Value)
if self._stop_distance is None:
self._stop_distance = Unit(distance)
# Preserve the Unit reference cached by native protection controllers.
self._stop_distance.Value = distance
if not self._protection_started and distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume
order.Comment = "Volume divergence entry"
self.RegisterOrder(order)
def CreateClone(self):
return volume_divergence_strategy()