Time Zone Pivots Open System Strategy
This strategy is a StockSharp high-level API port of the MetaTrader expert Exp_TimeZonePivotsOpenSystem. It reproduces the original logic that anchors a symmetric price channel to the daily opening price at a configurable hour and reacts when completed candles break above or below that band. All orders are sent as market orders and optional stop-loss / take-profit protection is configured through StartProtection.
How it works
- Subscribes to the configured candle timeframe, records the instrument price step and configures protective stops if distances are greater than zero.
- Tracks the first candle of every day whose opening time matches
StartHour. The open price of that candle becomes the anchor for the session and defines the upper and lower bands atOffsetPointsprice steps above and below the anchor. - Computes a five-state signal for each finished candle, mirroring the color-coded buffer of the original custom indicator:
0/1: the candle closed above the upper band (bullish breakout, with the index reflecting candle direction).2: the candle ended inside the band (neutral).3/4: the candle closed below the lower band (bearish breakout).
- Maintains a sliding history of signals. The candle located
SignalBarsteps back serves as the confirmation bar and the candle immediately before it must be neutral to trigger an entry, recreating the MetaTrader logic that waits for a bar after the breakout. - When a bullish confirmation appears the strategy optionally closes short positions and, if flat and allowed, opens a new long position. Bearish confirmations behave symmetrically for short trades.
- After a new position is opened the strategy postpones further entries in the same direction until the next candle following the confirmation bar begins, preventing duplicate orders in the same session.
Parameters
| Parameter | Description | Default |
|---|---|---|
CandleType |
Candle timeframe feeding the breakout calculations. | H1 |
OrderVolume |
Volume used for new positions. | 0.1 |
StartHour |
Hour (0-23) whose opening price anchors the daily bands. | 0 |
OffsetPoints |
Half-width of the band in price steps (tick units). | 100 |
SignalBar |
Number of closed candles between the current bar and the breakout confirmation. Must be ≥ 1 in this port. | 1 |
StopLossPoints |
Protective stop distance in price steps. | 1000 |
TakeProfitPoints |
Profit target distance in price steps. | 2000 |
EnableLongEntry |
Allow opening long positions after bullish signals. | true |
EnableShortEntry |
Allow opening short positions after bearish signals. | true |
CloseLongOnBearishBreak |
Close existing long positions on bearish confirmations. | true |
CloseShortOnBullishBreak |
Close existing short positions on bullish confirmations. | true |
Notes
- The money management block from the MetaTrader version is replaced by the explicit
OrderVolumeparameter typical for StockSharp strategies. - The stop-loss and take-profit parameters are converted from point distances to absolute price offsets using the current instrument price step.
- The S# implementation keeps only one net position (long, short, or flat) exactly like the MQL original, and will skip new entries while a position is still open.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Implements the Exp_TimeZonePivotsOpenSystem MetaTrader strategy using StockSharp's high level API.
/// The strategy anchors a symmetric price channel to the daily opening price at a configurable hour
/// and reacts when closed candles break above or below that band.
/// </summary>
public class TimeZonePivotsOpenSystemStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<decimal> _offsetPoints;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<bool> _enableLongEntry;
private readonly StrategyParam<bool> _enableShortEntry;
private readonly StrategyParam<bool> _closeLongOnBearishBreak;
private readonly StrategyParam<bool> _closeShortOnBullishBreak;
private decimal _priceStep;
private decimal _offsetDistance;
private decimal? _anchorPrice;
private DateTime? _anchorDate;
private decimal _upperZone;
private decimal _lowerZone;
private TimeSpan _candleSpan;
private DateTimeOffset? _nextLongTradeTime;
private DateTimeOffset? _nextShortTradeTime;
private readonly List<SignalRecord> _signalHistory = new();
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public TimeZonePivotsOpenSystemStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle type", "Timeframe that feeds the Time Zone Pivots logic.", "General");
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetNotNegative()
.SetDisplay("Order volume", "Volume used when opening a new position.", "Trading");
_startHour = Param(nameof(StartHour), 0)
.SetNotNegative()
.SetDisplay("Start hour", "Hour (0-23) whose opening price anchors the bands.", "Indicator");
_offsetPoints = Param(nameof(OffsetPoints), 250m)
.SetNotNegative()
.SetDisplay("Offset (points)", "Distance from the anchor price expressed in price steps.", "Indicator");
_signalBar = Param(nameof(SignalBar), 2)
.SetNotNegative()
.SetDisplay("Signal bar", "Shift of the confirmation candle used to trigger trades.", "Signals");
_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
.SetNotNegative()
.SetDisplay("Stop loss (points)", "Protective stop distance in price steps.", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000m)
.SetNotNegative()
.SetDisplay("Take profit (points)", "Profit target distance in price steps.", "Risk");
_enableLongEntry = Param(nameof(EnableLongEntry), true)
.SetDisplay("Enable long entries", "Allow opening long positions after bullish breakouts.", "Signals");
_enableShortEntry = Param(nameof(EnableShortEntry), true)
.SetDisplay("Enable short entries", "Allow opening short positions after bearish breakouts.", "Signals");
_closeLongOnBearishBreak = Param(nameof(CloseLongOnBearishBreak), true)
.SetDisplay("Close longs on bearish break", "Exit long trades when price falls below the lower band.", "Risk");
_closeShortOnBullishBreak = Param(nameof(CloseShortOnBullishBreak), true)
.SetDisplay("Close shorts on bullish break", "Exit short trades when price rallies above the upper band.", "Risk");
}
/// <summary>
/// Candle type that defines the working timeframe.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Volume sent with each new position.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Hour of the day used to anchor the pivot bands.
/// </summary>
public int StartHour
{
get => ClampHour(_startHour.Value);
set => _startHour.Value = ClampHour(value);
}
/// <summary>
/// Offset from the anchor price expressed in price steps.
/// </summary>
public decimal OffsetPoints
{
get => _offsetPoints.Value;
set => _offsetPoints.Value = value;
}
/// <summary>
/// Number of closed candles between the current bar and the breakout confirmation bar.
/// </summary>
public int SignalBar
{
get => Math.Max(1, _signalBar.Value);
set => _signalBar.Value = Math.Max(1, value);
}
/// <summary>
/// Stop loss distance measured in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance measured in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Enables long position entries after bullish breakouts.
/// </summary>
public bool EnableLongEntry
{
get => _enableLongEntry.Value;
set => _enableLongEntry.Value = value;
}
/// <summary>
/// Enables short position entries after bearish breakouts.
/// </summary>
public bool EnableShortEntry
{
get => _enableShortEntry.Value;
set => _enableShortEntry.Value = value;
}
/// <summary>
/// Enables closing long positions when bearish breakouts occur.
/// </summary>
public bool CloseLongOnBearishBreak
{
get => _closeLongOnBearishBreak.Value;
set => _closeLongOnBearishBreak.Value = value;
}
/// <summary>
/// Enables closing short positions when bullish breakouts occur.
/// </summary>
public bool CloseShortOnBullishBreak
{
get => _closeShortOnBullishBreak.Value;
set => _closeShortOnBullishBreak.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_anchorPrice = null;
_anchorDate = null;
_priceStep = 0m;
_upperZone = 0m;
_lowerZone = 0m;
_candleSpan = default;
_offsetDistance = 0m;
_signalHistory.Clear();
_nextLongTradeTime = null;
_nextShortTradeTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
_priceStep = Security?.PriceStep ?? 0m;
if (_priceStep <= 0m)
{
_priceStep = 1m;
}
_candleSpan = CandleType.Arg is TimeSpan span && span > TimeSpan.Zero
? span
: TimeSpan.FromHours(1);
_offsetDistance = OffsetPoints * _priceStep;
var stopLossDistance = StopLossPoints * _priceStep;
var takeProfitDistance = TakeProfitPoints * _priceStep;
if (stopLossDistance > 0m || takeProfitDistance > 0m)
{
StartProtection(
stopLoss: stopLossDistance > 0m ? new Unit(stopLossDistance, UnitTypes.Absolute) : null,
takeProfit: takeProfitDistance > 0m ? new Unit(takeProfitDistance, UnitTypes.Absolute) : null,
useMarketOrders: true);
}
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;
_offsetDistance = OffsetPoints * _priceStep;
Volume = OrderVolume;
UpdateAnchor(candle);
var signal = CalculateSignal(candle);
RecordSignal(candle.OpenTime, signal);
if (_signalHistory.Count <= SignalBar)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var confirmIndex = SignalBar;
var currentIndex = SignalBar - 1;
if (currentIndex < 0 || confirmIndex >= _signalHistory.Count)
return;
var currentSignal = _signalHistory[currentIndex];
var confirmSignal = _signalHistory[confirmIndex];
var bullishBreakout = confirmSignal.Signal <= 1;
var bearishBreakout = confirmSignal.Signal >= 3;
var position = Position;
if (position > 0m && bearishBreakout && CloseLongOnBearishBreak)
{
SellMarket(position);
position = Position;
}
if (position < 0m && bullishBreakout && CloseShortOnBullishBreak)
{
BuyMarket(Math.Abs(position));
position = Position;
}
var volume = OrderVolume;
if (volume <= 0m)
return;
var signalTime = confirmSignal.OpenTime + _candleSpan;
var candleTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
if (EnableLongEntry && bullishBreakout && currentSignal.Signal > 1 && position == 0m)
{
if (!_nextLongTradeTime.HasValue || candleTime >= _nextLongTradeTime.Value)
{
BuyMarket(volume);
_nextLongTradeTime = signalTime;
}
}
if (EnableShortEntry && bearishBreakout && currentSignal.Signal < 3 && position == 0m)
{
if (!_nextShortTradeTime.HasValue || candleTime >= _nextShortTradeTime.Value)
{
SellMarket(volume);
_nextShortTradeTime = signalTime;
}
}
}
private void UpdateAnchor(ICandleMessage candle)
{
var candleDate = candle.OpenTime.Date;
var hour = candle.OpenTime.Hour;
if (hour == StartHour && (_anchorDate == null || _anchorDate.Value != candleDate))
{
_anchorDate = candleDate;
_anchorPrice = candle.OpenPrice;
}
if (_anchorPrice.HasValue)
{
_upperZone = _anchorPrice.Value + _offsetDistance;
_lowerZone = _anchorPrice.Value - _offsetDistance;
}
}
private int CalculateSignal(ICandleMessage candle)
{
if (!_anchorPrice.HasValue)
return 2;
var close = candle.ClosePrice;
var open = candle.OpenPrice;
if (close > _upperZone)
return close >= open ? 0 : 1;
if (close < _lowerZone)
return close <= open ? 4 : 3;
return 2;
}
private void RecordSignal(DateTimeOffset time, int signal)
{
_signalHistory.Insert(0, new SignalRecord(signal, time));
var maxCapacity = Math.Max(SignalBar + 2, 4);
if (_signalHistory.Count > maxCapacity)
{
_signalHistory.RemoveRange(maxCapacity, _signalHistory.Count - maxCapacity);
}
}
private static int ClampHour(int hour)
{
if (hour < 0)
return 0;
if (hour > 23)
return 23;
return hour;
}
private sealed class SignalRecord
{
public SignalRecord(int signal, DateTimeOffset openTime)
{
Signal = signal;
OpenTime = openTime;
}
public int Signal { get; }
public DateTimeOffset OpenTime { get; }
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Strategies import Strategy
class time_zone_pivots_open_system_strategy(Strategy):
def __init__(self):
super(time_zone_pivots_open_system_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle type", "Timeframe that feeds the Time Zone Pivots logic.", "General")
self._order_volume = self.Param("OrderVolume", Decimal(0.1)) \
.SetNotNegative() \
.SetDisplay("Order volume", "Volume used when opening a new position.", "Trading")
self._start_hour = self.Param("StartHour", 0) \
.SetNotNegative() \
.SetDisplay("Start hour", "Hour (0-23) whose opening price anchors the bands.", "Indicator")
self._offset_points = self.Param("OffsetPoints", Decimal(250)) \
.SetNotNegative() \
.SetDisplay("Offset (points)", "Distance from the anchor price expressed in price steps.", "Indicator")
self._signal_bar = self.Param("SignalBar", 2) \
.SetNotNegative() \
.SetDisplay("Signal bar", "Shift of the confirmation candle used to trigger trades.", "Signals")
self._stop_loss_points = self.Param("StopLossPoints", Decimal(1000)) \
.SetNotNegative() \
.SetDisplay("Stop loss (points)", "Protective stop distance in price steps.", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", Decimal(2000)) \
.SetNotNegative() \
.SetDisplay("Take profit (points)", "Profit target distance in price steps.", "Risk")
self._enable_long_entry = self.Param("EnableLongEntry", True) \
.SetDisplay("Enable long entries", "Allow opening long positions after bullish breakouts.", "Signals")
self._enable_short_entry = self.Param("EnableShortEntry", True) \
.SetDisplay("Enable short entries", "Allow opening short positions after bearish breakouts.", "Signals")
self._close_long_on_bearish_break = self.Param("CloseLongOnBearishBreak", True) \
.SetDisplay("Close longs on bearish break", "Exit long trades when price falls below lower band.", "Risk")
self._close_short_on_bullish_break = self.Param("CloseShortOnBullishBreak", True) \
.SetDisplay("Close shorts on bullish break", "Exit short trades when price rallies above upper band.", "Risk")
self._price_step = Decimal(0)
self._offset_distance = Decimal(0)
self._anchor_price = None
self._anchor_date = None
self._upper_zone = Decimal(0)
self._lower_zone = Decimal(0)
self._candle_span = TimeSpan.Zero
self._next_long_trade_time = None
self._next_short_trade_time = None
self._signal_history = []
@property
def CandleType(self):
return self._candle_type.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def StartHour(self):
h = self._start_hour.Value
if h < 0:
return 0
if h > 23:
return 23
return h
@property
def OffsetPoints(self):
return self._offset_points.Value
@property
def SignalBar(self):
return max(1, self._signal_bar.Value)
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def EnableLongEntry(self):
return self._enable_long_entry.Value
@property
def EnableShortEntry(self):
return self._enable_short_entry.Value
@property
def CloseLongOnBearishBreak(self):
return self._close_long_on_bearish_break.Value
@property
def CloseShortOnBullishBreak(self):
return self._close_short_on_bullish_break.Value
def OnReseted(self):
super(time_zone_pivots_open_system_strategy, self).OnReseted()
self._anchor_price = None
self._anchor_date = None
self._upper_zone = Decimal(0)
self._lower_zone = Decimal(0)
self._signal_history = []
self._next_long_trade_time = None
self._next_short_trade_time = None
def OnStarted2(self, time):
super(time_zone_pivots_open_system_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
sec = self.Security
self._price_step = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(0)
if self._price_step <= Decimal(0):
self._price_step = Decimal(1)
arg = self.CandleType.Arg
if isinstance(arg, TimeSpan) and arg > TimeSpan.Zero:
self._candle_span = arg
else:
self._candle_span = TimeSpan.FromHours(1)
self._offset_distance = self.OffsetPoints * self._price_step
stop_loss_distance = self.StopLossPoints * self._price_step
take_profit_distance = self.TakeProfitPoints * self._price_step
if stop_loss_distance > Decimal(0) or take_profit_distance > Decimal(0):
sl_unit = Unit(stop_loss_distance, UnitTypes.Absolute) if stop_loss_distance > Decimal(0) else Unit()
tp_unit = Unit(take_profit_distance, UnitTypes.Absolute) if take_profit_distance > Decimal(0) else Unit()
self.StartProtection(tp_unit, sl_unit, False, None, None, True, False)
self._anchor_price = None
self._anchor_date = None
self._upper_zone = Decimal(0)
self._lower_zone = Decimal(0)
self._signal_history = []
self._next_long_trade_time = None
self._next_short_trade_time = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
self._offset_distance = self.OffsetPoints * self._price_step
self.Volume = self.OrderVolume
self._update_anchor(candle)
signal = self._calculate_signal(candle)
self._record_signal(candle.OpenTime, signal)
if len(self._signal_history) <= self.SignalBar:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
confirm_index = self.SignalBar
current_index = self.SignalBar - 1
if current_index < 0 or confirm_index >= len(self._signal_history):
return
current_signal = self._signal_history[current_index][0]
confirm_signal = self._signal_history[confirm_index][0]
confirm_time = self._signal_history[confirm_index][1]
bullish_breakout = confirm_signal <= 1
bearish_breakout = confirm_signal >= 3
position = self.Position
if position > Decimal(0) and bearish_breakout and self.CloseLongOnBearishBreak:
self.SellMarket(position)
position = self.Position
if position < Decimal(0) and bullish_breakout and self.CloseShortOnBullishBreak:
self.BuyMarket(Math.Abs(position))
position = self.Position
volume = self.OrderVolume
if volume <= Decimal(0):
return
signal_time = confirm_time + self._candle_span
candle_time = candle.CloseTime if candle.CloseTime != type(candle.CloseTime)() else candle.OpenTime
if self.EnableLongEntry and bullish_breakout and current_signal > 1 and position == Decimal(0):
if self._next_long_trade_time is None or candle_time >= self._next_long_trade_time:
self.BuyMarket(volume)
self._next_long_trade_time = signal_time
if self.EnableShortEntry and bearish_breakout and current_signal < 3 and position == Decimal(0):
if self._next_short_trade_time is None or candle_time >= self._next_short_trade_time:
self.SellMarket(volume)
self._next_short_trade_time = signal_time
def _update_anchor(self, candle):
candle_date = candle.OpenTime.Date
hour = candle.OpenTime.Hour
if hour == self.StartHour and (self._anchor_date is None or self._anchor_date != candle_date):
self._anchor_date = candle_date
self._anchor_price = candle.OpenPrice
if self._anchor_price is not None:
self._upper_zone = self._anchor_price + self._offset_distance
self._lower_zone = self._anchor_price - self._offset_distance
def _calculate_signal(self, candle):
if self._anchor_price is None:
return 2
close = candle.ClosePrice
open_p = candle.OpenPrice
if close > self._upper_zone:
return 0 if close >= open_p else 1
if close < self._lower_zone:
return 4 if close <= open_p else 3
return 2
def _record_signal(self, open_time, signal):
self._signal_history.insert(0, (signal, open_time))
max_cap = max(self.SignalBar + 2, 4)
if len(self._signal_history) > max_cap:
del self._signal_history[max_cap:]
def CreateClone(self):
return time_zone_pivots_open_system_strategy()