ブルロウブレイクアウト戦略
概要
Bull Row Breakout 戦略は、MetaTrader 5 エキスパート アドバイザー「BULL row full EA」を C# に変換したものです。オリジナルのロボットはブロック コンストラクターを使用して構築され、価格変動パターンと勢いの確認を組み合わせています。 StockSharp ポートは、構成可能な単一の時間枠で同じロジックを再現し、必要に応じて取引の解説を英語で維持します。
この戦略は、一連の弱気のローソク足の後に強気の勢いが続き、最近の高値を上抜けた後、ロングのみのポジションをオープンします。 Stochastic オシレーター フィルターは勢いの強さを制御し、動的なストップロスとターゲット レベルは MQL バージョンのリスク設定を再作成します。
信号ロジック
- 新しいローソク足が閉じるまで待ちます (「バーごとに 1 回」実行)。
- 現在オープンしているロングポジションがないことを確認します。
- 弱気の行を検出します。
BearShiftバーから始まるBearRowSize回連続のローソク足は弱気である必要があります。- 各キャンドル本体は少なくとも
BearMinBodyの価格ステップである必要があります。 - 体の進行は、選択した
BearRowMode(標準 / 大きい / 小さい) を満たす必要があります。
- 強気の行を検出します。
BullShiftバーから始まるBullRowSize回連続のローソク足は強気である必要があります。- 各キャンドル本体は少なくとも
BullMinBodyの価格ステップである必要があります。 - ボディの進行は
BullRowModeを満たす必要があります。
- ブレイクアウトの確認: 最後に終了したローソク足の終値は、2 バー目から
BreakoutLookbackバー前までに記録された最高値よりも高くなければなりません。 - Stochastic 確認:
- 現在の %K (
StochasticKPeriod) は %D (StochasticDPeriod) を超えている必要があります。 - 最後の
StochasticRangePeriod%K 値は、StochasticLowerLevelからStochasticUpperLevelの間にある必要があります。
- 現在の %K (
- リスク管理:
- ストップ価格は、最後の
StopLossLookbackローソク足の中で最も低い安値です (最新の閉じたバーから始まります)。 - 利食いはストップ距離の
TakeProfitPercentパーセントに等しい距離に配置されます。 - ストップとターゲットは、閉じたキャンドルごとに監視されます。いずれかのレベルがイントラバーに達した場合、ポジションは次の更新で市場でクローズされます。
- ストップ価格は、最後の
パラメーター
| パラメータ | 説明 |
|---|---|
Volume |
各エントリーに使用される固定取引量。 |
CandleTimeFrame |
処理されたキャンドルのタイムフレーム。 |
StopLossLookback |
動的ストップ価格の計算に使用されるバーの数。 |
TakeProfitPercent |
停止距離のパーセンテージとして表される報酬距離。 |
BearRowSize, BearMinBody, BearRowMode, BearShift |
ブレイクアウトに先立つ弱気の行の構成。 |
BullRowSize, BullMinBody, BullRowMode, BullShift |
シグナルの直前にある強気の行の構成。 |
BreakoutLookback |
ブレイクアウト確認に使用されるローリングハイの長さ。 |
StochasticKPeriod, StochasticDPeriod, StochasticSlowing |
Stochastic オシレーターの設定。 |
StochasticRangePeriod |
境界内に収まる必要がある過去の Stochastic 値の数。 |
StochasticUpperLevel, StochasticLowerLevel |
%K に適用されるオシレーター チャンネル制限。 |
すべての本体サイズは、元のコードの toDigits ヘルパーを反映するために価格ステップで表されます。商品が価格ステップを提供しない場合、デフォルト値の 1 が使用されます。
MQL バージョンとの違い
- MT5 プロジェクトでは、ブロック入力に個別のタイムフレームを許可しました。 StockSharp ポートは、
CandleTimeFrameで定義された 1 つのタイムフレームで動作し、元の EA の一般的な使用法(チャートのタイムフレームでのすべてのブロック)と一致します。 - 仮想ストップと汎用ブロック ライブラリからの未決注文の処理は必要ないため、省略されます。
- 保護的なストップロスとテイクプロフィットのレベルは、ローソク足を監視し、レベルを突破したら
SellMarketでポジションを閉じることによってエミュレートされます。 - MQL 環境のログとグラフの装飾は複製されません。
使用のヒント
- 取引商品の行サイズとシフトを最適化します。デフォルト値は、元のプリセットを模倣しています (3 つのバーで開始する 3 つの弱気のローソク足と、1 バーで開始する 2 つの強気のローソク足が続きます)。
StochasticLowerLevelとStochasticUpperLevelを調整して、発振器フィルターの制限を調整します。- ストップは最近の安値に基づいているため、ギャップが大きい商品ではルックバックを広げるか、フィルターを追加する必要がある場合があります。
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
}
}
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
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class bull_row_breakout_strategy(Strategy):
def __init__(self):
super(bull_row_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._ema_period = self.Param("EmaPeriod", 20)
self._bull_row_size = self.Param("BullRowSize", 2)
self._breakout_lookback = self.Param("BreakoutLookback", 10)
self._stop_loss_pct = self.Param("StopLossPct", 2.0)
self._take_profit_pct = self.Param("TakeProfitPct", 3.0)
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def BullRowSize(self):
return self._bull_row_size.Value
@BullRowSize.setter
def BullRowSize(self, value):
self._bull_row_size.Value = value
@property
def BreakoutLookback(self):
return self._breakout_lookback.Value
@BreakoutLookback.setter
def BreakoutLookback(self, value):
self._breakout_lookback.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
def OnReseted(self):
super(bull_row_breakout_strategy, self).OnReseted()
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
def OnStarted2(self, time):
super(bull_row_breakout_strategy, self).OnStarted2(time)
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _has_bull_row(self):
size = self.BullRowSize
if len(self._candles) < size:
return False
for i in range(size):
c = self._candles[-(i + 1)]
if float(c.ClosePrice) <= float(c.OpenPrice):
return False
return True
def _has_breakout(self):
lookback = self.BreakoutLookback
if len(self._candles) < lookback + 1:
return False
prev_close = float(self._candles[-1].ClosePrice)
highest = float('-inf')
for i in range(2, min(lookback + 1, len(self._candles) + 1)):
if i <= len(self._candles):
highest = max(highest, float(self._candles[-i].HighPrice))
return prev_close > highest
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
# Check SL/TP on existing position
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.BuyMarket()
self._entry_price = 0.0
self._candles.append(candle)
max_needed = max(self.BullRowSize, self.BreakoutLookback) + 5
if len(self._candles) > max_needed:
self._candles.pop(0)
if self._has_prev:
# Buy: EMA crossover + bull row + breakout
if self._prev_close <= self._prev_ema and close > ema_val and self._has_bull_row() and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif self._prev_close >= self._prev_ema and close < ema_val and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_close = close
self._prev_ema = ema_val
self._has_prev = True
def CreateClone(self):
return bull_row_breakout_strategy()