The Doji Arrows Strategy is a StockSharp port of the MetaTrader expert advisor Doji_arrows_expert1.mq4. The trading idea is to detect a neutral doji candle and immediately trade the breakout that follows on the next bar. When the market prints a very small body candle (open ≈ close) and the subsequent candle closes beyond the doji high or low, the strategy interprets the move as a directional breakout and enters in that direction.
Trading logic
Signal detection window – the strategy continuously buffers the two previously completed candles. The oldest candle must be a doji, while the more recent candle confirms the breakout.
Doji definition – a candle qualifies as a doji when the absolute difference between open and close is less than or equal to DojiBodyThresholdSteps * PriceStep. With the default threshold of 1 step the bar may deviate by one tick at most.
Breakout confirmation –
Long setup: the candle following the doji closes above the doji high plus the optional BreakoutBufferSteps filter.
Short setup: the candle following the doji closes below the doji low minus the same buffer.
Single-shot signaling – the strategy remembers whether the previous bar already triggered a long or short signal and only reacts to a fresh breakout. This behaviour mirrors the original expert that generated one arrow per breakout sequence.
Order execution –
If a breakout appears against an existing opposite position the strategy first closes it, then enters in the new direction with volume Volume + |Position| to both flip and open the new trade.
In neutral state it opens a market order in the breakout direction.
Risk management
Initial stop-loss – after each entry the strategy places an internal protective level InitialStopSteps * PriceStep away from the fill price.
Fixed take-profit – exits part or all of the position when price reaches TakeProfitSteps * PriceStep from the entry.
Trailing stop – once the trade moves in favour more than TrailingStopSteps * PriceStep, the stop level is trailed candle by candle, locking in profits while allowing the move to run.
All protective calculations are done in native price steps, making the logic instrument-agnostic.
Parameters
Name
Description
Default
CandleType
Candle type/timeframe to analyse.
5-minute time frame
DojiBodyThresholdSteps
Maximum doji body expressed in price steps.
1
BreakoutBufferSteps
Extra filter above/below the doji extreme before accepting a breakout.
0
InitialStopSteps
Initial stop-loss distance from entry in steps.
20
TakeProfitSteps
Take-profit distance from entry in steps.
25
TrailingStopSteps
Trailing stop distance maintained once the trade is in profit.
10
All parameters are exposed through StrategyParam<T> making them visible in the UI and ready for optimisation.
Implementation notes
The class is built on the high-level candle subscription API (SubscribeCandles().Bind(...)) to stay in sync with the framework best practices.
State between calls is maintained with _previousCandle and _twoCandlesAgo, ensuring that only finished candles participate in decision making.
Protective levels are stored separately for long and short positions and are reset when positions close or when market data is insufficient.
Logging statements provide insight into signal detection, stop-loss and take-profit events, simplifying debugging during backtests.
Usage tips
Validate the default tick thresholds on each instrument: increase DojiBodyThresholdSteps for volatile markets where exact doji prints are rare.
Optimise BreakoutBufferSteps to filter small fake breakouts when spreads or noise are significant.
Combine the strategy with external risk overlays (portfolio stop, trading session filters) if you deploy it on multiple symbols simultaneously.
Because signals rely on completed candles, choose a candle type compatible with your desired trading horizon (e.g., 1-minute for scalping, 15-minute for swing entries).
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Doji Arrows breakout strategy.
/// Detects doji candles (small body) and trades breakout of the doji range on the next candle.
/// Uses ATR to define what constitutes a small body.
/// </summary>
public class DojiArrowsBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _dojiThreshold;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevHigh;
private decimal _prevLow;
private bool _prevWasDoji;
private bool _hasPrev;
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal DojiThreshold { get => _dojiThreshold.Value; set => _dojiThreshold.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DojiArrowsBreakoutStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "ATR period for doji detection", "Indicators");
_dojiThreshold = Param(nameof(DojiThreshold), 0.3m)
.SetDisplay("Doji Threshold", "Max body/ATR ratio for doji", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevHigh = 0m; _prevLow = 0m; _prevWasDoji = false; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
_prevWasDoji = false;
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
if (atr <= 0)
return;
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
var isDoji = body / atr < DojiThreshold;
if (_hasPrev && _prevWasDoji)
{
// Breakout above doji high
if (candle.ClosePrice > _prevHigh && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Breakout below doji low
else if (candle.ClosePrice < _prevLow && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
}
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_prevWasDoji = isDoji;
_hasPrev = true;
}
}
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 AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class doji_arrows_breakout_strategy(Strategy):
def __init__(self):
super(doji_arrows_breakout_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period for doji detection", "Indicators")
self._doji_threshold = self.Param("DojiThreshold", 0.3) \
.SetDisplay("Doji Threshold", "Max body/ATR ratio for doji", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_high = 0.0
self._prev_low = 0.0
self._prev_was_doji = False
self._has_prev = False
@property
def atr_period(self):
return self._atr_period.Value
@property
def doji_threshold(self):
return self._doji_threshold.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(doji_arrows_breakout_strategy, self).OnReseted()
self._prev_high = 0.0
self._prev_low = 0.0
self._prev_was_doji = False
self._has_prev = False
def OnStarted2(self, time):
super(doji_arrows_breakout_strategy, self).OnStarted2(time)
self._has_prev = False
self._prev_was_doji = False
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self.process_candle).Start()
def process_candle(self, candle, atr):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr)
if atr_val <= 0:
return
body = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
is_doji = body / atr_val < self.doji_threshold
if self._has_prev and self._prev_was_doji:
if float(candle.ClosePrice) > self._prev_high and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif float(candle.ClosePrice) < self._prev_low and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._prev_was_doji = is_doji
self._has_prev = True
def CreateClone(self):
return doji_arrows_breakout_strategy()