Стратегия Time Zone Pivots Open System
Стратегия представляет собой перенос на StockSharp (высокоуровневый API) эксперта MetaTrader Exp_TimeZonePivotsOpenSystem. Как и оригинал, она строит симметричный ценовой канал вокруг цены открытия свечи в заданный час и реагирует на закрытие свечей выше или ниже границ канала. Все сделки исполняются рыночными ордерами, а защитные стопы и тейк-профиты подключаются через StartProtection.
Принцип работы
- Подписывается на выбранный таймфрейм свечей, считывает минимальный шаг цены инструмента и при ненулевых дистанциях настраивает защитные стопы.
- Для каждого торгового дня отслеживает первую свечу, чьё время открытия совпадает со значением
StartHour. Её цена открытия становится опорной, а верхняя и нижняя границы устанавливаются на расстоянииOffsetPointsшагов цены вверх и вниз. - Для каждой завершённой свечи рассчитывается пятисостоятый сигнал, полностью повторяющий цветовой буфер оригинального индикатора:
0/1: закрытие выше верхней границы (бычий пробой, индекс зависит от направления свечи).2: закрытие внутри канала (нейтральный режим).3/4: закрытие ниже нижней границы (медвежий пробой).
- Ведётся скользящая история сигналов. Свеча на расстоянии
SignalBarиспользуется как подтверждающая, а свеча перед ней должна быть нейтральной, что воспроизводит паузу в одну свечу после пробоя из MQL версии. - При подтверждённом бычьем сигнале стратегия по желанию закрывает короткие позиции и, если разрешено и позиция нулевая, открывает длинную. Для медвежьих сигналов логика зеркальна.
- После открытия новой позиции стратегия запрещает повторные входы в ту же сторону до начала следующей свечи после подтверждающего бара, что предотвращает дублирование сделок.
Параметры
| Параметр | Описание | Значение по умолчанию |
|---|---|---|
CandleType |
Таймфрейм свечей, используемый для расчёта пробоев. | H1 |
OrderVolume |
Объём новой позиции. | 0.1 |
StartHour |
Час (0-23), чья свеча формирует опорную цену. | 0 |
OffsetPoints |
Полуширина канала в шагах цены. | 100 |
SignalBar |
Количество закрытых свечей между текущей и подтверждающей. В данной реализации должно быть ≥ 1. | 1 |
StopLossPoints |
Дистанция стоп-лосса в шагах цены. | 1000 |
TakeProfitPoints |
Дистанция тейк-профита в шагах цены. | 2000 |
EnableLongEntry |
Разрешить открытие длинных позиций по бычьим сигналам. | true |
EnableShortEntry |
Разрешить открытие коротких позиций по медвежьим сигналам. | true |
CloseLongOnBearishBreak |
Закрывать длинные позиции при медвежьем подтверждении. | true |
CloseShortOnBullishBreak |
Закрывать короткие позиции при бычьем подтверждении. | true |
Дополнительно
- Блок money-management из MetaTrader заменён на явный параметр
OrderVolume, что типично для стратегий StockSharp. - Стопы и тейк-профиты пересчитываются из пунктов в абсолютные значения на основе текущего шага цены инструмента.
- Как и в MQL версии, стратегия работает с одной чистой позицией и не открывает новые сделки, пока прежняя позиция не закрыта.
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()