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 Trader V2 strategy converted from the MetaTrader expert Volume_trader_v2_www_forex-instruments_info.mq4.
/// Follows the original logic by comparing the volume of the last two finished candles and trading only during configured hours.
/// </summary>
public class VolumeTraderV2Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _tradeVolume;
private decimal? _previousVolume;
private decimal? _twoBarsAgoVolume;
/// <summary>
/// Initializes a new instance of the <see cref="VolumeTraderV2Strategy"/> class.
/// </summary>
public VolumeTraderV2Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used to request candles", "Data");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "First hour (inclusive) when trading is allowed", "Trading")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Last hour (inclusive) when trading is allowed", "Trading")
.SetRange(0, 23);
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Trade Volume", "Order volume replicated from the original EA", "Trading")
.SetGreaterThanZero();
Volume = TradeVolume;
}
/// <summary>
/// Candle type used for the strategy calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// First trading hour (inclusive).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Last trading hour (inclusive).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Default order volume for market operations.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Drop cached volume values so the warm-up sequence matches the EA behavior after a reset.
_previousVolume = null;
_twoBarsAgoVolume = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Subscribe to candles and process them with the same granularity as the original indicator buffers.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
// Only act on finished candles to replicate the bar-by-bar logic.
if (candle.State != CandleStates.Finished)
return;
var currentVolume = candle.TotalVolume;
// Collect the first two candles before generating signals.
if (_previousVolume is null)
{
_previousVolume = currentVolume;
return;
}
if (_twoBarsAgoVolume is null)
{
_twoBarsAgoVolume = _previousVolume;
_previousVolume = currentVolume;
return;
}
var volume1 = _previousVolume.Value;
var volume2 = _twoBarsAgoVolume.Value;
var hour = candle.OpenTime.Hour;
var hourValid = hour >= StartHour && hour <= EndHour;
var shouldGoLong = hourValid && volume1 < volume2;
var shouldGoShort = hourValid && volume1 > volume2;
var comment = !hourValid
? "Trading paused"
: shouldGoLong
? "Up trend"
: shouldGoShort
? "Down trend"
: "No trend...";
if (!shouldGoLong && !shouldGoShort)
{
// Exit the market when no direction is active (equal volume or outside trading hours).
ClosePosition();
}
else if (shouldGoLong)
{
// Flatten any short position before opening a new long trade.
if (Position < 0)
BuyMarket();
if (Position <= 0)
BuyMarket();
}
else if (shouldGoShort)
{
// Flatten any long position before opening a new short trade.
if (Position > 0)
SellMarket();
if (Position >= 0)
SellMarket();
}
// Shift the cached volumes to emulate Volume[1] and Volume[2] from MetaTrader.
_twoBarsAgoVolume = _previousVolume;
_previousVolume = currentVolume;
}
private void ClosePosition()
{
// Mirror the EA behavior by leaving the market whenever the signal is neutral.
if (Position > 0)
{
SellMarket();
}
else if (Position < 0)
{
BuyMarket();
}
}
}
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_trader_v2_strategy(Strategy):
"""Volume Trader V2: compares volume of the last two finished candles and trades
only during configured hours. Goes long when previous volume < two-bars-ago volume,
short when previous volume > two-bars-ago volume."""
def __init__(self):
super(volume_trader_v2_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))) \
.SetDisplay("Candle Type", "Time frame used to request candles", "Data")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "First hour (inclusive) when trading is allowed", "Trading")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Last hour (inclusive) when trading is allowed", "Trading")
self._trade_volume = self.Param("TradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Order volume replicated from the original EA", "Trading")
self._previous_volume = None
self._two_bars_ago_volume = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def TradeVolume(self):
return self._trade_volume.Value
def OnReseted(self):
super(volume_trader_v2_strategy, self).OnReseted()
self._previous_volume = None
self._two_bars_ago_volume = None
def OnStarted2(self, time):
super(volume_trader_v2_strategy, self).OnStarted2(time)
self.Volume = float(self.TradeVolume)
self.StartProtection(None, None)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
current_volume = candle.TotalVolume
if self._previous_volume is None:
self._previous_volume = current_volume
return
if self._two_bars_ago_volume is None:
self._two_bars_ago_volume = self._previous_volume
self._previous_volume = current_volume
return
volume1 = self._previous_volume
volume2 = self._two_bars_ago_volume
hour = candle.OpenTime.Hour
hour_valid = hour >= self.StartHour and hour <= self.EndHour
should_go_long = hour_valid and volume1 < volume2
should_go_short = hour_valid and volume1 > volume2
if not should_go_long and not should_go_short:
# Exit position when no direction is active
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
elif should_go_long:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
elif should_go_short:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._two_bars_ago_volume = self._previous_volume
self._previous_volume = current_volume
def CreateClone(self):
return volume_trader_v2_strategy()