SMC Order Block Zones Strategy
The strategy identifies swing highs and lows to define premium and discount zones. A simple moving average acts as a trend filter and recent order blocks confirm entries. Trades are executed when price moves from one zone toward equilibrium with order block confirmation, using a percentage stop loss for protection.
Details
- Entry Criteria:
- Close below equilibrium but above discount zone and SMA for long trades.
- Close above equilibrium but below premium zone and SMA for short trades.
- Price must touch the respective order block level.
- Long/Short: Configurable long, short, or both.
- Exit Criteria: Opposite signal or stop loss.
- Stops: Percent stop loss.
- Default Values:
SwingHighLength= 8SwingLowLength= 8SmaLength= 50OrderBlockLength= 20StopLossPercent= 2
- Filters:
- Category: Trend and SMC
- Direction: User-defined
- Indicators: SMA, Highest, Lowest
- Stops: Yes
- Complexity: Medium
- Timeframe: Any
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// SMC Order Block Zones Strategy.
/// The swing high of SwingHighLength bars is the premium zone, the swing low of SwingLowLength bars the discount zone and
/// their midpoint the equilibrium. The bullish order block is the lowest low and the bearish order block the highest high of
/// the previous OrderBlockLength bars. A long opens when the close is between the discount zone and the equilibrium, above
/// the SMA, and the candle touched the bullish order block; a short mirrors this between the equilibrium and the premium zone
/// below the SMA after touching the bearish order block. An opposite signal closes or reverses the position and a percent stop
/// limits the loss.
/// </summary>
public class SmcOrderBlockZonesStrategy : Strategy
{
private readonly StrategyParam<int> _swingHighLength;
private readonly StrategyParam<int> _swingLowLength;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<int> _orderBlockLength;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<bool> _allowLong;
private readonly StrategyParam<bool> _allowShort;
private readonly StrategyParam<DataType> _candleType;
private Highest _swingHigh;
private Lowest _swingLow;
private Highest _blockHigh;
private Lowest _blockLow;
private decimal? _prevBlockHigh;
private decimal? _prevBlockLow;
/// <summary>
/// Bars of the swing high.
/// </summary>
public int SwingHighLength
{
get => _swingHighLength.Value;
set => _swingHighLength.Value = value;
}
/// <summary>
/// Bars of the swing low.
/// </summary>
public int SwingLowLength
{
get => _swingLowLength.Value;
set => _swingLowLength.Value = value;
}
/// <summary>
/// SMA period of the trend filter.
/// </summary>
public int SmaLength
{
get => _smaLength.Value;
set => _smaLength.Value = value;
}
/// <summary>
/// Bars searched for order blocks.
/// </summary>
public int OrderBlockLength
{
get => _orderBlockLength.Value;
set => _orderBlockLength.Value = value;
}
/// <summary>
/// Stop loss percentage from entry price.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Allow long trades.
/// </summary>
public bool AllowLong
{
get => _allowLong.Value;
set => _allowLong.Value = value;
}
/// <summary>
/// Allow short trades.
/// </summary>
public bool AllowShort
{
get => _allowShort.Value;
set => _allowShort.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public SmcOrderBlockZonesStrategy()
{
_swingHighLength = Param(nameof(SwingHighLength), 8)
.SetGreaterThanZero()
.SetDisplay("Swing High Length", "Bars of the swing high", "Zones");
_swingLowLength = Param(nameof(SwingLowLength), 8)
.SetGreaterThanZero()
.SetDisplay("Swing Low Length", "Bars of the swing low", "Zones");
_smaLength = Param(nameof(SmaLength), 50)
.SetGreaterThanZero()
.SetDisplay("SMA Length", "SMA period of the trend filter", "Indicators");
_orderBlockLength = Param(nameof(OrderBlockLength), 20)
.SetGreaterThanZero()
.SetDisplay("Order Block Length", "Bars searched for order blocks", "Zones");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_allowLong = Param(nameof(AllowLong), true)
.SetDisplay("Allow Long", "Allow long trades", "Trading");
_allowShort = Param(nameof(AllowShort), true)
.SetDisplay("Allow Short", "Allow short trades", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevBlockHigh = null;
_prevBlockLow = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevBlockHigh = null;
_prevBlockLow = null;
var sma = new SimpleMovingAverage { Length = SmaLength };
_swingHigh = new Highest { Length = SwingHighLength };
_swingLow = new Lowest { Length = SwingLowLength };
_blockHigh = new Highest { Length = OrderBlockLength };
_blockLow = new Lowest { Length = OrderBlockLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, ProcessCandle)
.Start();
if (StopLossPercent > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// The stop has to see prices between candles, not only at their close.
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 area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before this callback, also between signal bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished)
return;
var time = candle.OpenTime;
var premium = _swingHigh.Process(candle.HighPrice, time, true).ToDecimal();
var discount = _swingLow.Process(candle.LowPrice, time, true).ToDecimal();
// Order blocks are taken from the candles before this one so the current candle can touch them.
var bullishBlock = _prevBlockLow;
var bearishBlock = _prevBlockHigh;
var blockHigh = _blockHigh.Process(candle.HighPrice, time, true).ToDecimal();
var blockLow = _blockLow.Process(candle.LowPrice, time, true).ToDecimal();
if (_blockHigh.IsFormed && _blockLow.IsFormed)
{
_prevBlockHigh = blockHigh;
_prevBlockLow = blockLow;
}
if (!smaValue.IsFormed || !_swingHigh.IsFormed || !_swingLow.IsFormed || bullishBlock is not decimal bullBlock || bearishBlock is not decimal bearBlock)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var sma = smaValue.GetValue<decimal>();
var close = candle.ClosePrice;
var equilibrium = (premium + discount) / 2m;
var longSignal = close < equilibrium && close > discount && close > sma && candle.LowPrice <= bullBlock;
var shortSignal = close > equilibrium && close < premium && close < sma && candle.HighPrice >= bearBlock;
if (longSignal)
{
if (AllowLong && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (Position < 0)
BuyMarket(-Position);
}
else if (shortSignal)
{
if (AllowShort && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0)
SellMarket(Position);
}
}
}
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
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class smc_order_block_zones_strategy(Strategy):
"""
SMC Order Block Zones Strategy.
The swing high of SwingHighLength bars is the premium zone, the swing low of SwingLowLength bars the discount zone and
their midpoint the equilibrium. The bullish order block is the lowest low and the bearish order block the highest high of
the previous OrderBlockLength bars. A long opens when the close is between the discount zone and the equilibrium, above
the SMA, and the candle touched the bullish order block; a short mirrors this between the equilibrium and the premium zone
below the SMA after touching the bearish order block. An opposite signal closes or reverses the position and a percent stop
limits the loss.
"""
def __init__(self):
super(smc_order_block_zones_strategy, self).__init__()
self._swing_high_length = self.Param("SwingHighLength", 8).SetGreaterThanZero().SetDisplay("Swing High Length", "Bars of the swing high", "Zones")
self._swing_low_length = self.Param("SwingLowLength", 8).SetGreaterThanZero().SetDisplay("Swing Low Length", "Bars of the swing low", "Zones")
self._sma_length = self.Param("SmaLength", 50).SetGreaterThanZero().SetDisplay("SMA Length", "SMA period of the trend filter", "Indicators")
self._order_block_length = self.Param("OrderBlockLength", 20).SetGreaterThanZero().SetDisplay("Order Block Length", "Bars searched for order blocks", "Zones")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._allow_long = self.Param("AllowLong", True).SetDisplay("Allow Long", "Allow long trades", "Trading")
self._allow_short = self.Param("AllowShort", True).SetDisplay("Allow Short", "Allow short trades", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._swing_high = None
self._swing_low = None
self._block_high = None
self._block_low = None
self._prev_block_high = None
self._prev_block_low = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(smc_order_block_zones_strategy, self).OnReseted()
self._prev_block_high = None
self._prev_block_low = None
def OnStarted2(self, time):
super(smc_order_block_zones_strategy, self).OnStarted2(time)
self._prev_block_high = None
self._prev_block_low = None
sma = SimpleMovingAverage()
sma.Length = self._sma_length.Value
self._swing_high = Highest()
self._swing_high.Length = self._swing_high_length.Value
self._swing_low = Lowest()
self._swing_low.Length = self._swing_low_length.Value
self._block_high = Highest()
self._block_high.Length = self._order_block_length.Value
self._block_low = Lowest()
self._block_low.Length = self._order_block_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, self._process_candle).Start()
stop = float(self._stop_loss_percent.Value)
if stop > 0:
self.StartProtection(Unit(), Unit(Decimal(stop), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# The stop has to see prices between candles, not only at their close.
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()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# The high-level handler activates native protection before the callback, including between bars.
pass
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
time = candle.OpenTime
premium = float(process_float(self._swing_high, candle.HighPrice, time, True).GetValue[Decimal](None))
discount = float(process_float(self._swing_low, candle.LowPrice, time, True).GetValue[Decimal](None))
# Order blocks are taken from the candles before this one so the current candle can touch them.
bull_block = self._prev_block_low
bear_block = self._prev_block_high
block_high = float(process_float(self._block_high, candle.HighPrice, time, True).GetValue[Decimal](None))
block_low = float(process_float(self._block_low, candle.LowPrice, time, True).GetValue[Decimal](None))
if self._block_high.IsFormed and self._block_low.IsFormed:
self._prev_block_high = block_high
self._prev_block_low = block_low
if not sma_value.IsFormed or not self._swing_high.IsFormed or not self._swing_low.IsFormed or bull_block is None or bear_block is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
sma = float(sma_value.GetValue[Decimal](None))
close = float(candle.ClosePrice)
equilibrium = (premium + discount) / 2.0
long_signal = close < equilibrium and close > discount and close > sma and float(candle.LowPrice) <= bull_block
short_signal = close > equilibrium and close < premium and close < sma and float(candle.HighPrice) >= bear_block
if long_signal:
if self._allow_long.Value and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif self.Position < 0:
self.BuyMarket(-self.Position)
elif short_signal:
if self._allow_short.Value and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0:
self.SellMarket(self.Position)
def CreateClone(self):
return smc_order_block_zones_strategy()