Порт эксперта MetaTrader 5 «Volume trader» (ID 21050) автора Владимир Карпутов.
Переписана на высокоуровневом API StockSharp без изменения базовой логики.
Ориентирована на работу по направлению изменения тикового объёма внутри заданного торгового окна.
Логика работы
Подписывается на свечи типа CandleType (по умолчанию таймфрейм 1 час) и считывает их тиковый объём TotalVolume.
После закрытия каждой свечи сравниваются объёмы двух предыдущих закрытых баров, что повторяет логику MQL5, запускаемую в момент рождения новой свечи.
Если последний объём выше, чем объём бара до него, и нет длинной позиции, стратегия покупает Volume контрактов и одновременно закрывает существующий шорт.
Если последний объём ниже, чем объём бара до него, и нет короткой позиции, стратегия продаёт Volume контрактов и одновременно закрывает существующий лонг.
Сигналы игнорируются, если время открытия следующей свечи выходит за пределы интервала [StartHour, EndHour]. Значения 09:00–18:00 повторяют исходные настройки.
Стоп-лосс и тейк-профит по умолчанию отсутствуют — позиции разворачиваются при появлении противоположного сигнала.
Управление заявками
Вход выполняется рыночными приказами через BuyMarket или SellMarket, что позволяет немедленно развернуть позицию на открытии новой свечи.
При смене направления стратегия автоматически торгует объём, равный модулю текущей позиции плюс Volume, полностью закрывая старую позицию перед открытием новой.
Дополнительных правил мани-менеджмента, помимо фиксированного параметра Volume, не предусмотрено.
Параметры
Параметр
Значение по умолчанию
Описание
CandleType
Таймфрейм 1 час
Свечная серия для расчёта тикового объёма. Настройте под используемый таймфрейм в MetaTrader.
StartHour
9
Начало торговой сессии (включительно), часы 0–23. Сигналы раньше этого времени игнорируются.
EndHour
18
Завершение торговой сессии (включительно), часы 0–23. Сигналы позже этого времени игнорируются.
Volume
0.1
Объём заявки для новых входов и переворота позиции.
Рекомендации по использованию
Убедитесь, что источник данных передаёт тиковый объём в сообщениях свечей. При отсутствии тиковых данных стратегия будет опираться на фактический торговый объём.
Приведите параметр CandleType к таймфрейму, на котором тестировалась оригинальная MQL-версия.
При необходимости добавьте внешние ограничения по рискам (стоп-лосс, тейк-профит, дневной лимит потерь).
В журнал (LogInfo) выводятся сообщения при открытии позиций, что упрощает анализ сигналов.
Отличия от оригинала на MQL5
Используется механизм подписки на свечи StockSharp вместо явного вызова CopyTickVolume.
Фильтр по времени рассчитывается через CloseTime завершённой свечи, что соответствует проверке времени открытия следующего бара в исходном советнике.
Заявки отправляются через высокоуровневые методы BuyMarket/SellMarket вместо прямого обращения к CTrade.
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()