The Doji Arrows strategy converts the original MetaTrader "Doji Arrows" expert advisor into the StockSharp high-level API. The idea is to wait for a genuine doji candle and then trade a breakout of its range. A doji candle represents indecision, therefore a close above the doji high hints at bullish strength while a close below the doji low indicates bearish control.
The strategy processes only completed candles from the configured CandleType subscription.
The previous candle is analysed to determine whether it is a doji. The candle is classified as a doji when the absolute difference between the open and close is less than or equal to DojiBodyPoints multiplied by the security price step. If the parameter is set to 0, a single price step is used as tolerance which matches the strict equality check in the MQL5 version.
When the next candle closes above the doji high, the strategy sends a market buy order. When the next candle closes below the doji low, a market sell order is issued. Existing opposite positions are flattened automatically by the market order volume.
This sequence mirrors the original expert advisor that reacted once at the opening of each new bar.
Risk Management
The converted implementation keeps the protective behaviour of the MQL script:
Stop loss: StopLossPoints controls how far, in price steps, the initial stop loss is placed from the entry price. Set to zero to disable the fixed stop.
Take profit: TakeProfitPoints defines the distance to the profit target in price steps. Set to zero to skip the target.
Trailing stop: TrailingStopPoints and TrailingStepPoints reproduce the trailing mechanism. Once the trade gains more than TrailingStopPoints + TrailingStepPoints, the stop level is pulled to TrailingStopPoints away from the latest close (highest close for long, lowest close for short). Trailing is optional and activates only when TrailingStopPoints is greater than zero.
Stops and targets are evaluated on every finished candle. When any level is breached (using the candle high/low), the strategy exits the position with a market order and resets the protection state.
Parameters
Parameter
Default
Description
StopLossPoints
30
Distance of the initial stop loss in price steps.
TakeProfitPoints
90
Distance of the take profit in price steps.
TrailingStopPoints
15
Distance used by the trailing stop in price steps.
TrailingStepPoints
5
Extra profit required before the trailing stop is adjusted, in price steps.
DojiBodyPoints
1
Maximum allowed body size of the previous candle in price steps to treat it as a doji. 0 uses one price step as tolerance.
CandleType
1 hour
Candle type subscribed for signal generation.
Implementation Notes
The strategy subscribes to candles through SubscribeCandles(CandleType).Bind(ProcessCandle) and keeps only the latest completed candle in memory.
The security price step is retrieved via Security?.PriceStep. When it is unavailable, a fallback value of 1 is used so that the strategy can still operate on synthetic or historical data.
Protective levels are recalculated after every entry, and the trailing logic can create a stop even when the fixed stop loss is disabled (matching the MQL behaviour where the trailing stop could start from zero).
All actions are executed with market orders to stay aligned with the original advisor that relied on immediate market execution.
Usage Tips
Configure the Security, Portfolio and Volume properties before starting the strategy.
Adjust the point-based parameters according to the traded instrument. For instruments quoted with fractional pips, increase the values to match the broker tick size.
Combine the strategy with StockSharp risk controls or analytics modules if more advanced position sizing is required, because the conversion keeps the fixed-volume logic of the original code.
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>
/// Doji breakout strategy with optional fixed and trailing protection.
/// </summary>
public class DojiArrowsStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<decimal> _dojiBodyPoints;
private readonly StrategyParam<DataType> _candleType;
private bool _hasPreviousCandle;
private decimal _prevOpen;
private decimal _prevClose;
private decimal _prevHigh;
private decimal _prevLow;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
public decimal DojiBodyPoints
{
get => _dojiBodyPoints.Value;
set => _dojiBodyPoints.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public DojiArrowsStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Stop loss distance in price steps.", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 90m)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Take profit distance in price steps.", "Risk")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
.SetNotNegative()
.SetDisplay("Trailing Stop Points", "Trailing distance in price steps.", "Risk")
;
_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step Points", "Minimum profit before the trailing stop moves.", "Risk")
;
_dojiBodyPoints = Param(nameof(DojiBodyPoints), 1m)
.SetNotNegative()
.SetDisplay("Doji Body Points", "Maximum difference between open and close to treat the candle as a doji.", "Pattern")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for signal generation.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_hasPreviousCandle = false;
_prevOpen = 0m;
_prevClose = 0m;
_prevHigh = 0m;
_prevLow = 0m;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageActivePosition(candle);
if (!_hasPreviousCandle)
{
CachePreviousCandle(candle);
return;
}
var step = Security?.PriceStep ?? 1m;
var tolerance = DojiBodyPoints <= 0m ? step : DojiBodyPoints * step;
var bodySize = Math.Abs(_prevOpen - _prevClose);
var isDoji = bodySize <= tolerance;
var breakoutUp = isDoji && candle.ClosePrice > _prevHigh;
var breakoutDown = isDoji && candle.ClosePrice < _prevLow;
if (breakoutUp && Position == 0)
{
BuyMarket();
}
else if (breakoutDown && Position == 0)
{
SellMarket();
}
CachePreviousCandle(candle);
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position == 0)
return;
var step = Security?.PriceStep ?? 1m;
var trailingDistance = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;
var trailingStep = TrailingStepPoints > 0m ? TrailingStepPoints * step : 0m;
if (Position > 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = candle.ClosePrice - _entryPrice.Value;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice - trailingDistance;
if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
else if (Position < 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = _entryPrice.Value - candle.ClosePrice;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice + trailingDistance;
if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
}
private void InitializeProtection(decimal price, bool isLong, decimal step)
{
_entryPrice = price;
if (StopLossPoints > 0m)
{
var offset = StopLossPoints * step;
_stopPrice = isLong ? price - offset : price + offset;
}
else
{
_stopPrice = null;
}
if (TakeProfitPoints > 0m)
{
var offset = TakeProfitPoints * step;
_takePrice = isLong ? price + offset : price - offset;
}
else
{
_takePrice = null;
}
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
private void CachePreviousCandle(ICandleMessage candle)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_hasPreviousCandle = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class doji_arrows_strategy(Strategy):
def __init__(self):
super(doji_arrows_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 30.0)
self._take_profit_points = self.Param("TakeProfitPoints", 90.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 15.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 5.0)
self._doji_body_points = self.Param("DojiBodyPoints", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(doji_arrows_strategy, self).OnStarted2(time)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_active_position(candle)
if not self._has_previous_candle:
self._cache_previous_candle(candle)
return
step = self._get_price_step()
tolerance = step if self._doji_body_points.Value <= 0 else self._doji_body_points.Value * step
body_size = abs(self._prev_open - self._prev_close)
is_doji = body_size <= tolerance
breakout_up = is_doji and float(candle.ClosePrice) > self._prev_high
breakout_down = is_doji and float(candle.ClosePrice) < self._prev_low
if breakout_up and self.Position == 0:
self.BuyMarket()
self._initialize_protection(float(candle.ClosePrice), True, step)
elif breakout_down and self.Position == 0:
self.SellMarket()
self._initialize_protection(float(candle.ClosePrice), False, step)
self._cache_previous_candle(candle)
def _manage_active_position(self, candle):
if self.Position == 0:
return
step = self._get_price_step()
trailing_distance = self._trailing_stop_points.Value * step if self._trailing_stop_points.Value > 0 else 0.0
trailing_step = self._trailing_step_points.Value * step if self._trailing_step_points.Value > 0 else 0.0
if self.Position > 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = float(candle.ClosePrice) - self._entry_price
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) - trailing_distance
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
elif self.Position < 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = self._entry_price - float(candle.ClosePrice)
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) + trailing_distance
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
def _initialize_protection(self, price, is_long, step):
self._entry_price = price
if self._stop_loss_points.Value > 0:
offset = self._stop_loss_points.Value * step
self._stop_price = price - offset if is_long else price + offset
else:
self._stop_price = None
if self._take_profit_points.Value > 0:
offset = self._take_profit_points.Value * step
self._take_price = price + offset if is_long else price - offset
else:
self._take_price = None
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def _cache_previous_candle(self, candle):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_previous_candle = True
def _get_price_step(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
return step if step > 0 else 1.0
def OnReseted(self):
super(doji_arrows_strategy, self).OnReseted()
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._reset_protection()
def CreateClone(self):
return doji_arrows_strategy()