Стратегия SMC Order Block Zones
Стратегия определяет локальные экстремумы для построения премиум и дисконт зон. Простая скользящая средняя используется как фильтр тренда, а недавние ордер-блоки подтверждают вход. Сделки открываются при движении цены из одной зоны к равновесию при подтверждении ордер-блока, с защитой через процентный стоп-лосс.
Детали
- Условия входа:
- Цена закрытия ниже равновесия, но выше дисконт-зоны и SMA для длинных сделок.
- Цена закрытия выше равновесия, но ниже премиум-зоны и SMA для коротких сделок.
- Цена должна коснуться соответствующего уровня ордер-блока.
- Длинные/Короткие: Настраивается на длинные, короткие или оба направления.
- Условия выхода: Противоположный сигнал или стоп-лосс.
- Стопы: Процентный стоп-лосс.
- Значения по умолчанию:
SwingHighLength= 8SwingLowLength= 8SmaLength= 50OrderBlockLength= 20StopLossPercent= 2
- Фильтры:
- Категория: Тренд и SMC
- Направление: Задаётся пользователем
- Индикаторы: SMA, Highest, Lowest
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Любой
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()