看涨与看跌吞没策略
概述
该策略重现了 MetaTrader 平台上 "Bullish and Bearish Engulfing" 专家顾问的经典看涨/看跌吞没形态。StockSharp 版本在可配置的时间框架上读取已完成的 K 线,根据设定的位移跳过最新的若干根 K 线,并在吞没形态满足最小距离过滤条件时做出反应。该实现适合希望自动化经典价格行为形态、但仍要自主控制交易方向、下单手数以及持仓处理方式的交易者。
形态定义
在应用位移参数之后,两根连续的完成 K 线必须同时满足以下条件才会触发吞没信号:
- 看涨吞没
- 当前评估的 K 线收盘价高于开盘价(实体为阳线)。
- 前一根 K 线收盘价低于开盘价(实体为阴线)。
- 当前阳线的最高价高于前一根最高价、最低价低于前一根最低价,且超出距离不少于过滤阈值。
- 当前阳线的收盘价高于前一根开盘价,且其开盘价低于前一根收盘价,同样满足距离阈值。
- 看跌吞没
- 当前评估的 K 线收盘价低于开盘价(实体为阴线)。
- 前一根 K 线收盘价高于开盘价(实体为阳线)。
- 当前阴线仍然创出更高的最高价,但收盘价显著低于前一根开盘价,同时开盘价高于前一根收盘价,两项条件都满足距离阈值。
- 当前阴线的最低价低于前一根最低价,且超出距离阈值。
上述规则完整复现了原始 MetaTrader 代码的判断方式:吞没 K 线不仅要完全覆盖前一根 K 线实体,还必须向上、向下同时突破。距离过滤以“点”/“pips”为单位,根据标的的价格最小变动和小数位自动换算成价格值(对 5 位和 3 位报价的外汇品种会自动按 10 个基点进行放大)。
交易逻辑
- 通过高层 API 订阅所选的 K 线类型,只处理状态为完成的 K 线。
- 维护一个简短的滑动窗口,保存当前位移参数所需的 OHLC 值。
- 当缓存中至少包含两根可评估的历史 K 线时,检查上述看涨/看跌吞没条件。
- 看涨信号触发时,根据 BullishSide 参数指定的方向发送市价单;看跌信号则使用 BearishSide 参数设定的方向。
- 如果开启 CloseOppositePositions,且当前持有相反方向仓位,策略会在下单量中加入现有仓位的绝对值,从而先行平掉对冲头寸,再开立新的方向;关闭该选项时,若存在反向仓位,则直接忽略信号。
- 下单手数由 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class bullish_bearish_engulfing_strategy(Strategy):
"""Engulfing pattern strategy that reacts to bullish and bearish engulfing candles."""
def __init__(self):
super(bullish_bearish_engulfing_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame for analysis", "General")
self._shift = self.Param("Shift", 1) \
.SetGreaterThanZero() \
.SetDisplay("Shift", "Number of completed candles to skip", "Pattern")
self._distance_pips = self.Param("DistanceInPips", 0.0) \
.SetDisplay("Distance (pips)", "Additional filter in pips", "Pattern")
self._close_opposite = self.Param("CloseOpposite", True) \
.SetDisplay("Close Opposite", "Close opposite position before entering", "Risk")
self._candles = []
@property
def CandleType(self):
return self._candle_type.Value
@property
def Shift(self):
return self._shift.Value
@property
def DistanceInPips(self):
return self._distance_pips.Value
@property
def CloseOpposite(self):
return self._close_opposite.Value
def OnStarted2(self, time):
super(bullish_bearish_engulfing_strategy, self).OnStarted2(time)
self._candles = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
c = float(candle.ClosePrice)
self._candles.append((o, h, lo, c))
max_count = max(self.Shift + 2, 3)
while len(self._candles) > max_count:
self._candles.pop(0)
if len(self._candles) < self.Shift + 1:
return
idx = len(self._candles) - self.Shift
if idx <= 0:
return
prev_idx = idx - 1
if prev_idx < 0:
return
cur = self._candles[idx]
prev = self._candles[prev_idx]
dist = self._calc_distance()
# Bullish engulfing
is_bullish = (cur[3] > cur[0] and prev[0] > prev[3] and
cur[1] > prev[1] + dist and
cur[3] > prev[0] + dist and
cur[0] < prev[3] - dist and
cur[2] < prev[2] - dist)
if is_bullish:
self._enter_long()
return
# Bearish engulfing
is_bearish = (cur[0] > cur[3] and prev[0] < prev[3] and
cur[1] > prev[1] + dist and
cur[0] > prev[3] + dist and
cur[3] < prev[0] - dist and
cur[2] < prev[2] - dist)
if is_bearish:
self._enter_short()
def _enter_long(self):
if self.Position > 0:
return
if self.Position < 0:
if not self.CloseOpposite:
return
self.BuyMarket()
self.BuyMarket()
def _enter_short(self):
if self.Position < 0:
return
if self.Position > 0:
if not self.CloseOpposite:
return
self.SellMarket()
self.SellMarket()
def _calc_distance(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return 0.0
step = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
mult = 10.0 if decimals == 3 or decimals == 5 else 1.0
return float(self.DistanceInPips) * step * mult
def OnReseted(self):
super(bullish_bearish_engulfing_strategy, self).OnReseted()
self._candles = []
def CreateClone(self):
return bullish_bearish_engulfing_strategy()