Volume by Session Strategy
Simplified strategy derived from the TradingView "Volume by Session" indicator. The trading day is divided into four sessions, each with its own average volume. When current volume within a session deviates from its average, the strategy enters trades accordingly.
Details
- Entry: Current session volume above or below its moving average.
- Exit: Opposite signal closes existing position.
- Long/Short: Both.
- Indicators: SMA.
- Timeframe: Intraday.
This is a minimal educational translation; visualization and extensive settings of the original script are omitted.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volume by session strategy.
/// Tracks average volume and trades on volume deviations with price confirmation.
/// Buys when volume spikes above average and price is rising, sells when opposite.
/// </summary>
public class VolumeBySessionStrategy : Strategy
{
private readonly StrategyParam<int> _volAvgLength;
private readonly StrategyParam<decimal> _volMult;
private readonly StrategyParam<decimal> _stopPct;
private readonly StrategyParam<decimal> _tpPct;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly List<decimal> _volumes = new();
private decimal _volumeSum;
private decimal _entryPrice;
private decimal _stopDist;
private decimal? _prevHigh;
private decimal? _prevLow;
private int _cooldownRemaining;
public int VolAvgLength { get => _volAvgLength.Value; set => _volAvgLength.Value = value; }
public decimal VolMult { get => _volMult.Value; set => _volMult.Value = value; }
public decimal StopPct { get => _stopPct.Value; set => _stopPct.Value = value; }
public decimal TpPct { get => _tpPct.Value; set => _tpPct.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public VolumeBySessionStrategy()
{
_volAvgLength = Param(nameof(VolAvgLength), 20)
.SetGreaterThanZero()
.SetDisplay("Vol Avg Length", "Volume average period", "Parameters");
_volMult = Param(nameof(VolMult), 2.25m)
.SetGreaterThanZero()
.SetDisplay("Vol Multiplier", "Volume spike multiplier", "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");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between new entries", "Trading");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_volumes.Clear();
_volumeSum = 0m;
_entryPrice = 0;
_stopDist = 0;
_prevHigh = null;
_prevLow = null;
_cooldownRemaining = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_volumeSum = 0m;
_entryPrice = 0;
_stopDist = 0;
_prevHigh = null;
_prevLow = null;
_cooldownRemaining = 0;
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 vol = candle.TotalVolume;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
// TP/SL
if (Position > 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice <= _entryPrice - _stopDist || candle.ClosePrice >= _entryPrice + _stopDist * (TpPct / StopPct))
{
SellMarket(Position);
_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(Math.Abs(Position));
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = SignalCooldownBars;
}
}
if (_volumes.Count >= VolAvgLength && _prevHigh.HasValue && _prevLow.HasValue && _cooldownRemaining == 0 && Position == 0)
{
var avgVol = _volumeSum / _volumes.Count;
var highVol = vol >= avgVol * VolMult;
var bullishBreakout = candle.ClosePrice > _prevHigh.Value && candle.ClosePrice > candle.OpenPrice;
var bearishBreakout = candle.ClosePrice < _prevLow.Value && candle.ClosePrice < candle.OpenPrice;
var minBody = Math.Max(candle.ClosePrice * 0.001m, Security?.PriceStep ?? 0m);
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
if (highVol && bullishBreakout && body >= minBody)
{
BuyMarket(Volume);
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
else if (highVol && bearishBreakout && body >= minBody)
{
SellMarket(Volume);
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
}
_volumes.Add(vol);
_volumeSum += vol;
if (_volumes.Count > VolAvgLength)
{
_volumeSum -= _volumes[0];
_volumes.RemoveAt(0);
}
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class volume_by_session_strategy(Strategy):
def __init__(self):
super(volume_by_session_strategy, self).__init__()
self._vol_avg_length = self.Param("VolAvgLength", 20) \
.SetDisplay("Vol Avg Length", "Volume average period", "Parameters")
self._vol_mult = self.Param("VolMult", 2.25) \
.SetDisplay("Vol Multiplier", "Volume spike multiplier", "Parameters")
self._stop_pct = self.Param("StopPct", 0.5) \
.SetDisplay("Stop %", "Stop loss percent", "Risk")
self._tp_pct = self.Param("TpPct", 1.0) \
.SetDisplay("TP %", "Take profit percent", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between new entries", "Trading")
self._volumes = []
self._volume_sum = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_high = None
self._prev_low = None
self._cooldown_remaining = 0
@property
def vol_avg_length(self):
return self._vol_avg_length.Value
@property
def vol_mult(self):
return self._vol_mult.Value
@property
def stop_pct(self):
return self._stop_pct.Value
@property
def tp_pct(self):
return self._tp_pct.Value
@property
def candle_type(self):
return self._candle_type.Value
@property
def signal_cooldown_bars(self):
return self._signal_cooldown_bars.Value
def OnReseted(self):
super(volume_by_session_strategy, self).OnReseted()
self._volumes = []
self._volume_sum = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_high = None
self._prev_low = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(volume_by_session_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
vol = float(candle.TotalVolume)
close = float(candle.ClosePrice)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
# TP/SL
tp_pct = float(self.tp_pct)
stop_pct = float(self.stop_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_avg_length and self._prev_high is not None and self._prev_low is not None and self._cooldown_remaining == 0 and self.Position == 0:
avg_vol = self._volume_sum / len(self._volumes)
high_vol = vol >= avg_vol * float(self.vol_mult)
bullish_breakout = close > self._prev_high and close > float(candle.OpenPrice)
bearish_breakout = close < self._prev_low and close < float(candle.OpenPrice)
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
min_body = max(float(candle.ClosePrice) * 0.001, price_step)
body = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
if high_vol and bullish_breakout and body >= min_body:
self.BuyMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
elif high_vol and bearish_breakout and body >= min_body:
self.SellMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
self._volumes.append(vol)
self._volume_sum += vol
if len(self._volumes) > self.vol_avg_length:
self._volume_sum -= self._volumes[0]
self._volumes.pop(0)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
def CreateClone(self):
return volume_by_session_strategy()