在 GitHub 上查看

看涨与看跌吞没策略

概述

该策略重现了 MetaTrader 平台上 "Bullish and Bearish Engulfing" 专家顾问的经典看涨/看跌吞没形态。StockSharp 版本在可配置的时间框架上读取已完成的 K 线,根据设定的位移跳过最新的若干根 K 线,并在吞没形态满足最小距离过滤条件时做出反应。该实现适合希望自动化经典价格行为形态、但仍要自主控制交易方向、下单手数以及持仓处理方式的交易者。

形态定义

在应用位移参数之后,两根连续的完成 K 线必须同时满足以下条件才会触发吞没信号:

  • 看涨吞没
    • 当前评估的 K 线收盘价高于开盘价(实体为阳线)。
    • 前一根 K 线收盘价低于开盘价(实体为阴线)。
    • 当前阳线的最高价高于前一根最高价、最低价低于前一根最低价,且超出距离不少于过滤阈值。
    • 当前阳线的收盘价高于前一根开盘价,且其开盘价低于前一根收盘价,同样满足距离阈值。
  • 看跌吞没
    • 当前评估的 K 线收盘价低于开盘价(实体为阴线)。
    • 前一根 K 线收盘价高于开盘价(实体为阳线)。
    • 当前阴线仍然创出更高的最高价,但收盘价显著低于前一根开盘价,同时开盘价高于前一根收盘价,两项条件都满足距离阈值。
    • 当前阴线的最低价低于前一根最低价,且超出距离阈值。

上述规则完整复现了原始 MetaTrader 代码的判断方式:吞没 K 线不仅要完全覆盖前一根 K 线实体,还必须向上、向下同时突破。距离过滤以“点”/“pips”为单位,根据标的的价格最小变动和小数位自动换算成价格值(对 5 位和 3 位报价的外汇品种会自动按 10 个基点进行放大)。

交易逻辑

  1. 通过高层 API 订阅所选的 K 线类型,只处理状态为完成的 K 线。
  2. 维护一个简短的滑动窗口,保存当前位移参数所需的 OHLC 值。
  3. 当缓存中至少包含两根可评估的历史 K 线时,检查上述看涨/看跌吞没条件。
  4. 看涨信号触发时,根据 BullishSide 参数指定的方向发送市价单;看跌信号则使用 BearishSide 参数设定的方向。
  5. 如果开启 CloseOppositePositions,且当前持有相反方向仓位,策略会在下单量中加入现有仓位的绝对值,从而先行平掉对冲头寸,再开立新的方向;关闭该选项时,若存在反向仓位,则直接忽略信号。
  6. 下单手数由 Volume 参数控制(默认 1 手/份额)。策略不会自动附加止损或止盈,风险管理应由用户自行配置或结合 StockSharp 的保护模块完成。

参数

参数 说明 默认值 备注
CandleType 用于识别信号的 K 线类型(StockSharp DataType)。 1 小时 K 线 可调整为任意受支持的时间框架。
Shift 在评估吞没形态前需要跳过的已完成 K 线数量。 1 取值 1 表示检查最新完成的 K 线,更大的值会往回看更久。
DistanceInPips 吞没 K 线相对前一根必须超出的最小距离(以 pips 表示)。 0 根据标的的价格步长换算成价格差,可用来过滤实体很小的 K 线。
CloseOppositePositions 信号触发时是否先平掉反向持仓。 true 关闭时若存在反向持仓则不执行新交易。
BullishSide 看涨吞没信号执行的订单方向。 Buy 可改为 Sell 以实现逆势做法。
BearishSide 看跌吞没信号执行的订单方向。 Sell 可改为 Buy 以进行逆势交易。
Volume 基础下单手数。 1 当需要平掉反向仓位时,会在下单量中加上当前仓位绝对值。

仓位与风险控制

  • 策略使用市价单且默认不带止损/止盈,建议结合 StartProtection 等保护功能或外部风险控制措施使用。
  • 原始 MetaTrader 版本使用按风险百分比计算手数的资金管理模块。本移植版本改为直接使用固定手数参数,以便在 StockSharp 中保持确定性。如需动态仓位管理,可按需扩展自定义模块。
  • CloseOppositePositions 设为 true 时,反向信号会直接反手:下单量等于基础手数加上当前仓位的绝对值,保证从原方向平仓并立即建立新方向。

文件结构

  • CS/BullishBearishEngulfingStrategy.cs – 基于 StockSharp 高层策略 API 的 C# 实现。

