在 GitHub 上查看

Bull Row Breakout 策略

概览

Bull Row Breakout 策略是 MetaTrader 5 专家顾问 “BULL row full EA” 的 C# 版本。原策略使用模块化构建,结合蜡烛排列和动量确认。移植到 StockSharp 后,我们在单一可配置的时间框架上复现相同逻辑,并按照仓库要求将代码注释保持为英文。

当一组看跌蜡烛被看涨动量和向上突破取代时,策略只做多头。Stochastic 随机指标用于过滤动量,而动态止损与止盈重建了 MQL 版本的风险设置。

入场流程

  1. 仅在新蜡烛收盘时评估信号(“每根柱子一次”)。
  2. 如果当前没有多头仓位,继续检测条件。
  3. 看跌排列:
    • BearShift 开始向前数 BearRowSize 根蜡烛必须全部收阴。
    • 每根蜡烛实体至少达到 BearMinBody 个价格步长。
    • 实体变化需符合 BearRowMode(普通 / 逐渐增大 / 逐渐减小)。
  4. 看涨排列:
    • BullShift 开始向前数 BullRowSize 根蜡烛必须全部收阳。
    • 每根蜡烛实体至少达到 BullMinBody 个价格步长。
    • 实体变化需符合 BullRowMode
  5. 突破确认:最近收盘价要高于第 2 根到第 BreakoutLookback 根历史蜡烛的最高价。
  6. 随机指标确认:
    • 当前 %K(StochasticKPeriod)必须高于 %D(StochasticDPeriod)。
    • 过去 StochasticRangePeriod 个 %K 值全部位于 StochasticLowerLevelStochasticUpperLevel 之间。
  7. 风险控制:
    • 止损价格取自最近 StopLossLookback 根蜡烛的最低价。
    • 止盈价格等于止损距离的 TakeProfitPercent%。
    • 每根蜡烛收盘时检测是否触发止损或止盈,若触发则用 SellMarket 平仓。

参数说明

参数 说明
Volume 每次入场使用的固定交易量。
CandleTimeFrame 参与计算的蜡烛时间框架。
StopLossLookback 计算动态止损所使用的历史蜡烛数量。
TakeProfitPercent 止盈相对于止损距离的百分比。
BearRowSizeBearMinBodyBearRowModeBearShift 看跌排列设置。
BullRowSizeBullMinBodyBullRowModeBullShift 看涨排列设置。
BreakoutLookback 用于突破确认的最高价回溯长度。
StochasticKPeriodStochasticDPeriodStochasticSlowing 随机指标参数。
StochasticRangePeriod 需要保持在通道内的 %K 历史值数量。
StochasticUpperLevelStochasticLowerLevel %K 通道上下界。

蜡烛实体长度以价格步长表示,对应原版中 toDigits 的处理方式。当品种没有提供价格步长时,默认使用 1。

与 MQL 版本的差异

  • 原策略可以为每个模块选择不同的时间框架,此移植版在单一的 CandleTimeFrame 上运行,以匹配最常见的使用方式。
  • 模块化代码中的虚拟止损和挂单管理未在移植版中实现。
  • 止损与止盈通过检测蜡烛实现,一旦价格越过水平即调用 SellMarket 平仓。
  • 原有的图形对象与状态显示未移植。

使用建议

  • 根据交易品种优化蜡烛序列的长度与偏移;默认值复现了原策略的设定(向前 3 根看跌 + 向前 2 根看涨)。
  • 可通过调整 StochasticLowerLevelStochasticUpperLevel 改变过滤强度。
  • 由于止损依赖最近低点,出现跳空的市场可能需要增大 StopLossLookback 或增加额外过滤条件。
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Strategy converted from the "BULL row full EA" expert advisor.
/// </summary>
public class BullRowBreakoutStrategy : Strategy
{
	private readonly List<ICandleMessage> _candles = new();
	private readonly Queue<decimal> _stochasticHistory = new();
	private StochasticOscillator _stochastic = null!;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	private readonly StrategyParam<TimeSpan> _candleTimeFrame;
	private readonly StrategyParam<int> _stopLossLookback;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<int> _bearRowSize;
	private readonly StrategyParam<decimal> _bearMinBody;
	private readonly StrategyParam<RowSequenceModes> _bearRowMode;
	private readonly StrategyParam<int> _bearShift;
	private readonly StrategyParam<int> _bullRowSize;
	private readonly StrategyParam<decimal> _bullMinBody;
	private readonly StrategyParam<RowSequenceModes> _bullRowMode;
	private readonly StrategyParam<int> _bullShift;
	private readonly StrategyParam<int> _breakoutLookback;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<int> _stochasticRangePeriod;
	private readonly StrategyParam<decimal> _stochasticUpperLevel;
	private readonly StrategyParam<decimal> _stochasticLowerLevel;

