Volume Trader V2 策略
概述
Volume Trader V2 是 MetaTrader 专家顾问 Volume_trader_v2_www_forex-instruments_info.mq4 的直接移植版本。原始系统通过观察最近蜡烛的总成交量变化来判断短期资金流向,并据此保持单一方向的仓位。本次移植保留了仅持有单仓、按时间窗口过滤以及每根完成蜡烛只处理一次的行为特征。
策略会订阅一个可配置的蜡烛序列,并缓存最近两根完成蜡烛的成交量。当新蜡烛收盘时,会比较前两根蜡烛的成交量(即 MetaTrader 中的 Volume[1] 和 Volume[2]),并生成最新的交易方向:
- 当
Volume[1] < Volume[2]时产生 做多 信号。 - 当
Volume[1] > Volume[2]时产生 做空 信号。 - 成交量相等或不在允许的交易时间内,则平掉所有仓位。
在发送新订单之前,如果当前仓位方向相反,会先平仓,以确保 StockSharp 版本与 MetaTrader 的订单生命周期保持一致。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CandleType |
5 分钟周期 | 通过 SubscribeCandles 请求的数据类型,请根据原始图表周期进行调整。 |
StartHour |
8 | 允许交易的起始小时(包含)。在此时间段之外会忽略信号并关闭仓位。 |
EndHour |
20 | 允许交易的结束小时(包含)。蜡烛起始时间超过该值时策略保持空仓。 |
TradeVolume |
0.1 | 从 EA 复制的下单手数,同时写入 Strategy.Volume 供辅助下单方法使用。 |
所有参数都通过 StrategyParam<T> 暴露,可用于界面配置或参数优化。
交易逻辑
- 仅处理已完成的蜡烛,确保与 EA 的逐根逻辑保持一致。
- 在计算信号前,将
Volume[1]与Volume[2]的对应值缓存到_previousVolume和_twoBarsAgoVolume。 - 检查蜡烛的开始时间是否处于
StartHour与EndHour之间(包含端点)。若不在范围内,则立即平仓并跳过开仓。 - 根据成交量比较得出目标方向:
- 最新成交量小于上一根时做多。
- 最新成交量大于上一根时做空。
- 其余情况视为中性。
- 当目标方向与当前仓位不一致时,先通过
BuyMarket(-Position)或SellMarket(Position)平掉反向仓位。 - 仅在当前为空仓或刚刚完成反向平仓时,使用配置的
TradeVolume开启新仓。 - 更新缓存的成交量,以便下一次循环继续比较最近两根完成蜡烛。
上述流程确保蜡烛尚未收盘时不会产生订单,并保持与依赖 LastBarChecked 的 MetaTrader 实现同样的节奏。
补充说明
- 在
OnStarted中调用StartProtection(),利用框架的仓位保护辅助工具追踪当前仓位。 Comment属性会输出与 EA 相同的提示信息("Up trend"、"Down trend"、"No trend..."、"Trading paused"),方便监控。- 策略未引入额外集合,完全使用高层蜡烛订阅 API,符合项目规范。
- 请根据原始 EA 使用的品种和周期,设置合适的蜡烛类型、标的与手数,以获得可比的表现。
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()