在 GitHub 上查看

AH HM MFI 策略

摘要

AH HM MFI 策略结合 Money Flow Index(MFI)指标来交易锤头(Hammer)与上吊线(Hanging Man)形态。当市场在短期下跌后出现锤头且 MFI 低于超卖阈值时,策略开多单;当上涨行情中形成上吊线且 MFI 高于超买阈值时,策略开空单。MFI 穿越预设的上、下限时会触发离场。

核心逻辑

  1. 订阅指定周期的 K 线,并计算两个指标:
    • Money Flow Index,可调周期(默认 47)。
    • 简单移动平均线(SMA),用于模拟原始 MQL 策略中的趋势过滤器(默认周期 5)。
  2. 识别 锤头 与 上吊线 形态:
    • K 线实体位于全长的上三分之一。
    • 下影线相对实体较长。
    • 与前一根 K 线相比存在顺势跳空。
    • 使用前一根 K 线的中点与移动平均线对比来确认趋势方向。
  3. 利用 MFI 阈值确认进场:
    • 当检测到锤头且 MFI 小于等于设定的超卖阈值(默认 40)时买入。
    • 当出现上吊线且 MFI 大于等于设定的超买阈值(默认 60)时卖出。
  4. 根据 MFI 穿越情况管理持仓:
    • MFI 向上突破下限或上限(默认 30、70)时平掉空单。
    • MFI 向上突破上限或向下跌破下限时平掉多单。
  5. 启动内置的 StartProtection 风险保护,处理异常情况。

参数

名称 说明 默认值
CandleType 用于形态识别的 K 线类型与周期。 30 分钟 K 线
MfiPeriod MFI 指标的计算周期。 47
MaPeriod 用于趋势过滤的 SMA 周期。 5
HammerEntryThreshold 锤头进场允许的最大 MFI 值。 40
HangingEntryThreshold 上吊线进场需要的最小 MFI 值。 60
MfiUpperExitLevel MFI 上限,向上突破时关闭仓位。 70
MfiLowerExitLevel MFI 下限:向下突破时平多,向上突破时平空。 30

说明

  • 策略只处理已完成的 K 线,避免对未收盘数据做出决策。
  • 对锤头与上吊线的判定较为严格,需要长下影线与贴近最高价的实体,从而减少噪声。
  • SMA 代替原始 MetaTrader 5 专家顾问中的 CloseAvg 过滤器,确保信号顺应主要趋势。
namespace StockSharp.Samples.Strategies;

using System;

using Ecng.Common;

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

/// <summary>
/// Hammer/hanging-man lower-shadow patterns confirmed by previous SMA trend and MFI.
/// </summary>
public class AhHmMfiStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _mfiPeriod;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _hammerEntryThreshold;
	private readonly StrategyParam<decimal> _hangingEntryThreshold;
	private readonly StrategyParam<decimal> _mfiUpperExitLevel;
	private readonly StrategyParam<decimal> _mfiLowerExitLevel;

	private MoneyFlowIndex _mfi;
	private SimpleMovingAverage _ma;
	private ICandleMessage _previousCandle;
	private decimal? _previousMa;
	private decimal? _previousMfi;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int MfiPeriod { get => _mfiPeriod.Value; set => _mfiPeriod.Value = value; }
	public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
	public decimal HammerEntryThreshold { get => _hammerEntryThreshold.Value; set => _hammerEntryThreshold.Value = value; }
	public decimal HangingEntryThreshold { get => _hangingEntryThreshold.Value; set => _hangingEntryThreshold.Value = value; }
	public decimal MfiUpperExitLevel { get => _mfiUpperExitLevel.Value; set => _mfiUpperExitLevel.Value = value; }
	public decimal MfiLowerExitLevel { get => _mfiLowerExitLevel.Value; set => _mfiLowerExitLevel.Value = value; }

	public AhHmMfiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame());
		_mfiPeriod = Param(nameof(MfiPeriod), 47).SetGreaterThanZero();
		_maPeriod = Param(nameof(MaPeriod), 5).SetGreaterThanZero();
		_hammerEntryThreshold = Param(nameof(HammerEntryThreshold), 40m);
		_hangingEntryThreshold = Param(nameof(HangingEntryThreshold), 60m);
		_mfiUpperExitLevel = Param(nameof(MfiUpperExitLevel), 70m);
		_mfiLowerExitLevel = Param(nameof(MfiLowerExitLevel), 30m);
	}

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

	private void ResetRuntime()
	{
		_previousCandle = null;
		_previousMa = null;
		_previousMfi = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ResetRuntime();
		_mfi = new MoneyFlowIndex { Length = MfiPeriod };
		_ma = new SimpleMovingAverage { Length = MaPeriod };
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var mfiValue = _mfi.Process(candle);
		var maValue = _ma.Process(candle.ClosePrice, candle.OpenTime, true);

		if (!mfiValue.IsFormed || !maValue.IsFormed)
		{
			Remember(candle, maValue.IsFormed ? maValue.ToDecimal() : null,
				mfiValue.IsFormed ? mfiValue.ToDecimal() : null);
			return;
		}

		var mfi = mfiValue.ToDecimal();
		var ma = maValue.ToDecimal();
		if (_previousMfi is decimal previousMfi)
		{
			if (Position > 0m &&
				((previousMfi <= MfiUpperExitLevel && mfi > MfiUpperExitLevel) ||
				 (previousMfi >= MfiLowerExitLevel && mfi < MfiLowerExitLevel)))
			{
				ClosePosition();
				Remember(candle, ma, mfi);
				return;
			}

			if (Position < 0m &&
				((previousMfi <= MfiLowerExitLevel && mfi > MfiLowerExitLevel) ||
				 (previousMfi <= MfiUpperExitLevel && mfi > MfiUpperExitLevel)))
			{
				ClosePosition();
				Remember(candle, ma, mfi);
				return;
			}
		}

		if (Position == 0m && _previousCandle is { } previous && _previousMa is decimal previousMa)
		{
			var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
			var range = candle.HighPrice - candle.LowPrice;
			if (body > 0m && range > 0m)
			{
				var upperShadow = candle.HighPrice - Math.Max(candle.OpenPrice, candle.ClosePrice);
				var lowerShadow = Math.Min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice;
				var bodyInUpperThird = Math.Min(candle.OpenPrice, candle.ClosePrice) >= candle.HighPrice - range / 3m;
				var lowerTailPattern = bodyInUpperThird && lowerShadow >= body * 2.5m && upperShadow <= body * 0.5m;
				var previousMidpoint = (previous.OpenPrice + previous.ClosePrice) / 2m;

				if (lowerTailPattern && candle.OpenPrice < previous.ClosePrice &&
					previousMidpoint < previousMa && mfi <= HammerEntryThreshold)
					BuyMarket();
				else if (lowerTailPattern && candle.OpenPrice > previous.ClosePrice &&
					previousMidpoint > previousMa && mfi >= HangingEntryThreshold)
					SellMarket();
			}
		}

		Remember(candle, ma, mfi);
	}

	private void Remember(ICandleMessage candle, decimal? ma, decimal? mfi)
	{
		_previousCandle = candle;
		_previousMa = ma;
		_previousMfi = mfi;
	}
}