Chandel Exit 再入场策略
该策略将 MetaTrader 智能交易系统“Exp_ChandelExitSign_ReOpen”移植到 StockSharp 的高级 API。它利用 Chandelier Exit 指标捕捉趋势突破,并在趋势持续时自动加仓。信号基于可配置的高周期 K 线计算,同时通过基于 ATR 的止损和可选的止盈保护仓位。
核心思想是把 Chandelier Exit 既当作趋势过滤器,又当作动态的追踪边界。当下轨向上穿越上轨时判定为多头信号,反向穿越则给出空头信号。多空逻辑完全对称,并且每类信号都可以通过参数单独启用或关闭。开仓后,只有当价格朝有利方向移动至少 PriceStepPoints 个最小价位时,系统才允许再次加仓,且总加仓次数受 MaxAdditions 限制,避免仓位无限膨胀。
交易逻辑
- 信号计算
RangePeriod(配合Shift偏移)决定 Chandelier Exit 所使用的最高价和最低价窗口。AtrPeriod与AtrMultiplier共同生成波动缓冲区,将退出带从价格中移开。SignalBar(默认 1)让策略在上一根完整 K 线上执行,复现 MT5 的延迟逻辑。
- 入场条件
- 做多:当下轨穿越上轨(
IsUpSignal)且EnableBuyEntries = true时触发。若已有空头持仓且EnableSellExits = true,策略先尝试平掉空单。 - 做空:当上轨穿越下轨(
IsDownSignal)且EnableSellEntries = true时触发。若有多头持仓,只在EnableBuyExits = true时才会先行平仓。
- 做多:当下轨穿越上轨(
- 出场条件
- 多单:在
EnableBuyExits = true且出现空头信号时全部平仓,或当止损/止盈被击中时平仓。 - 空单:在
EnableSellExits = true且出现多头信号时全部平仓,或当保护位触发时平仓。 - 当多空同时允许开平仓时,策略会回溯更早的指标值,确保即使当前 K 线只产生入场,也能找到合适的出场信号。
- 多单:在
- 加仓规则
- 每次成交后都会记录最近的成交价。只有当价格朝有利方向移动不少于
PriceStepPoints * PriceStep时,才会按Volume加仓一次,最多执行MaxAdditions次。 - 每次加仓都会重新计算止损/止盈,使保护价格紧跟最新仓位。
- 每次成交后都会记录最近的成交价。只有当价格朝有利方向移动不少于
- 风险控制
StopLossPoints与TakeProfitPoints以最小价位为单位设置止损和止盈距离,填写 0 即可关闭相应功能。- 每根完成的 K 线都会检查保护条件,若价格在柱内触发保护位,则立即以市价平仓。
默认参数
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
TimeSpan.FromHours(4).TimeFrame() |
计算指标时使用的时间周期。 |
RangePeriod |
15 | 计算最高/最低价时的窗口长度。 |
Shift |
1 | 在计算窗口之前跳过的最近 K 线数量。 |
AtrPeriod |
14 | ATR 的周期。 |
AtrMultiplier |
4 | ATR 乘数。 |
SignalBar |
1 | 信号回溯的完成 K 线数量。 |
PriceStepPoints |
300 | 允许加仓前价格需移动的最小价位数量。 |
MaxAdditions |
10 | 首次开仓后允许的最大加仓次数。 |
StopLossPoints |
1000 | 止损距离(以最小价位表示)。 |
TakeProfitPoints |
2000 | 止盈距离(以最小价位表示)。 |
EnableBuyEntries / EnableSellEntries |
true |
是否开启多头/空头入场。 |
EnableBuyExits / EnableSellExits |
true |
是否允许多头/空头出场。 |
使用建议
Volume决定基础下单手数,加仓同样使用该手数。若想降低风险,可调小Volume或减少MaxAdditions。- 由于阈值按最小价位定义,请确保合约的
PriceStep信息正确,否则距离计算会失真。 - 将
SignalBar设为 1 可以避免在产生信号的同一根 K 线上立即成交;若希望更激进,可以设置为 0。 - 策略适用于可做多做空的市场。如果只想交易单边,可通过参数禁用另一方向。
- 当图表区域可用时,策略会自动调用
DrawCandles、DrawIndicator和DrawOwnTrades,便于观察指标与成交情况。
示例流程
- 观察到下轨向上穿越上轨,出现多头信号。
- 若无持仓且允许做多,则按
Volume下市价买单,同时根据成交价设置止损和止盈。 - 当价格向上运行不少于
PriceStepPoints * PriceStep时,在不超出MaxAdditions的前提下再次买入加仓。 - 出现反向信号、触及止损或止盈时平掉全部多单;空头流程与之相反。
该说明在保持 MT5 原始逻辑的同时,遵循 StockSharp 的常见约定:通过策略参数管理配置、使用高级 K 线订阅以及显式的仓位管理。
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()