在 GitHub 上查看

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> 暴露,可用于界面配置或参数优化。

交易逻辑

  1. 仅处理已完成的蜡烛,确保与 EA 的逐根逻辑保持一致。
  2. 在计算信号前,将 Volume[1]Volume[2] 的对应值缓存到 _previousVolume_twoBarsAgoVolume
  3. 检查蜡烛的开始时间是否处于 StartHourEndHour 之间(包含端点)。若不在范围内,则立即平仓并跳过开仓。
  4. 根据成交量比较得出目标方向:
    • 最新成交量小于上一根时做多。
    • 最新成交量大于上一根时做空。
    • 其余情况视为中性。
  5. 当目标方向与当前仓位不一致时,先通过 BuyMarket(-Position)SellMarket(Position) 平掉反向仓位。
  6. 仅在当前为空仓或刚刚完成反向平仓时,使用配置的 TradeVolume 开启新仓。
  7. 更新缓存的成交量,以便下一次循环继续比较最近两根完成蜡烛。

上述流程确保蜡烛尚未收盘时不会产生订单,并保持与依赖 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();
		}
	}
}