在 GitHub 上查看

VHF 滑动窗口策略

概述

  • 根据 Vladimir Karputov 编写的 MetaTrader 5 专家顾问 “VHF EA” 改写。
  • 通过垂直水平过滤器(Vertical Horizontal Filter,VHF)识别趋势行情与震荡行情。
  • 适用于 StockSharp 支持的任意品种与周期,可通过参数调整所订阅的蜡烛类型。

交易逻辑

  1. 订阅选定周期的蜡烛数据,在每根收盘蜡烛上计算周期为 VhfPeriod 的 VHF 指标。
  2. 维护两组滑动窗口以跟踪最新的 VHF 数值:
    • 主窗口 (MainWindowSize):刻画较长区间内的 VHF 极值与中点。
    • 工作窗口 (WorkingWindowSize):捕捉短期内 VHF 相对高低的突破。
  3. 当且仅当当前 VHF 同时高于两组窗口的中点时,才视为进入趋势行情。
  4. 在趋势行情中,将最新收盘价与 MainWindowSize 根之前的收盘价进行比较:
    • 当前收盘价更高 → 默认开仓或持有多头;
    • 当前收盘价更低 → 默认开仓或持有空头;
    • 设置 ReverseSignals 可反向执行上述方向。
  5. 一旦 VHF 回落到中点以下,即判定重新进入震荡区间,策略会平掉所有持仓。
  6. 当方向翻转时,会一次性提交足够的市价委托,同时平掉旧仓位并建立新的反向仓位。

参数

参数 说明 默认值 备注
MainWindowSize 主窗口内的 VHF 数量。 11 必须大于 WorkingWindowSize
WorkingWindowSize 工作窗口内的 VHF 数量。 7 用于快速确认突破。
VhfPeriod VHF 指标的计算周期。 9 周期越小越敏感。
Volume 每次下单的手数。 1 方向翻转时会自动加上当前仓位的绝对值。
ReverseSignals 是否反向执行多空逻辑。 true 保持与原版 EA 相同的默认设置。
CandleType 使用的蜡烛周期/类型。 15 分钟 根据需要修改为其他周期。

仓位管理

  • 始终以固定手数 Volume 下单,不包含止损或止盈。
  • 调用了 StartProtection() 保护机制,确保在平台出现异常持仓时能够自动平仓。
  • 退出完全依赖 VHF 对行情状态的判断,不额外计算风险报酬比。

实现细节

  • 按照项目规范使用高级 API:通过蜡烛订阅与 Bind 方法连接指标。
  • 在策略内部实现了与 MQL 版本一致的 VHF 指标算法。
  • 日志会详细记录趋势切换与持仓变化,便于测试与回溯。
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;

public class VhfSlidingWindowsStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public VhfSlidingWindowsStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}