Volume per Point Strategy
This strategy calculates the volume per price point for each candle. A long trade is opened when the candle range decreases but volume increases and the RSI filter (if enabled) confirms the signal. A short trade is opened when the range expands while volume contracts.
Parameters
- RSI Length – period for RSI calculation.
- RSI Above/Below – thresholds for the optional RSI filter.
- Use RSI Filter – enable or disable RSI filtering.
- Candle Type – timeframe of input candles.
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>
/// Strategy based on volume per price point with RSI filter.
/// Buys when range decreases but volume increases, sells on opposite condition.
/// </summary>
public class VolumePerPointStrategy : Strategy
{
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _rsiHigh;
private readonly StrategyParam<int> _rsiLow;
private readonly StrategyParam<bool> _useRsiFilter;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevRange;
private decimal _prevVolume;
private decimal _prevClose;
private int _cooldownRemaining;
/// <summary>
/// RSI length.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// Upper RSI threshold.
/// </summary>
public int RsiHigh
{
get => _rsiHigh.Value;
set => _rsiHigh.Value = value;
}
/// <summary>
/// Lower RSI threshold.
/// </summary>
public int RsiLow
{
get => _rsiLow.Value;
set => _rsiLow.Value = value;
}
/// <summary>
/// Use RSI filter.
/// </summary>
public bool UseRsiFilter
{
get => _useRsiFilter.Value;
set => _useRsiFilter.Value = value;
}
/// <summary>
/// Bars to wait between trading actions.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="VolumePerPointStrategy"/>.
/// </summary>
public VolumePerPointStrategy()
{
_rsiLength = Param(nameof(RsiLength), 14)
.SetDisplay("RSI Length", "Period for RSI", "Indicators")
.SetOptimize(10, 20, 2);
_rsiHigh = Param(nameof(RsiHigh), 65)
.SetDisplay("RSI Above", "Upper RSI threshold", "Filters");
_rsiLow = Param(nameof(RsiLow), 35)
.SetDisplay("RSI Below", "Lower RSI threshold", "Filters");
_useRsiFilter = Param(nameof(UseRsiFilter), true)
.SetDisplay("Use RSI Filter", "Enable RSI filtering", "Filters");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevRange = 0;
_prevVolume = 0;
_prevClose = 0;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var rsi = new RelativeStrengthIndex { Length = RsiLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (_prevRange == 0)
{
_prevRange = candle.HighPrice - candle.LowPrice;
_prevVolume = candle.TotalVolume;
_prevClose = candle.ClosePrice;
return;
}
var step = Security?.PriceStep ?? 0.0001m;
var range = Math.Max(candle.HighPrice - candle.LowPrice, step);
var previousRange = Math.Max(_prevRange, step);
var volume = candle.TotalVolume;
var volumePerPoint = volume / range;
var previousVolumePerPoint = _prevVolume / previousRange;
var bullishImpulse = candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > _prevClose;
var bearishImpulse = candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < _prevClose;
var buySignal = volumePerPoint >= previousVolumePerPoint * 1.5m && bullishImpulse && (!UseRsiFilter || rsiValue <= RsiLow);
var sellSignal = volumePerPoint >= previousVolumePerPoint * 1.5m && bearishImpulse && (!UseRsiFilter || rsiValue >= RsiHigh);
if (_cooldownRemaining == 0 && buySignal && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && sellSignal && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
_prevRange = range;
_prevVolume = volume;
_prevClose = candle.ClosePrice;
}
}
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, Unit, UnitTypes, Sides
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class volume_per_point_strategy(Strategy):
def __init__(self):
super(volume_per_point_strategy, self).__init__()
self._rsi_length = self.Param("RsiLength", 14) \
.SetDisplay("RSI Length", "Period for RSI", "Indicators")
self._rsi_high = self.Param("RsiHigh", 65) \
.SetDisplay("RSI Above", "Upper RSI threshold", "Filters")
self._rsi_low = self.Param("RsiLow", 35) \
.SetDisplay("RSI Below", "Lower RSI threshold", "Filters")
self._use_rsi_filter = self.Param("UseRsiFilter", True) \
.SetDisplay("Use RSI Filter", "Enable RSI filtering", "Filters")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._cooldown_remaining = 0
self._prev_range = 0.0
self._prev_volume = 0.0
self._prev_close = 0.0
@property
def rsi_length(self):
return self._rsi_length.Value
@property
def rsi_high(self):
return self._rsi_high.Value
@property
def rsi_low(self):
return self._rsi_low.Value
@property
def use_rsi_filter(self):
return self._use_rsi_filter.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_per_point_strategy, self).OnReseted()
self._cooldown_remaining = 0
self._prev_range = 0.0
self._prev_volume = 0.0
self._prev_close = 0.0
def OnStarted2(self, time):
super(volume_per_point_strategy, self).OnStarted2(time)
rsi = RelativeStrengthIndex()
rsi.Length = self.rsi_length
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(rsi, self.on_process).Start()
self.StartProtection(Unit(2, UnitTypes.Percent), Unit(1, UnitTypes.Percent))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def on_process(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
rsi_value = float(rsi_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_p = float(candle.OpenPrice)
if self._prev_range == 0:
self._prev_range = high - low
self._prev_volume = float(candle.TotalVolume)
self._prev_close = close
return
ps = self.Security.PriceStep if self.Security is not None else None
step = float(ps) if ps is not None else 0.0001
candle_range = max(high - low, step)
previous_range = max(self._prev_range, step)
volume = float(candle.TotalVolume)
volume_per_point = volume / candle_range
previous_volume_per_point = self._prev_volume / previous_range
bullish_impulse = close > open_p and close > self._prev_close
bearish_impulse = close < open_p and close < self._prev_close
buy_signal = volume_per_point >= previous_volume_per_point * 1.5 and bullish_impulse and (not self.use_rsi_filter or rsi_value <= float(self.rsi_low))
sell_signal = volume_per_point >= previous_volume_per_point * 1.5 and bearish_impulse and (not self.use_rsi_filter or rsi_value >= float(self.rsi_high))
if self._cooldown_remaining == 0 and buy_signal and self.Position <= 0:
self.BuyMarket()
self._cooldown_remaining = self.signal_cooldown_bars
elif self._cooldown_remaining == 0 and sell_signal and self.Position >= 0:
self.SellMarket()
self._cooldown_remaining = self.signal_cooldown_bars
self._prev_range = candle_range
self._prev_volume = volume
self._prev_close = close
def CreateClone(self):
return volume_per_point_strategy()