在 GitHub 上查看

Tunnel Gen4 对冲网格策略

该策略使用 StockSharp 高级 API 复刻 MetaTrader "Tunnel gen4" 智能交易系统的逻辑。策略通过建立买卖对冲头寸来保持市场中性,当价格沿突破方向运行预设的点数时会按两倍基础手数加仓,并在第二个锚点之外再走出同样的距离后平掉整套仓位。

交易逻辑

  • 初始对冲: 当没有持仓时,策略会同时发送买入和卖出市价单,手数为 StartVolume。第一笔成交价格被记作参考价格,用于后续所有判断。
  • 步长监控: StepPips 参数会根据品种最小报价单位转换为价格偏移,并自动考虑三位和五位小数的外汇报价。来自 Level 1 的最佳买价和卖价与该偏移进行比较。
  • 加仓订单: 如果最佳买价较第一次成交价至少上涨一个步长,则发送两倍基础手数的卖单;如果最佳卖价较第一次成交价至少下跌一个步长,则发送同样手数的买单。该订单的第一笔成交确定第二个锚点。
  • 循环结束: 第二个锚点建立之后,价格再向任意方向走出一个步长即触发平仓,策略会一次性平掉所有多头和空头头寸。完成平仓后内部状态被重置,等待下一轮循环。
  • 手数校验: 策略启动时会检查基础手数及其两倍是否符合品种的最小、最大及步长限制,确保所有提交的订单都可被执行。

入场条件

多头加仓

  • 初始对冲的仓位仍然存在。
  • 第二锚点尚未生成。
  • 当前最佳卖价小于或等于 第一次成交价 - StepPips_折算后的价格

空头加仓

  • 初始对冲的仓位仍然存在。
  • 第二锚点尚未生成。
  • 当前最佳买价大于或等于 第一次成交价 + StepPips_折算后的价格

离场管理

  • 平掉篮子: 当第二锚点激活后,如果最佳买价超过 第二锚点 + StepOffset,或最佳卖价跌破 第二锚点 - StepOffset,策略会发送市价单来关闭全部多头和空头敞口。平仓订单会被跟踪,只有在成交确认后才会重置状态。
  • 状态重置: 在多空两侧全部关闭且没有未完成的平仓订单后,内部锚点被清空,策略重新等待新的对冲开仓。

数据与指标

  • 订阅 Level 1 可获得最佳买卖价,用于比较是否达到步长。
  • 策略不依赖任何额外指标,完全基于行情报价运行。
  • 点值转换沿用 MetaTrader 中 point 到 pip 的处理方式,使三位和五位小数的外汇品种表现与原策略一致。

参数

参数 说明
StartVolume 形成初始对冲时买单与卖单的手数。
StepPips 触发加仓以及退出篮子所需的点数距离。

实现细节

  • StockSharp 对单个证券只维护净仓位。为模拟 MetaTrader 中互不抵消的多空单,策略内部记录多头与空头的累积成交量,并在退出时按该数量发送市价单。
  • 策略依赖实时价差,因此在回测和实盘中都需要提供 Level 1 数据。如果缺少最佳买卖价,交易循环会停止。
  • 请确认交易账户支持同一品种的多空同时持仓,否则在退出条件触发之前无法维持所需的对冲结构。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Tunnel strategy that uses Bollinger Bands to define a price channel.
/// Buys when price crosses above the lower band (reversal from oversold),
/// sells when price crosses below the upper band (reversal from overbought).
/// </summary>
public class TunnelGen4Strategy : Strategy
{
	private readonly StrategyParam<int> _bbLength;
	private readonly StrategyParam<decimal> _bbWidth;
	private readonly StrategyParam<decimal> _stepPips;

	private BollingerBands _bb;

	private decimal _prevClose;
	private decimal _prevUpper;
	private decimal _prevLower;
	private decimal _entryPrice;

	/// <summary>
	/// Bollinger Bands period length.
	/// </summary>
	public int BbLength
	{
		get => _bbLength.Value;
		set => _bbLength.Value = value;
	}

	/// <summary>
	/// Bollinger Bands width (standard deviations).
	/// </summary>
	public decimal BbWidth
	{
		get => _bbWidth.Value;
		set => _bbWidth.Value = value;
	}

	/// <summary>
	/// Step distance expressed in pips for profit target.
	/// </summary>
	public decimal StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public TunnelGen4Strategy()
	{
		_bbLength = Param(nameof(BbLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("BB Length", "Bollinger Bands period", "Indicator");

		_bbWidth = Param(nameof(BbWidth), 2.0m)
			.SetGreaterThanZero()
			.SetDisplay("BB Width", "Bollinger Bands width", "Indicator");

		_stepPips = Param(nameof(StepPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between tunnel anchors", "Trading");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_bb = null;
		_prevClose = 0;
		_prevUpper = 0;
		_prevLower = 0;
		_entryPrice = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_bb = new BollingerBands
		{
			Length = BbLength,
			Width = BbWidth
		};

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.BindEx(_bb, OnProcess);
		subscription.Start();
	}

	private void OnProcess(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var bb = (BollingerBandsValue)value;
		if (bb.UpBand is not decimal upper ||
			bb.LowBand is not decimal lower)
			return;

		if (!_bb.IsFormed)
		{
			_prevClose = candle.ClosePrice;
			_prevUpper = upper;
			_prevLower = lower;
			return;
		}

		var close = candle.ClosePrice;

		// Buy signal: price crosses above lower band from below
		if (_prevClose < _prevLower && close >= lower && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
		}
		// Sell signal: price crosses below upper band from above
		else if (_prevClose > _prevUpper && close <= upper && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
		}

		// Exit on profit target if in position
		if (Position > 0 && _entryPrice > 0)
		{
			var pipValue = Security?.PriceStep ?? 1m;
			var target = _entryPrice + StepPips * pipValue;
			if (close >= target)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			var pipValue = Security?.PriceStep ?? 1m;
			var target = _entryPrice - StepPips * pipValue;
			if (close <= target)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		_prevClose = close;
		_prevUpper = upper;
		_prevLower = lower;
	}
}