Doji Reversal Strategy
Doji candles reflect a temporary balance of buyers and sellers. When a doji appears after a strong directional move it can precede a reversal as momentum fades. This strategy measures the candle body relative to its range to decide if a true doji formed.
Testing indicates an average annual return of about 103%. It performs best in the stocks market.
Once a doji is detected, the previous candles are checked for an uptrend or downtrend. A doji following a decline may trigger a long entry while one after a rise can open a short. Stops are placed at a percentage distance from the entry and exits occur if price breaks beyond the doji's extremes.
The method aims to capture the first reaction away from the doji and is best suited for intraday charts where quick reversals often unfold.
Details
- Entry Criteria: Doji candle after a directional move.
- Long/Short: Both.
- Exit Criteria: Price moving beyond doji high/low or stop-loss.
- Stops: Yes, percentage based.
- Default Values:
CandleType= 5 minuteDojiThreshold= 0.1StopLossPercent= 1
- Filters:
- Category: Pattern
- Direction: Both
- Indicators: Candlestick
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Doji Reversal strategy.
/// Looks for doji candlestick patterns after a trend and takes a reversal position.
/// Doji after downtrend = buy, doji after uptrend = sell.
/// Uses SMA for exit signals.
/// </summary>
public class DojiReversalStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _dojiThreshold;
private readonly StrategyParam<int> _cooldownBars;
private ICandleMessage _bar1;
private ICandleMessage _bar2;
private int _cooldown;
/// <summary>
/// MA Period.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Doji threshold as fraction of candle range.
/// </summary>
public decimal DojiThreshold
{
get => _dojiThreshold.Value;
set => _dojiThreshold.Value = value;
}
/// <summary>
/// Cooldown bars.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public DojiReversalStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period for SMA", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_dojiThreshold = Param(nameof(DojiThreshold), 0.1m)
.SetNotNegative()
.SetDisplay("Doji Threshold", "Max body/range ratio for doji", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 500)
.SetRange(1, 1000)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bar1 = null;
_bar2 = null;
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bar1 = null;
_bar2 = null;
_cooldown = 0;
var sma = new SimpleMovingAverage { Length = MAPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
_bar1 = _bar2;
_bar2 = candle;
return;
}
if (_bar1 != null && _bar2 != null)
{
var isDoji = IsDoji(candle);
if (isDoji)
{
var isDowntrend = _bar2.ClosePrice < _bar1.ClosePrice;
var isUptrend = _bar2.ClosePrice > _bar1.ClosePrice;
if (Position == 0 && isDowntrend)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (Position == 0 && isUptrend)
{
SellMarket();
_cooldown = CooldownBars;
}
}
// Exit on SMA cross
if (Position > 0 && candle.ClosePrice < smaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (Position < 0 && candle.ClosePrice > smaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
}
_bar1 = _bar2;
_bar2 = candle;
}
private bool IsDoji(ICandleMessage candle)
{
var bodySize = Math.Abs(candle.OpenPrice - candle.ClosePrice);
var totalRange = candle.HighPrice - candle.LowPrice;
if (totalRange == 0)
return false;
return bodySize / totalRange < DojiThreshold;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class doji_reversal_strategy(Strategy):
"""
Doji Reversal strategy.
Looks for doji candlestick patterns after a trend and takes a reversal position.
Doji after downtrend = buy, doji after uptrend = sell.
Uses SMA for exit signals.
"""
def __init__(self):
super(doji_reversal_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetDisplay("MA Period", "Period for SMA", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._doji_threshold = self.Param("DojiThreshold", 0.1).SetDisplay("Doji Threshold", "Max body/range ratio for doji", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 500).SetDisplay("Cooldown Bars", "Bars to wait between trades", "General")
self._bar1 = None
self._bar2 = None
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(doji_reversal_strategy, self).OnReseted()
self._bar1 = None
self._bar2 = None
self._cooldown = 0
def OnStarted2(self, time):
super(doji_reversal_strategy, self).OnStarted2(time)
self._bar1 = None
self._bar2 = None
self._cooldown = 0
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _is_doji(self, candle):
body_size = abs(float(candle.OpenPrice) - float(candle.ClosePrice))
total_range = float(candle.HighPrice) - float(candle.LowPrice)
if total_range == 0:
return False
return body_size / total_range < float(self._doji_threshold.Value)
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
self._bar1 = self._bar2
self._bar2 = candle
return
if self._bar1 is not None and self._bar2 is not None:
is_doji = self._is_doji(candle)
if is_doji:
is_downtrend = self._bar2.ClosePrice < self._bar1.ClosePrice
is_uptrend = self._bar2.ClosePrice > self._bar1.ClosePrice
cd = self._cooldown_bars.Value
if self.Position == 0 and is_downtrend:
self.BuyMarket()
self._cooldown = cd
elif self.Position == 0 and is_uptrend:
self.SellMarket()
self._cooldown = cd
# Exit on SMA cross
sv = float(sma_val)
close = float(candle.ClosePrice)
cd = self._cooldown_bars.Value
if self.Position > 0 and close < sv:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0 and close > sv:
self.BuyMarket()
self._cooldown = cd
self._bar1 = self._bar2
self._bar2 = candle
def CreateClone(self):
return doji_reversal_strategy()