Стратегия повторных входов по Chandel Exit
Эта стратегия является переносом советника MetaTrader «Exp_ChandelExitSign_ReOpen» на высокоуровневый API StockSharp. Она торгует пробои по полосам Chandelier Exit и автоматически наращивает позицию, если тренд продолжается. Сигналы рассчитываются на настраиваемом таймфрейме, а управление риском реализовано через ATR-стоп и необязательный тейк-профит.
Основная идея — использовать Chandelier Exit одновременно как трендовый фильтр и динамический барьер. Когда нижняя полоса пересекает верхнюю, фиксируется бычий импульс; обратное пересечение даёт медвежий сигнал. Логика симметрична для покупок и продаж, причём каждый тип сигнала можно включить или отключить параметрами. После открытия позиции цена должна пройти заданное количество шагов цены (PriceStepPoints), прежде чем разрешено добавление. Добор копирует поведение оригинального советника и ограничивается значением MaxAdditions, чтобы объём не рос бесконтрольно.
Логика работы
- Расчёт сигналов
RangePeriodбаров (со сдвигомShift) формируют максимум и минимум, по которым строятся полосы Chandelier Exit.AtrPeriodиAtrMultiplierзадают волатильностный буфер, смещающий полосы от цены.SignalBar(по умолчанию 1) задерживает исполнение, чтобы работать по последней завершённой свече и повторять MT5-поведение.
- Входы
- Покупка: срабатывает, когда нижняя полоса пересекает верхнюю (
IsUpSignal) иEnableBuyEntries = true. При наличии короткой позиции робот сначала пытается закрыть её, еслиEnableSellExits = true. - Продажа: противоположный сигнал (
IsDownSignal) приEnableSellEntries = true. Длинная позиция закрывается только еслиEnableBuyExits = true.
- Покупка: срабатывает, когда нижняя полоса пересекает верхнюю (
- Выходы
- Лонг закрывается по медвежьему сигналу при
EnableBuyExits = true, либо по защитному стопу/тейку. - Шорт закрывается по бычьему сигналу при
EnableSellExits = true, либо по защитным уровням. - Когда включены и входы, и выходы, стратегия дополнительно просматривает более старые значения индикатора, чтобы гарантировать наличие сигнала закрытия даже если текущая свеча дала только вход.
- Лонг закрывается по медвежьему сигналу при
- Повторные входы
- После каждого открытия запоминается цена сделки. Когда цена проходит в прибыль не менее
PriceStepPoints * PriceStep, отправляется дополнительная заявка объёмомVolume— но не болееMaxAdditionsраз. - Каждый добор пересчитывает уровни стопа и тейка от последней цены, удерживая защиту рядом с актуальной позицией.
- После каждого открытия запоминается цена сделки. Когда цена проходит в прибыль не менее
- Риск-менеджмент
StopLossPointsиTakeProfitPointsзадают расстояние в шагах цены от последней сделки. Нули отключают соответствующий уровень.- Защитные условия проверяются на каждой завершённой свече. Если цена внутри бара достигает стопа или тейка, позиция закрывается рыночной заявкой.
Параметры по умолчанию
| Параметр | Значение | Назначение |
|---|---|---|
CandleType |
TimeSpan.FromHours(4).TimeFrame() |
Таймфрейм, на котором строится индикатор. |
RangePeriod |
15 | Длина окна для максимумов и минимумов. |
Shift |
1 | Количество свежих баров, пропускаемых перед расчётом диапазона. |
AtrPeriod |
14 | Период ATR для волатильностного буфера. |
AtrMultiplier |
4 | Множитель ATR. |
SignalBar |
1 | На сколько завершённых баров назад брать сигнал. |
PriceStepPoints |
300 | Минимальное движение в шагах цены перед добавлением к позиции. |
MaxAdditions |
10 | Максимальное число доборов после первого входа. |
StopLossPoints |
1000 | Размер стоп-лосса в шагах цены. |
TakeProfitPoints |
2000 | Размер тейк-профита в шагах цены. |
EnableBuyEntries / EnableSellEntries |
true |
Разрешить открытие лонгов/шортов по сигналам. |
EnableBuyExits / EnableSellExits |
true |
Разрешить закрытие лонгов/шортов по противоположным сигналам. |
Практические рекомендации
- Базовый объём сделки задаётся параметром
Volume. Доборы используют тот же объём, поэтому для контроля риска корректируйтеVolumeилиMaxAdditions. - Порог
PriceStepPointsвыражен в шагах цены. Убедитесь, что у инструмента корректно настроенPriceStep, иначе расстояния будут искажены. - Значение
SignalBar = 1повторяет оригинального бота и защищает от исполнения на той же свече, которая генерирует сигнал. Установка 0 включает работу на последнем закрытом баре. - Стратегия рассчитана на инструменты, где разрешены обе стороны торговли. При необходимости можно отключить лонговые или шортовые сигналы параметрами.
- Если графическая панель доступна, методы
DrawCandles,DrawIndicatorиDrawOwnTradesавтоматически отрисуют свечи, индикатор и сделки для наглядного контроля.
Пример сценария
- Появляется бычий сигнал: нижняя полоса Chandelier Exit пересекает верхнюю.
- При отсутствии позиции и включённых покупках стратегия отправляет рыночную заявку
Volume. Стоп и тейк рассчитываются от цены входа. - Если цена проходит вперёд как минимум
PriceStepPoints * PriceStep, добавляется ещё один лот (до лимитаMaxAdditions). - Лонг закрывается при обратном сигнале, достижении стопа или тейка. Для шорта последовательность зеркальная.
Документация сохраняет логику MT5-советника, но использует соглашения StockSharp: параметры стратегии, подписку на свечи высокого уровня и явное управление позицией.
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy converted from the ChandelExitSign expert advisor with re-entry logic.
/// </summary>
public class ChandelExitReopenStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<int> _shift;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<decimal> _priceStepPoints;
private readonly StrategyParam<int> _maxAdditions;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<bool> _enableBuyEntries;
private readonly StrategyParam<bool> _enableSellEntries;
private readonly StrategyParam<bool> _enableBuyExits;
private readonly StrategyParam<bool> _enableSellExits;
private readonly List<CandleInfo> _history = new();
private readonly List<SignalInfo> _signals = new();
private decimal? _previousUp;
private decimal? _previousDown;
private int _direction;
private int _longAdditions;
private int _shortAdditions;
private decimal? _longEntryPrice;
private decimal? _shortEntryPrice;
private decimal? _longStopPrice;
private decimal? _shortStopPrice;
private decimal? _longTakePrice;
private decimal? _shortTakePrice;
private DateTimeOffset? _lastLongAdditionTime;
private DateTimeOffset? _lastShortAdditionTime;
/// <summary>
/// Initializes a new instance of <see cref="ChandelExitReopenStrategy"/>.
/// </summary>
public ChandelExitReopenStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for signals", "General");
_rangePeriod = Param(nameof(RangePeriod), 15)
.SetDisplay("Range Period", "Lookback for highest high and lowest low", "Indicator")
.SetGreaterThanZero()
;
_shift = Param(nameof(Shift), 1)
.SetDisplay("Shift", "Bars to skip from the most recent data", "Indicator")
.SetNotNegative()
;
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "ATR length for volatility filter", "Indicator")
.SetGreaterThanZero()
;
_atrMultiplier = Param(nameof(AtrMultiplier), 4m)
.SetDisplay("ATR Multiplier", "Multiplier applied to ATR", "Indicator")
.SetGreaterThanZero()
;
_signalBar = Param(nameof(SignalBar), 1)
.SetDisplay("Signal Bar", "How many bars back to read signals", "Trading")
.SetNotNegative();
_priceStepPoints = Param(nameof(PriceStepPoints), 1000m)
.SetDisplay("Re-entry Distance", "Minimum favorable move in price steps before adding", "Position Management")
.SetNotNegative()
;
_maxAdditions = Param(nameof(MaxAdditions), 1)
.SetDisplay("Max Additions", "Maximum number of re-entries after the initial position", "Position Management")
.SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetDisplay("Stop Loss Points", "Stop-loss distance in price steps", "Risk Management")
.SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetDisplay("Take Profit Points", "Take-profit distance in price steps", "Risk Management")
.SetNotNegative();
_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
.SetDisplay("Enable Long Entries", "Allow opening long positions on up signals", "Trading");
_enableSellEntries = Param(nameof(EnableSellEntries), true)
.SetDisplay("Enable Short Entries", "Allow opening short positions on down signals", "Trading");
_enableBuyExits = Param(nameof(EnableBuyExits), true)
.SetDisplay("Enable Long Exits", "Allow closing long positions on down signals", "Trading");
_enableSellExits = Param(nameof(EnableSellExits), true)
.SetDisplay("Enable Short Exits", "Allow closing short positions on up signals", "Trading");
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Range length for the Chandelier exit bands.
/// </summary>
public int RangePeriod
{
get => _rangePeriod.Value;
set => _rangePeriod.Value = value;
}
/// <summary>
/// Number of the most recent bars skipped before measuring the range.
/// </summary>
public int Shift
{
get => _shift.Value;
set => _shift.Value = value;
}
/// <summary>
/// ATR length used in the signal calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Multiplier applied to the ATR value.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Offset of the signal bar relative to the latest finished candle.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Required move in price steps before another position add is allowed.
/// </summary>
public decimal PriceStepPoints
{
get => _priceStepPoints.Value;
set => _priceStepPoints.Value = value;
}
/// <summary>
/// Maximum number of additional entries after the first fill.
/// </summary>
public int MaxAdditions
{
get => _maxAdditions.Value;
set => _maxAdditions.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Enables long entries generated by the up buffer.
/// </summary>
public bool EnableBuyEntries
{
get => _enableBuyEntries.Value;
set => _enableBuyEntries.Value = value;
}
/// <summary>
/// Enables short entries generated by the down buffer.
/// </summary>
public bool EnableSellEntries
{
get => _enableSellEntries.Value;
set => _enableSellEntries.Value = value;
}
/// <summary>
/// Enables long exits on down signals.
/// </summary>
public bool EnableBuyExits
{
get => _enableBuyExits.Value;
set => _enableBuyExits.Value = value;
}
/// <summary>
/// Enables short exits on up signals.
/// </summary>
public bool EnableSellExits
{
get => _enableSellExits.Value;
set => _enableSellExits.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_signals.Clear();
_previousUp = null;
_previousDown = null;
_direction = 0;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
var atr = atrValue.IsFinal ? atrValue.ToDecimal() : 0m;
var info = new CandleInfo(candle.OpenTime, candle.HighPrice, candle.LowPrice, candle.ClosePrice, atr);
_history.Add(info);
SignalInfo signal;
if (atrValue.IsFinal)
{
signal = CalculateSignal(info);
}
else
{
signal = SignalInfo.Empty(info.Time);
}
_signals.Add(signal);
TrimCache();
if (!atrValue.IsFinal)
return;
if (_signals.Count <= SignalBar)
return;
var signals = _signals.ToArray();
var targetIndex = signals.Length - 1 - SignalBar;
if (targetIndex < 0)
return;
var targetSignal = signals[targetIndex];
if (targetSignal is null)
return;
var buyOpen = targetSignal.IsUpSignal && EnableBuyEntries;
var sellOpen = targetSignal.IsDownSignal && EnableSellEntries;
var buyClose = targetSignal.IsDownSignal && EnableBuyExits;
var sellClose = targetSignal.IsUpSignal && EnableSellExits;
if (((EnableBuyEntries && EnableBuyExits) || (EnableSellEntries && EnableSellExits)) && !buyClose && !sellClose)
{
for (var idx = targetIndex - 1; idx >= 0; idx--)
{
var previousSignal = signals[idx];
if (previousSignal is null)
continue;
if (!sellClose && EnableSellExits && previousSignal.IsUpSignal)
{
sellClose = true;
break;
}
if (!buyClose && EnableBuyExits && previousSignal.IsDownSignal)
{
buyClose = true;
break;
}
}
}
var step = Security.PriceStep ?? 1m;
var priceStep = PriceStepPoints * step;
var longClosed = false;
var shortClosed = false;
if (Position > 0m)
{
if (_longStopPrice is decimal sl && candle.LowPrice <= sl)
{
SellMarket();
ResetLongState();
longClosed = true;
this.LogInfo($"Long stop triggered at {sl:0.########}");
}
else if (_longTakePrice is decimal tp && candle.HighPrice >= tp)
{
SellMarket();
ResetLongState();
longClosed = true;
this.LogInfo($"Long take profit triggered at {tp:0.########}");
}
}
if (Position < 0m)
{
if (_shortStopPrice is decimal sl && candle.HighPrice >= sl)
{
BuyMarket();
ResetShortState();
shortClosed = true;
this.LogInfo($"Short stop triggered at {sl:0.########}");
}
else if (_shortTakePrice is decimal tp && candle.LowPrice <= tp)
{
BuyMarket();
ResetShortState();
shortClosed = true;
this.LogInfo($"Short take profit triggered at {tp:0.########}");
}
}
if (!longClosed && buyClose && Position > 0m)
{
SellMarket();
ResetLongState();
longClosed = true;
this.LogInfo($"Long exit on down signal at {candle.ClosePrice:0.########}");
}
if (!shortClosed && sellClose && Position < 0m)
{
BuyMarket();
ResetShortState();
shortClosed = true;
this.LogInfo($"Short exit on up signal at {candle.ClosePrice:0.########}");
}
if (!longClosed && Position > 0m && MaxAdditions > 0 && _longEntryPrice is decimal lastLongPrice && priceStep > 0m && _longAdditions < MaxAdditions)
{
if (candle.ClosePrice - lastLongPrice >= priceStep && _lastLongAdditionTime != candle.OpenTime)
{
if (Volume > 0m)
{
BuyMarket();
_longAdditions++;
_longEntryPrice = candle.ClosePrice;
_lastLongAdditionTime = candle.OpenTime;
UpdateLongProtection(candle.ClosePrice, step);
this.LogInfo($"Added to long position at {candle.ClosePrice:0.########} (add #{_longAdditions})");
}
}
}
if (!shortClosed && Position < 0m && MaxAdditions > 0 && _shortEntryPrice is decimal lastShortPrice && priceStep > 0m && _shortAdditions < MaxAdditions)
{
if (lastShortPrice - candle.ClosePrice >= priceStep && _lastShortAdditionTime != candle.OpenTime)
{
if (Volume > 0m)
{
SellMarket();
_shortAdditions++;
_shortEntryPrice = candle.ClosePrice;
_lastShortAdditionTime = candle.OpenTime;
UpdateShortProtection(candle.ClosePrice, step);
this.LogInfo($"Added to short position at {candle.ClosePrice:0.########} (add #{_shortAdditions})");
}
}
}
if (buyOpen && Position < 0m && !EnableSellExits)
buyOpen = false;
if (sellOpen && Position > 0m && !EnableBuyExits)
sellOpen = false;
if (buyOpen && Volume > 0m)
{
BuyMarket();
ResetShortState();
_longAdditions = 0;
_longEntryPrice = candle.ClosePrice;
_lastLongAdditionTime = candle.OpenTime;
UpdateLongProtection(candle.ClosePrice, step);
this.LogInfo($"Opened long position at {candle.ClosePrice:0.########}");
}
if (sellOpen && Volume > 0m)
{
SellMarket();
ResetLongState();
_shortAdditions = 0;
_shortEntryPrice = candle.ClosePrice;
_lastShortAdditionTime = candle.OpenTime;
UpdateShortProtection(candle.ClosePrice, step);
this.LogInfo($"Opened short position at {candle.ClosePrice:0.########}");
}
}
private void TrimCache()
{
var maxItems = Math.Max(RangePeriod + Shift + 5, SignalBar + 5) + 50;
if (_history.Count <= maxItems)
return;
var removeCount = _history.Count - maxItems;
_history.RemoveRange(0, removeCount);
_signals.RemoveRange(0, removeCount);
}
private SignalInfo CalculateSignal(CandleInfo current)
{
var history = _history.ToArray();
var currentIndex = history.Length - 1;
var range = RangePeriod;
var shift = Shift;
if (range <= 0 || currentIndex - shift < 0)
return SignalInfo.Empty(current.Time);
var windowEnd = currentIndex - shift;
var windowStart = windowEnd - (range - 1);
if (windowStart < 0 || windowEnd >= history.Length)
return SignalInfo.Empty(current.Time);
var highestHigh = decimal.MinValue;
var lowestLow = decimal.MaxValue;
for (var i = windowStart; i <= windowEnd; i++)
{
var item = history[i];
if (item is null)
continue;
if (item.High > highestHigh)
highestHigh = item.High;
if (item.Low < lowestLow)
lowestLow = item.Low;
}
if (highestHigh == decimal.MinValue || lowestLow == decimal.MaxValue)
return SignalInfo.Empty(current.Time);
var atr = current.Atr * AtrMultiplier;
var upperBand = highestHigh - atr;
var lowerBand = lowestLow + atr;
decimal up;
decimal down;
if (_direction >= 0)
{
if (current.Close < upperBand)
{
_direction = -1;
up = lowerBand;
down = upperBand;
}
else
{
up = upperBand;
down = lowerBand;
}
}
else
{
if (current.Close > lowerBand)
{
_direction = 1;
down = lowerBand;
up = upperBand;
}
else
{
up = lowerBand;
down = upperBand;
}
}
var isUpSignal = false;
var isDownSignal = false;
if (_previousDown is decimal prevDn && _previousUp is decimal prevUp)
{
if (prevDn <= prevUp && down > up)
isUpSignal = true;
if (prevDn >= prevUp && down < up)
isDownSignal = true;
}
_previousUp = up;
_previousDown = down;
return new SignalInfo(current.Time, isUpSignal, isDownSignal, up, down);
}
private void UpdateLongProtection(decimal entryPrice, decimal step)
{
_longStopPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * step : null;
_longTakePrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * step : null;
}
private void UpdateShortProtection(decimal entryPrice, decimal step)
{
_shortStopPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * step : null;
_shortTakePrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * step : null;
}
private void ResetLongState()
{
_longAdditions = 0;
_longEntryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_lastLongAdditionTime = null;
}
private void ResetShortState()
{
_shortAdditions = 0;
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_lastShortAdditionTime = null;
}
private sealed record CandleInfo(DateTimeOffset Time, decimal High, decimal Low, decimal Close, decimal Atr);
private sealed record SignalInfo(DateTimeOffset Time, bool IsUpSignal, bool IsDownSignal, decimal Up, decimal Down)
{
public static SignalInfo Empty(DateTimeOffset time) => new(time, false, false, 0m, 0m);
}
}
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
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class chandel_exit_reopen_strategy(Strategy):
def __init__(self):
super(chandel_exit_reopen_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._range_period = self.Param("RangePeriod", 15)
self._shift = self.Param("Shift", 1)
self._atr_period = self.Param("AtrPeriod", 14)
self._atr_multiplier = self.Param("AtrMultiplier", 4.0)
self._signal_bar = self.Param("SignalBar", 1)
self._price_step_points = self.Param("PriceStepPoints", 1000.0)
self._max_additions = self.Param("MaxAdditions", 1)
self._stop_loss_points = self.Param("StopLossPoints", 1000)
self._take_profit_points = self.Param("TakeProfitPoints", 2000)
self._enable_buy_entries = self.Param("EnableBuyEntries", True)
self._enable_sell_entries = self.Param("EnableSellEntries", True)
self._enable_buy_exits = self.Param("EnableBuyExits", True)
self._enable_sell_exits = self.Param("EnableSellExits", True)
self._history = []
self._signals = []
self._previous_up = None
self._previous_down = None
self._direction = 0
self._long_additions = 0
self._short_additions = 0
self._long_entry_price = None
self._short_entry_price = None
self._long_stop_price = None
self._short_stop_price = None
self._long_take_price = None
self._short_take_price = None
self._last_long_addition_time = None
self._last_short_addition_time = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RangePeriod(self):
return self._range_period.Value
@RangePeriod.setter
def RangePeriod(self, value):
self._range_period.Value = value
@property
def Shift(self):
return self._shift.Value
@Shift.setter
def Shift(self, value):
self._shift.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def AtrMultiplier(self):
return self._atr_multiplier.Value
@AtrMultiplier.setter
def AtrMultiplier(self, value):
self._atr_multiplier.Value = value
@property
def SignalBar(self):
return self._signal_bar.Value
@SignalBar.setter
def SignalBar(self, value):
self._signal_bar.Value = value
@property
def PriceStepPoints(self):
return self._price_step_points.Value
@PriceStepPoints.setter
def PriceStepPoints(self, value):
self._price_step_points.Value = value
@property
def MaxAdditions(self):
return self._max_additions.Value
@MaxAdditions.setter
def MaxAdditions(self, value):
self._max_additions.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def EnableBuyEntries(self):
return self._enable_buy_entries.Value
@EnableBuyEntries.setter
def EnableBuyEntries(self, value):
self._enable_buy_entries.Value = value
@property
def EnableSellEntries(self):
return self._enable_sell_entries.Value
@EnableSellEntries.setter
def EnableSellEntries(self, value):
self._enable_sell_entries.Value = value
@property
def EnableBuyExits(self):
return self._enable_buy_exits.Value
@EnableBuyExits.setter
def EnableBuyExits(self, value):
self._enable_buy_exits.Value = value
@property
def EnableSellExits(self):
return self._enable_sell_exits.Value
@EnableSellExits.setter
def EnableSellExits(self, value):
self._enable_sell_exits.Value = value
def OnStarted2(self, time):
super(chandel_exit_reopen_strategy, self).OnStarted2(time)
self._history = []
self._signals = []
self._previous_up = None
self._previous_down = None
self._direction = 0
self._reset_long_state()
self._reset_short_state()
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(atr, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
atr = float(atr_value) if atr_value.IsFinal else 0.0
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
open_time = candle.OpenTime
info = (open_time, high, low, close, atr)
self._history.append(info)
if atr_value.IsFinal:
signal = self._calculate_signal(info)
else:
signal = (open_time, False, False, 0.0, 0.0)
self._signals.append(signal)
self._trim_cache()
if not atr_value.IsFinal:
return
sb = int(self.SignalBar)
if len(self._signals) <= sb:
return
target_index = len(self._signals) - 1 - sb
if target_index < 0:
return
target_signal = self._signals[target_index]
is_up_signal = target_signal[1]
is_down_signal = target_signal[2]
buy_open = is_up_signal and self.EnableBuyEntries
sell_open = is_down_signal and self.EnableSellEntries
buy_close = is_down_signal and self.EnableBuyExits
sell_close = is_up_signal and self.EnableSellExits
if ((self.EnableBuyEntries and self.EnableBuyExits) or (self.EnableSellEntries and self.EnableSellExits)) and not buy_close and not sell_close:
for idx in range(target_index - 1, -1, -1):
prev_signal = self._signals[idx]
if not sell_close and self.EnableSellExits and prev_signal[1]:
sell_close = True
break
if not buy_close and self.EnableBuyExits and prev_signal[2]:
buy_close = True
break
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
price_step_dist = float(self.PriceStepPoints) * step
long_closed = False
short_closed = False
if self.Position > 0:
if self._long_stop_price is not None and low <= self._long_stop_price:
self.SellMarket()
self._reset_long_state()
long_closed = True
elif self._long_take_price is not None and high >= self._long_take_price:
self.SellMarket()
self._reset_long_state()
long_closed = True
if self.Position < 0:
if self._short_stop_price is not None and high >= self._short_stop_price:
self.BuyMarket()
self._reset_short_state()
short_closed = True
elif self._short_take_price is not None and low <= self._short_take_price:
self.BuyMarket()
self._reset_short_state()
short_closed = True
if not long_closed and buy_close and self.Position > 0:
self.SellMarket()
self._reset_long_state()
long_closed = True
if not short_closed and sell_close and self.Position < 0:
self.BuyMarket()
self._reset_short_state()
short_closed = True
if not long_closed and self.Position > 0 and self.MaxAdditions > 0 and self._long_entry_price is not None and price_step_dist > 0.0 and self._long_additions < self.MaxAdditions:
if close - self._long_entry_price >= price_step_dist and self._last_long_addition_time != open_time:
self.BuyMarket()
self._long_additions += 1
self._long_entry_price = close
self._last_long_addition_time = open_time
self._update_long_protection(close, step)
if not short_closed and self.Position < 0 and self.MaxAdditions > 0 and self._short_entry_price is not None and price_step_dist > 0.0 and self._short_additions < self.MaxAdditions:
if self._short_entry_price - close >= price_step_dist and self._last_short_addition_time != open_time:
self.SellMarket()
self._short_additions += 1
self._short_entry_price = close
self._last_short_addition_time = open_time
self._update_short_protection(close, step)
if buy_open and self.Position < 0 and not self.EnableSellExits:
buy_open = False
if sell_open and self.Position > 0 and not self.EnableBuyExits:
sell_open = False
if buy_open:
self.BuyMarket()
self._reset_short_state()
self._long_additions = 0
self._long_entry_price = close
self._last_long_addition_time = open_time
self._update_long_protection(close, step)
if sell_open:
self.SellMarket()
self._reset_long_state()
self._short_additions = 0
self._short_entry_price = close
self._last_short_addition_time = open_time
self._update_short_protection(close, step)
def _trim_cache(self):
max_items = max(int(self.RangePeriod) + int(self.Shift) + 5, int(self.SignalBar) + 5) + 50
if len(self._history) <= max_items:
return
remove_count = len(self._history) - max_items
self._history = self._history[remove_count:]
self._signals = self._signals[remove_count:]
def _calculate_signal(self, current):
current_time, current_high, current_low, current_close, current_atr = current
history = list(self._history)
current_index = len(history) - 1
range_period = int(self.RangePeriod)
shift = int(self.Shift)
if range_period <= 0 or current_index - shift < 0:
return (current_time, False, False, 0.0, 0.0)
window_end = current_index - shift
window_start = window_end - (range_period - 1)
if window_start < 0 or window_end >= len(history):
return (current_time, False, False, 0.0, 0.0)
highest_high = -1e18
lowest_low = 1e18
for i in range(window_start, window_end + 1):
item = history[i]
if item[1] > highest_high:
highest_high = item[1]
if item[2] < lowest_low:
lowest_low = item[2]
if highest_high < -1e17 or lowest_low > 1e17:
return (current_time, False, False, 0.0, 0.0)
atr_adj = current_atr * float(self.AtrMultiplier)
upper_band = highest_high - atr_adj
lower_band = lowest_low + atr_adj
if self._direction >= 0:
if current_close < upper_band:
self._direction = -1
up = lower_band
down = upper_band
else:
up = upper_band
down = lower_band
else:
if current_close > lower_band:
self._direction = 1
down = lower_band
up = upper_band
else:
up = lower_band
down = upper_band
is_up_signal = False
is_down_signal = False
if self._previous_down is not None and self._previous_up is not None:
if self._previous_down <= self._previous_up and down > up:
is_up_signal = True
if self._previous_down >= self._previous_up and down < up:
is_down_signal = True
self._previous_up = up
self._previous_down = down
return (current_time, is_up_signal, is_down_signal, up, down)
def _update_long_protection(self, entry_price, step):
sl = int(self.StopLossPoints)
tp = int(self.TakeProfitPoints)
self._long_stop_price = entry_price - sl * step if sl > 0 else None
self._long_take_price = entry_price + tp * step if tp > 0 else None
def _update_short_protection(self, entry_price, step):
sl = int(self.StopLossPoints)
tp = int(self.TakeProfitPoints)
self._short_stop_price = entry_price + sl * step if sl > 0 else None
self._short_take_price = entry_price - tp * step if tp > 0 else None
def _reset_long_state(self):
self._long_additions = 0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._last_long_addition_time = None
def _reset_short_state(self):
self._short_additions = 0
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_short_addition_time = None
def OnReseted(self):
super(chandel_exit_reopen_strategy, self).OnReseted()
self._history = []
self._signals = []
self._previous_up = None
self._previous_down = None
self._direction = 0
self._reset_long_state()
self._reset_short_state()
def CreateClone(self):
return chandel_exit_reopen_strategy()