在 GitHub 上查看

Volume Trader 策略

概述

  • 基于 MetaTrader 5 专家顾问 “Volume trader”(ID 21050)(作者 Vladimir Karputov)的移植版本。
  • 使用 StockSharp 的高层 API 重写,保留原始交易思路。
  • 当最新两个柱子的 tick 成交量发生变化时,在自定义交易时段内顺势切换持仓方向。

交易逻辑

  1. 订阅由 CandleType 定义的 K 线序列(默认 1 小时),读取每根 K 线的 tick 成交量 TotalVolume
  2. 每当一根 K 线收盘时,比较前两根已收盘 K 线的成交量,模拟原始 MQL5 脚本在新柱诞生时的比较方式。
  3. 如果最近一根 K 线的成交量高于之前一根,并且当前没有多头仓位,则买入 Volume 合约;若存在空头仓位,会先买入同等数量平仓再建立新的多头。
  4. 如果最近一根 K 线的成交量低于之前一根,并且当前没有空头仓位,则卖出 Volume 合约;若存在多头仓位,会先卖出相同数量平仓再建立新的空头。
  5. 当下一根 K 线的开盘时间不在 [StartHour, EndHour] 区间内时,信号会被忽略。默认的 09:00–18:00 区间与原版参数一致。
  6. 策略没有内置止损或止盈,出现反向信号时直接反手。

下单与持仓

  • 通过 BuyMarket / SellMarket 发送市价单,在新 K 线开盘时立即完成建仓或反手。
  • 反手时交易量等于当前仓位的绝对值加上参数 Volume,确保旧仓位被完全平掉后再开新仓。
  • 除固定的 Volume 参数外,没有其它资金管理规则。

参数

参数 默认值 说明
CandleType 1 小时 用于计算 tick 成交量的 K 线类型,请根据目标市场调整。
StartHour 9 交易时段起始小时(包含,0–23)。在此之前产生的信号将被忽略。
EndHour 18 交易时段结束小时(包含,0–23)。在此之后产生的信号将被忽略。
Volume 0.1 新开仓以及反手时使用的下单数量。

使用建议

  • 确认数据源能够在蜡烛消息中提供 tick 成交量;若只有真实成交量,策略会基于该数据运行。
  • 调整 CandleType 以匹配原策略在 MetaTrader 中使用的时间周期。
  • 如需风险控制,请在外部添加止损、止盈或日内亏损限制。
  • 策略在开仓时调用 LogInfo 输出日志,便于回溯信号。

与原版 MQL 实现的区别

  • 采用 StockSharp 的 K 线订阅机制,而不是显式调用 CopyTickVolume
  • 通过已收盘 K 线的 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;
	}
}