提示: 按需求仅提供 C# 版本,本目录中没有 Python 实现或 Python 子目录。

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>
/// Engulfing pattern strategy that reacts to bullish and bearish engulfing candles.
/// </summary>
public class BullishBearishEngulfingStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<decimal> _distanceInPips;
	private readonly StrategyParam<bool> _closeOpposite;
	private readonly StrategyParam<Sides> _bullishSide;
	private readonly StrategyParam<Sides> _bearishSide;
	private readonly List<CandleSnapshot> _candles = new();

	/// <summary>
	/// Initializes a new instance of <see cref="BullishBearishEngulfingStrategy"/>.
	/// </summary>
	public BullishBearishEngulfingStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for analysis", "General");

		_shift = Param(nameof(Shift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift", "Number of completed candles to skip", "Pattern")
			
			.SetOptimize(1, 5, 1);

		_distanceInPips = Param(nameof(DistanceInPips), 0m)
			.SetNotNegative()
			.SetDisplay("Distance (pips)", "Additional filter expressed in pips", "Pattern")
			
			.SetOptimize(0m, 10m, 1m);

		_closeOpposite = Param(nameof(CloseOppositePositions), true)
			.SetDisplay("Close Opposite", "Close opposite position before entering", "Risk");

		_bullishSide = Param(nameof(BullishSide), Sides.Buy)
			.SetDisplay("Bullish Action", "Order side for bullish engulfing", "Pattern");

		_bearishSide = Param(nameof(BearishSide), Sides.Sell)
			.SetDisplay("Bearish Action", "Order side for bearish engulfing", "Pattern");
	}

	/// <summary>
	/// Candle type used for pattern detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of fully completed candles to skip before pattern evaluation.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// Additional price filter expressed in pips.
	/// </summary>
	public decimal DistanceInPips
	{
		get => _distanceInPips.Value;
		set => _distanceInPips.Value = value;
	}

	/// <summary>
	/// Indicates whether opposite positions should be closed before entering a new trade.
	/// </summary>
	public bool CloseOppositePositions
	{
		get => _closeOpposite.Value;
		set => _closeOpposite.Value = value;
	}

	/// <summary>
	/// Side used when a bullish engulfing pattern appears.
	/// </summary>
	public Sides BullishSide
	{
		get => _bullishSide.Value;
		set => _bullishSide.Value = value;
	}

	/// <summary>
	/// Side used when a bearish engulfing pattern appears.
	/// </summary>
	public Sides BearishSide
	{
		get => _bearishSide.Value;
		set => _bearishSide.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

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

		_candles.Clear();

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		// no protection needed
	}

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

		var snapshot = new CandleSnapshot
		{
			Open = candle.OpenPrice,
			High = candle.HighPrice,
			Low = candle.LowPrice,
			Close = candle.ClosePrice
		};

		_candles.Add(snapshot);

		var maxCount = Math.Max(Shift + 2, 3);
		while (_candles.Count > maxCount)
			try { _candles.RemoveAt(0); } catch { break; }

		if (_candles.Count < Shift + 1)
			return;

		var candles = _candles.ToArray();
		var currentIndex = candles.Length - Shift;
		if (currentIndex <= 0)
			return;

		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = candles[currentIndex];
		var previous = candles[previousIndex];
		var distance = CalculateDistanceInPrice();

		var isBullishEngulfing = current.Close > current.Open && previous.Open > previous.Close &&
			current.High > previous.High + distance &&
			current.Close > previous.Open + distance &&
			current.Open < previous.Close - distance &&
			current.Low < previous.Low - distance;

		if (isBullishEngulfing)
		{
			HandleSignal(BullishSide);
			return;
		}

		var isBearishEngulfing = current.Open > current.Close && previous.Open < previous.Close &&
			current.High > previous.High + distance &&
			current.Open > previous.Close + distance &&
			current.Close < previous.Open - distance &&
			current.Low < previous.Low - distance;

		if (isBearishEngulfing)
			HandleSignal(BearishSide);
	}

	private void HandleSignal(Sides side)
	{
		switch (side)
		{
			case Sides.Buy:
				EnterLong();
				break;
			case Sides.Sell:
				EnterShort();
				break;
		}
	}

	private void EnterLong()
	{
		if (Position > 0)
			return;

		if (Position < 0)
		{
			if (!CloseOppositePositions)
				return;

			// Close short first
			BuyMarket();
		}

		BuyMarket();
	}

	private void EnterShort()
	{
		if (Position < 0)
			return;

		if (Position > 0)
		{
			if (!CloseOppositePositions)
				return;

			// Close long first
			SellMarket();
		}

		SellMarket();
	}

	private decimal CalculateDistanceInPrice()
	{
		var priceStep = Security?.PriceStep;
		if (priceStep == null)
			return 0m;

		var decimals = Security?.Decimals ?? 0;
		var multiplier = decimals is 3 or 5 ? 10m : 1m;
		return DistanceInPips * priceStep.Value * multiplier;
	}

	private struct CandleSnapshot
	{
		public decimal Open;
		public decimal High;
		public decimal Low;
		public decimal Close;
	}
}