	/// <summary>
	/// Initializes a new instance of the <see cref="BullRowBreakoutStrategy"/> class.
	/// </summary>
	public BullRowBreakoutStrategy()
	{
		_candleTimeFrame = Param(nameof(CandleTimeFrame), TimeSpan.FromMinutes(5))
		.SetDisplay("Timeframe", "Primary candle timeframe", "Market")
		;

		_stopLossLookback = Param(nameof(StopLossLookback), 10)
		.SetDisplay("Stop loss bars", "Bars used to locate protective stop", "Risk")
		;

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 100m)
		.SetDisplay("Take profit %", "Reward distance relative to stop", "Risk")
		;

		_bearRowSize = Param(nameof(BearRowSize), 3)
		.SetDisplay("Bear row size", "Required consecutive bearish candles", "Pattern")
		;

		_bearMinBody = Param(nameof(BearMinBody), 0m)
		.SetDisplay("Bear min body", "Minimum bearish candle body (price steps)", "Pattern")
		;

		_bearRowMode = Param(nameof(BearRowMode), RowSequenceModes.Normal)
		.SetDisplay("Bear row mode", "Body size progression for bearish row", "Pattern")
		;

		_bearShift = Param(nameof(BearShift), 3)
		.SetDisplay("Bear row shift", "How many bars back the bearish row starts", "Pattern")
		;

		_bullRowSize = Param(nameof(BullRowSize), 2)
		.SetDisplay("Bull row size", "Required consecutive bullish candles", "Pattern")
		;

		_bullMinBody = Param(nameof(BullMinBody), 0m)
		.SetDisplay("Bull min body", "Minimum bullish candle body (price steps)", "Pattern")
		;

		_bullRowMode = Param(nameof(BullRowMode), RowSequenceModes.Normal)
		.SetDisplay("Bull row mode", "Body size progression for bullish row", "Pattern")
		;

		_bullShift = Param(nameof(BullShift), 1)
		.SetDisplay("Bull row shift", "How many bars back the bullish row starts", "Pattern")
		;

