Pinbar Reversal Strategy
Pinbars highlight sudden rejections of price and can signal short-term turning points. This strategy measures the length of the candle's tail relative to its body, looking for long shadows that stick out from recent price action. A moving average filter helps trade in the direction of the underlying trend.
Testing indicates an average annual return of about 82%. It performs best in the stocks market.
During each candle update the system calculates upper and lower shadows and compares them to the body size. A bullish pinbar with a long lower wick may trigger a long entry if price is above the moving average. Likewise a bearish pinbar with an extended upper tail can initiate a short position in a downtrend. Stops are placed a fixed percentage from entry.
The trade is closed when an opposite pinbar appears against the open position or the protective stop is reached. Combining the pinbar logic with a trend filter improves reliability by avoiding countertrend setups.
Details
- Entry Criteria: Pinbar with long tail and small opposite shadow, confirmed by trend.
- Long/Short: Both.
- Exit Criteria: Opposite pinbar or stop-loss.
- Stops: Yes, percentage based.
- Default Values:
TailToBodyRatio = 2
OppositeTailRatio = 0.5
MAPeriod = 20
CandleType = 15 minute
StopLossPercent = 1
- Filters:
- Category: Pattern
- Direction: Both
- Indicators: Candlestick, MA
- 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>
/// Enters in the SMA-aligned direction of a formed pinbar and exits on the opposite pinbar or stop.
/// </summary>
public class PinbarReversalStrategy : Strategy
{
private readonly StrategyParam<decimal> _tailToBodyRatio;
private readonly StrategyParam<decimal> _oppositeTailRatio;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPercent;
private Order _pendingOrder;
public decimal TailToBodyRatio { get => _tailToBodyRatio.Value; set => _tailToBodyRatio.Value = value; }
public decimal OppositeTailRatio { get => _oppositeTailRatio.Value; set => _oppositeTailRatio.Value = value; }
public int MAPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
public PinbarReversalStrategy()
{
_tailToBodyRatio = Param(nameof(TailToBodyRatio), 2m).SetRange(1m, 10m)
.SetDisplay("Tail/Body Ratio", "Minimum dominant shadow relative to the body", "Pattern");
_oppositeTailRatio = Param(nameof(OppositeTailRatio), 0.5m).SetRange(0m, 2m)
.SetDisplay("Opposite Tail Ratio", "Maximum opposite shadow relative to the body", "Pattern");
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Close SMA trend filter", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Pinbar and MA timeframe", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 1m).SetNotNegative()
.SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pendingOrder = null;
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var sma = new SimpleMovingAverage { Length = MAPeriod };
var candles = SubscribeCandles(CandleType);
candles.Bind(sma, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candles);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native actual-fill protection evaluates executable quotes between signal candles.
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished || !IsFormedAndOnlineAndAllowTrading() ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
if (body <= 0m)
return;
var lower = Math.Min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice;
var upper = candle.HighPrice - Math.Max(candle.OpenPrice, candle.ClosePrice);
var bullish = lower >= body * TailToBodyRatio && upper <= body * OppositeTailRatio;
var bearish = upper >= body * TailToBodyRatio && lower <= body * OppositeTailRatio;
if (Position > 0m && bearish)
SellMarket(Position);
else if (Position < 0m && bullish)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && bullish && candle.ClosePrice > smaValue)
BuyMarket(Volume);
else if (Position == 0m && bearish && candle.ClosePrice < smaValue)
SellMarket(Volume);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class pinbar_reversal_strategy(Strategy):
"""
Enters in the SMA-aligned direction of a pinbar and exits on the opposite pinbar or stop.
"""
def __init__(self):
super(pinbar_reversal_strategy, self).__init__()
self._tail_to_body_ratio = self.Param("TailToBodyRatio", 2.0).SetRange(1.0, 10.0).SetDisplay("Tail/Body Ratio", "Minimum dominant shadow relative to the body", "Pattern")
self._opposite_tail_ratio = self.Param("OppositeTailRatio", 0.5).SetRange(0.0, 2.0).SetDisplay("Opposite Tail Ratio", "Maximum opposite shadow relative to the body", "Pattern")
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Close SMA trend filter", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Pinbar and MA timeframe", "General")
self._stop_loss_percent = self.Param("StopLossPercent", 1.0).SetNotNegative().SetDisplay("Stop Loss (%)", "Actual-fill percent stop; zero disables it.", "Protection")
self._pending_order = None
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def OnReseted(self):
super(pinbar_reversal_strategy, self).OnReseted()
self._pending_order = None
def OnStarted2(self, time):
super(pinbar_reversal_strategy, self).OnStarted2(time)
self._pending_order = None
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
candles = self.SubscribeCandles(self.candle_type)
candles.Bind(sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native actual-fill protection evaluates executable quotes between signal candles.
pass
def _process_candle(self, candle, sma):
if candle.State != CandleStates.Finished or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
body = Math.Abs(candle.ClosePrice - candle.OpenPrice)
if body <= Decimal(0):
return
lower = Math.Min(candle.OpenPrice, candle.ClosePrice) - candle.LowPrice
upper = candle.HighPrice - Math.Max(candle.OpenPrice, candle.ClosePrice)
tail_ratio = Decimal(self._tail_to_body_ratio.Value)
opposite_ratio = Decimal(self._opposite_tail_ratio.Value)
bullish = lower >= body * tail_ratio and upper <= body * opposite_ratio
bearish = upper >= body * tail_ratio and lower <= body * opposite_ratio
if self.Position > 0 and bearish:
self.SellMarket(self.Position)
elif self.Position < 0 and bullish:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and bullish and candle.ClosePrice > sma:
self.BuyMarket(self.Volume)
elif self.Position == 0 and bearish and candle.ClosePrice < sma:
self.SellMarket(self.Volume)
def CreateClone(self):
return pinbar_reversal_strategy()