View on GitHub

Rejection Candle Strategy

A Rejection Candle forms when price probes a level but fails to hold beyond it, leaving a long wick and small body. Such candles indicate an attempt to move in one direction was firmly rejected by the market.

Testing indicates an average annual return of about 49%. It performs best in the crypto market.

The strategy enters in the opposite direction of the wick once the candle closes, expecting price to reverse back through the range.

Stops are set outside the rejected high or low to cap risk, and trades exit if momentum fails to materialize.

Details

  • Entry Criteria: pattern match
  • Long/Short: Both
  • Exit Criteria: stop-loss or opposite signal
  • Stops: Yes, percent based
  • Default Values:
    • CandleType = 15 minute
    • WickRatio = 1.5
    • StopLossPercent = 2 A rejection candle probes beyond the previous candle's low (high), closes the other way, and has a wick longer than WickRatio bodies. The stop lies StopLossPercent beyond the rejected low or high, and a close beyond it closes the position.
  • 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>
/// Rejection Candle strategy.
/// A bullish rejection probes below the previous candle's low, closes up, and has a lower wick longer than WickRatio bodies;
/// a bearish rejection mirrors it above the previous high. While flat the strategy trades against the wick.
/// The stop lies StopLossPercent beyond the rejected low or high, and a close beyond it closes the position.
/// </summary>
public class RejectionCandleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _wickRatio;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _prevCandle;
	private decimal _stopPrice;

	/// <summary>
	/// How many bodies long the rejecting wick must be.
	/// </summary>
	public decimal WickRatio
	{
		get => _wickRatio.Value;
		set => _wickRatio.Value = value;
	}

	/// <summary>
	/// Distance of the stop beyond the rejected extreme, in percent.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public RejectionCandleStrategy()
	{
		_wickRatio = Param(nameof(WickRatio), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Wick Ratio", "How many bodies long the rejecting wick must be", "Pattern");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Distance of the stop beyond the rejected extreme, in percent", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevCandle = null;
		_stopPrice = default;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevCandle = null;
		_stopPrice = default;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

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

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

		var previous = _prevCandle;
		_prevCandle = candle;

		if (previous == null || !IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close <= _stopPrice)
				SellMarket(Position);

			return;
		}

		if (Position < 0)
		{
			if (close >= _stopPrice)
				BuyMarket(-Position);

			return;
		}

		var body = Math.Abs(close - candle.OpenPrice);
		var upperWick = candle.HighPrice - Math.Max(candle.OpenPrice, close);
		var lowerWick = Math.Min(candle.OpenPrice, close) - candle.LowPrice;

		if (candle.LowPrice < previous.LowPrice && close > candle.OpenPrice && lowerWick > body * WickRatio)
		{
			BuyMarket(Volume);
			_stopPrice = candle.LowPrice * (1 - StopLossPercent / 100m);
		}
		else if (candle.HighPrice > previous.HighPrice && close < candle.OpenPrice && upperWick > body * WickRatio)
		{
			SellMarket(Volume);
			_stopPrice = candle.HighPrice * (1 + StopLossPercent / 100m);
		}
	}
}