		_breakoutLookback = Param(nameof(BreakoutLookback), 10)
		.SetDisplay("Breakout lookback", "Bars checked for the breakout filter", "Pattern")
		;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 40)
		.SetDisplay("Stochastic %K", "%K period", "Indicators")
		;

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 8)
		.SetDisplay("Stochastic %D", "%D period", "Indicators")
		;

		_stochasticSlowing = Param(nameof(StochasticSlowing), 10)
		.SetDisplay("Stochastic slowing", "Smoothing applied to %K", "Indicators")
		;

		_stochasticRangePeriod = Param(nameof(StochasticRangePeriod), 3)
		.SetDisplay("Stochastic bars", "Bars that must remain inside the oscillator channel", "Indicators")
		;

		_stochasticUpperLevel = Param(nameof(StochasticUpperLevel), 70m)
		.SetDisplay("Stochastic upper", "Upper bound for the oscillator", "Indicators")
		;

		_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 30m)
		.SetDisplay("Stochastic lower", "Lower bound for the oscillator", "Indicators")
		;
	}

	/// <summary>
	/// Primary candle timeframe.
	/// </summary>
	public TimeSpan CandleTimeFrame
	{
		get => _candleTimeFrame.Value;
		set => _candleTimeFrame.Value = value;
	}

	/// <summary>
	/// Bars used to locate the stop price.
	/// </summary>
	public int StopLossLookback
	{
		get => _stopLossLookback.Value;
		set => _stopLossLookback.Value = value;
	}

	/// <summary>
	/// Take profit distance relative to the stop in percent.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Bearish row length in candles.
	/// </summary>
	public int BearRowSize
	{
		get => _bearRowSize.Value;
		set => _bearRowSize.Value = value;
	}

	/// <summary>
	/// Minimum bearish body expressed in price steps.
	/// </summary>
	public decimal BearMinBody
	{
		get => _bearMinBody.Value;
		set => _bearMinBody.Value = value;
	}

	/// <summary>
	/// Bearish row body progression requirement.
	/// </summary>
	public RowSequenceModes BearRowMode
	{
		get => _bearRowMode.Value;
		set => _bearRowMode.Value = value;
	}

	/// <summary>
	/// Offset in bars where the bearish row starts.
	/// </summary>
	public int BearShift
	{
		get => _bearShift.Value;
		set => _bearShift.Value = value;
	}

	/// <summary>
	/// Bullish row length in candles.
	/// </summary>
	public int BullRowSize
	{
		get => _bullRowSize.Value;
		set => _bullRowSize.Value = value;
	}

	/// <summary>
	/// Minimum bullish body expressed in price steps.
	/// </summary>
	public decimal BullMinBody
	{
		get => _bullMinBody.Value;
		set => _bullMinBody.Value = value;
	}

	/// <summary>
	/// Bullish row body progression requirement.
	/// </summary>
	public RowSequenceModes BullRowMode
	{
		get => _bullRowMode.Value;
		set => _bullRowMode.Value = value;
	}

	/// <summary>
	/// Offset in bars where the bullish row starts.
	/// </summary>
	public int BullShift
	{
		get => _bullShift.Value;
		set => _bullShift.Value = value;
	}

	/// <summary>
	/// Lookback used to determine the breakout high.
	/// </summary>
	public int BreakoutLookback
	{
		get => _breakoutLookback.Value;
		set => _breakoutLookback.Value = value;
	}

	/// <summary>
	/// Stochastic %K length.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic %D length.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Additional smoothing applied to %K.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochasticSlowing.Value;
		set => _stochasticSlowing.Value = value;
	}

	/// <summary>
	/// Number of candles that must remain inside the Stochastic channel.
	/// </summary>
	public int StochasticRangePeriod
	{
		get => _stochasticRangePeriod.Value;
		set => _stochasticRangePeriod.Value = value;
	}

	/// <summary>
	/// Upper bound for the Stochastic filter.
	/// </summary>
	public decimal StochasticUpperLevel
	{
		get => _stochasticUpperLevel.Value;
		set => _stochasticUpperLevel.Value = value;
	}

	/// <summary>
	/// Lower bound for the Stochastic filter.
	/// </summary>
	public decimal StochasticLowerLevel
	{
		get => _stochasticLowerLevel.Value;
		set => _stochasticLowerLevel.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_candles.Clear();
		_stochasticHistory.Clear();
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_candles.Clear();
		_stochasticHistory.Clear();
		_stopPrice = null;
		_takeProfitPrice = null;

		_stochastic = new StochasticOscillator
		{
			K = { Length = StochasticKPeriod },
			D = { Length = StochasticDPeriod },
		};

		var subscription = SubscribeCandles(CandleTimeFrame.TimeFrame());
		subscription
		.BindEx(_stochastic, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _stochastic);
			DrawOwnTrades(area);
		}

		StartProtection(null, null);
	}

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

		var stoch = (StochasticOscillatorValue)stochasticValue;
		if (!stochasticValue.IsFinal || stoch.K is not decimal kValue || stoch.D is not decimal dValue)
		return;

		_candles.Add(candle);
		var maxNeeded = Math.Max(Math.Max(BearShift + BearRowSize - 1, BullShift + BullRowSize - 1), Math.Max(StopLossLookback, BreakoutLookback));
		if (_candles.Count > Math.Max(maxNeeded + 5, StochasticRangePeriod + 5))
		_candles.RemoveAt(0);

		_stochasticHistory.Enqueue(kValue);
		while (_stochasticHistory.Count > Math.Max(StochasticRangePeriod, 1))
		_stochasticHistory.Dequeue();

		ManageProtectiveLevels(candle);

		if (Position > 0m)
		return;

		if (!HasEnoughHistory())
		return;

		if (!HasBearRow())
		return;

		if (!HasBullRow())
		return;

		if (!HasBreakout())
		return;

		if (!HasStochasticCross(kValue, dValue))
		return;

		if (!IsStochasticContained())
		return;

		var volume = Volume;
		if (volume <= 0m)
		return;

		var stopPrice = CalculateStopPrice();
		if (stopPrice is null)
		return;

		var entryPrice = candle.ClosePrice;
		var risk = entryPrice - stopPrice.Value;
		if (risk <= 0m)
		return;

		var reward = risk * TakeProfitPercent / 100m;
		var takeProfitPrice = entryPrice + reward;

		if (BuyMarket(volume) is null)
		return;

		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
	}

	private void ManageProtectiveLevels(ICandleMessage candle)
	{
		if (Position <= 0m)
		return;

		if (_stopPrice is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Math.Abs(Position));
			ResetProtection();
			return;
		}

		if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
		{
			SellMarket(Math.Abs(Position));
			ResetProtection();
			return;
		}
	}
	private void ResetProtection()
	{
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private bool HasEnoughHistory()
	{
		if (_candles.Count < Math.Max(BreakoutLookback, StopLossLookback))
		return false;

		var bearRequirement = BearShift + BearRowSize - 1;
		var bullRequirement = BullShift + BullRowSize - 1;
		var minCandles = Math.Max(bearRequirement, bullRequirement);
		return _candles.Count >= Math.Max(minCandles, 2);
	}

	private bool HasBearRow() => HasRow(BearRowSize, BearMinBody, BearRowMode, BearShift, isBullish: false);

	private bool HasBullRow() => HasRow(BullRowSize, BullMinBody, BullRowMode, BullShift, isBullish: true);

	private bool HasRow(int size, decimal minBody, RowSequenceModes mode, int shift, bool isBullish)
	{
		if (size <= 0 || shift <= 0)
		return false;

		var maxShift = shift + size - 1;
		if (_candles.Count < maxShift)
		return false;

		var bodyStep = Security?.PriceStep ?? 0m;
		if (bodyStep <= 0m)
		bodyStep = 1m;

		var minBodyValue = minBody * bodyStep;
		decimal previousBody = 0m;

		for (var i = 0; i < size; i++)
		{
			var candle = GetCandle(shift + i);
			var body = isBullish ? candle.ClosePrice - candle.OpenPrice : candle.OpenPrice - candle.ClosePrice;

			if (body <= 0m)
			return false;

			if (body < minBodyValue)
			return false;

			if (mode == RowSequenceModes.Bigger && previousBody > 0m && body <= previousBody)
			return false;

			if (mode == RowSequenceModes.Smaller && previousBody > 0m && body >= previousBody)
			return false;

			previousBody = body;
		}

		return true;
	}

	private bool HasBreakout()
	{
		if (BreakoutLookback <= 2)
		return false;

		var prevClose = GetCandle(1).ClosePrice;
		var highest = decimal.MinValue;

		for (var shift = 2; shift <= BreakoutLookback; shift++)
		{
			var candle = GetCandle(shift);
			highest = Math.Max(highest, candle.HighPrice);
		}

		return prevClose > highest;
	}

	private bool HasStochasticCross(decimal kValue, decimal dValue)
	{
		return kValue > dValue;
	}

	private bool IsStochasticContained()
	{
		if (StochasticRangePeriod <= 0)
		return true;

		if (_stochasticHistory.Count < StochasticRangePeriod)
		return false;

		var history = _stochasticHistory.ToArray();
		return history.All(v => v <= StochasticUpperLevel && v >= StochasticLowerLevel);
	}

	private decimal? CalculateStopPrice()
	{
		if (StopLossLookback <= 0)
		return null;

		var lowest = decimal.MaxValue;
		var bars = Math.Min(StopLossLookback, _candles.Count);
		for (var shift = 1; shift <= bars; shift++)
		{
			var candle = GetCandle(shift);
			lowest = Math.Min(lowest, candle.LowPrice);
		}

		return lowest == decimal.MaxValue ? null : lowest;
	}

	private ICandleMessage GetCandle(int shift)
	{
		return _candles[^shift];
	}

	public enum RowSequenceModes
	{
		/// <summary>
		/// Only direction and minimum body size are checked.
		/// </summary>
		Normal,

		/// <summary>
		/// Each candle must have a larger body than the previous one.
		/// </summary>
		Bigger,

		/// <summary>
		/// Each candle must have a smaller body than the previous one.
		/// </summary>
		Smaller
	}
}