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>
/// Volume based reversal strategy that reacts to increasing or decreasing tick volume.
/// </summary>
public class VolumeTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private decimal? _previousVolume;
private decimal? _previousPreviousVolume;
/// <summary>
/// Candle type used to calculate the signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Inclusive start hour of the trading session.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Inclusive end hour of the trading session.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="VolumeTraderStrategy"/>.
/// </summary>
public VolumeTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signal calculation", "General");
_startHour = Param(nameof(StartHour), 9)
.SetDisplay("Start Hour", "Inclusive start hour for trading", "Session")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 18)
.SetDisplay("End Hour", "Inclusive end hour for trading", "Session")
.SetRange(0, 23);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousVolume = null;
_previousPreviousVolume = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Wait until the candle is finished to avoid partial data.
if (candle.State != CandleStates.Finished)
return;
var currentVolume = candle.TotalVolume;
if (_previousVolume.HasValue && _previousPreviousVolume.HasValue)
{
// MQL version trades at the open of the next bar, so use the next bar time for the filter.
var nextBarTime = candle.CloseTime;
var hour = nextBarTime.Hour;
var inSession = hour >= StartHour && hour <= EndHour;
if (inSession && IsFormedAndOnlineAndAllowTrading())
{
var prevVolume = _previousVolume.Value;
var prevPrevVolume = _previousPreviousVolume.Value;
// Rising volume suggests upward pressure -> go long.
if (prevVolume > prevPrevVolume * 1.1m && Position <= 0)
{
var volumeToTrade = Volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (volumeToTrade > 0)
{
BuyMarket(volumeToTrade);
LogInfo($"Volume increased from {prevPrevVolume} to {prevVolume}. Opening long position.");
}
}
// Falling volume suggests weakening demand -> go short.
else if (prevVolume < prevPrevVolume * 0.9m && Position >= 0)
{
var volumeToTrade = Volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (volumeToTrade > 0)
{
SellMarket(volumeToTrade);
LogInfo($"Volume decreased from {prevPrevVolume} to {prevVolume}. Opening short position.");
}
}
}
}
// Shift stored volumes so the latest closed candle becomes the previous reference.
_previousPreviousVolume = _previousVolume;
_previousVolume = currentVolume;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class volume_trader_strategy(Strategy):
"""Volume-based reversal: rising volume suggests long, falling volume suggests short."""
def __init__(self):
super(volume_trader_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 9).SetDisplay("Start Hour", "Trading session start", "Session")
self._end_hour = self.Param("EndHour", 18).SetDisplay("End Hour", "Trading session end", "Session")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(volume_trader_strategy, self).OnReseted()
self._prev_vol = None
self._prev_prev_vol = None
def OnStarted2(self, time):
super(volume_trader_strategy, self).OnStarted2(time)
self._prev_vol = None
self._prev_prev_vol = None
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
vol = float(candle.TotalVolume)
if self._prev_vol is not None and self._prev_prev_vol is not None:
hour = candle.CloseTime.Hour
start_h = self._start_hour.Value
end_h = self._end_hour.Value
in_session = hour >= start_h and hour <= end_h
if in_session:
if self._prev_vol > self._prev_prev_vol * 1.1 and self.Position <= 0:
vol_to_trade = self.Volume + (abs(self.Position) if self.Position < 0 else 0)
if vol_to_trade > 0:
self.BuyMarket(vol_to_trade)
elif self._prev_vol < self._prev_prev_vol * 0.9 and self.Position >= 0:
vol_to_trade = self.Volume + (abs(self.Position) if self.Position > 0 else 0)
if vol_to_trade > 0:
self.SellMarket(vol_to_trade)
self._prev_prev_vol = self._prev_vol
self._prev_vol = vol
def CreateClone(self):
return volume_trader_strategy()