namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Port of the Exp_Hans_Indicator_Cloud_System_Tm_Plus MQL5 expert advisor.
/// Replicates the Hans indicator breakout logic, including time-based exits and pip-based risk limits.
/// </summary>
public class ExpHansIndicatorCloudSystemTmPlusStrategy : Strategy
{
private readonly StrategyParam<int> _maxHistory;
private static readonly TimeSpan Session1Start = TimeSpan.FromHours(4);
private static readonly TimeSpan Session1End = TimeSpan.FromHours(8);
private static readonly TimeSpan Session2End = TimeSpan.FromHours(12);
private readonly StrategyParam<decimal> _moneyManagement;
private readonly StrategyParam<MoneyManagementModes> _moneyMode;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _deviationPoints;
private readonly StrategyParam<bool> _allowBuyEntries;
private readonly StrategyParam<bool> _allowSellEntries;
private readonly StrategyParam<bool> _allowBuyExits;
private readonly StrategyParam<bool> _allowSellExits;
private readonly StrategyParam<bool> _useTimeExit;
private readonly StrategyParam<int> _holdingMinutes;
private readonly StrategyParam<int> _pipsForEntry;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<int> _localTimeZone;
private readonly StrategyParam<int> _destinationTimeZone;
private readonly StrategyParam<int> _entryCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private DailySessionState _dayState;
private readonly List<int> _colorHistory = new();
private decimal? _stopPrice;
private decimal? _takePrice;
private DateTimeOffset? _entryTime;
private decimal _prevClosePrice;
private int _cooldownRemaining;
private bool _hasPrevClose;
/// <summary>
/// Enumeration matching the money management modes of the original expert.
/// Currently only the Lot mode is applied; other options are reserved for future extensions.
/// </summary>
public enum MoneyManagementModes
{
FreeMargin,
Balance,
LossFreeMargin,
LossBalance,
Lot,
}
/// <summary>
/// Portion of the base strategy volume used for each order.
/// </summary>
public decimal MoneyManagement
{
get => _moneyManagement.Value;
set => _moneyManagement.Value = value;
}
/// <summary>
/// Selected money management interpretation.
/// </summary>
public MoneyManagementModes MoneyMode
{
get => _moneyMode.Value;
set => _moneyMode.Value = value;
}
/// <summary>
/// Stop loss distance expressed in instrument points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in instrument points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Allowed execution deviation in points (kept for compatibility).
/// </summary>
public int DeviationPoints
{
get => _deviationPoints.Value;
set => _deviationPoints.Value = value;
}
/// <summary>
/// Enables long entries when the bullish breakout sequence completes.
/// </summary>
public bool AllowBuyEntries
{
get => _allowBuyEntries.Value;
set => _allowBuyEntries.Value = value;
}
/// <summary>
/// Enables short entries when the bearish breakout sequence completes.
/// </summary>
public bool AllowSellEntries
{
get => _allowSellEntries.Value;
set => _allowSellEntries.Value = value;
}
/// <summary>
/// Allows closing long positions on bearish Hans colors.
/// </summary>
public bool AllowBuyExits
{
get => _allowBuyExits.Value;
set => _allowBuyExits.Value = value;
}
/// <summary>
/// Allows closing short positions on bullish Hans colors.
/// </summary>
public bool AllowSellExits
{
get => _allowSellExits.Value;
set => _allowSellExits.Value = value;
}
/// <summary>
/// Enables the time-based exit filter.
/// </summary>
public bool UseTimeExit
{
get => _useTimeExit.Value;
set => _useTimeExit.Value = value;
}
/// <summary>
/// Maximum holding time in minutes before the position is liquidated.
/// </summary>
public int HoldingMinutes
{
get => _holdingMinutes.Value;
set => _holdingMinutes.Value = value;
}
/// <summary>
/// Number of pips added to the breakout range.
/// </summary>
public int PipsForEntry
{
get => _pipsForEntry.Value;
set => _pipsForEntry.Value = value;
}
/// <summary>
/// Number of closed candles used as signal offset.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Broker/server time zone in hours.
/// </summary>
public int LocalTimeZone
{
get => _localTimeZone.Value;
set => _localTimeZone.Value = value;
}
/// <summary>
/// Destination time zone defining the Hans breakout sessions.
/// </summary>
public int DestinationTimeZone
{
get => _destinationTimeZone.Value;
set => _destinationTimeZone.Value = value;
}
/// <summary>
/// Bars to wait after each entry before accepting a new one.
/// </summary>
public int EntryCooldownBars
{
get => _entryCooldownBars.Value;
set => _entryCooldownBars.Value = value;
}
/// <summary>
/// Candle series used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of Hans color samples preserved for decision making.
/// </summary>
public int MaxHistory
{
get => _maxHistory.Value;
set => _maxHistory.Value = value;
}
/// <summary>
/// Initialize the strategy parameters with defaults matching the MQL5 inputs.
/// </summary>
public ExpHansIndicatorCloudSystemTmPlusStrategy()
{
_maxHistory = Param(nameof(MaxHistory), 1024)
.SetGreaterThanZero()
.SetDisplay("Max History", "Maximum number of Hans color entries stored", "Indicator");
_moneyManagement = Param(nameof(MoneyManagement), 0.1m)
.SetDisplay("Money Management", "Portion of the base volume traded per entry", "Risk");
_moneyMode = Param(nameof(MoneyMode), MoneyManagementModes.Lot)
.SetDisplay("Money Mode", "Interpretation of the money management value", "Risk");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetDisplay("Stop Loss (points)", "Distance to the protective stop in points", "Risk")
.SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetDisplay("Take Profit (points)", "Distance to the profit target in points", "Risk")
.SetNotNegative();
_deviationPoints = Param(nameof(DeviationPoints), 10)
.SetDisplay("Execution Deviation", "Maximum acceptable slippage in points", "Orders")
.SetNotNegative();
_allowBuyEntries = Param(nameof(AllowBuyEntries), true)
.SetDisplay("Enable Long Entries", "Allow opening long positions", "Signals");
_allowSellEntries = Param(nameof(AllowSellEntries), true)
.SetDisplay("Enable Short Entries", "Allow opening short positions", "Signals");
_allowBuyExits = Param(nameof(AllowBuyExits), true)
.SetDisplay("Enable Long Exits", "Allow automated long exits", "Signals");
_allowSellExits = Param(nameof(AllowSellExits), true)
.SetDisplay("Enable Short Exits", "Allow automated short exits", "Signals");
_useTimeExit = Param(nameof(UseTimeExit), true)
.SetDisplay("Use Time Exit", "Close positions after the holding period", "Risk");
_holdingMinutes = Param(nameof(HoldingMinutes), 1500)
.SetDisplay("Holding Minutes", "Maximum position lifetime in minutes", "Risk")
.SetNotNegative();
_pipsForEntry = Param(nameof(PipsForEntry), 5)
.SetDisplay("Pips For Entry", "Offset added above/below the breakout range", "Indicator")
.SetNotNegative();
_signalBar = Param(nameof(SignalBar), 1)
.SetDisplay("Signal Bar", "Closed candle offset used for signals", "Indicator")
.SetNotNegative();
_localTimeZone = Param(nameof(LocalTimeZone), 0)
.SetDisplay("Local Time Zone", "Broker/server time zone", "Indicator");
_destinationTimeZone = Param(nameof(DestinationTimeZone), 0)
.SetDisplay("Destination Time Zone", "Target time zone for sessions", "Indicator");
_entryCooldownBars = Param(nameof(EntryCooldownBars), 10)
.SetDisplay("Entry Cooldown", "Bars to wait after an entry signal", "Risk")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for Hans calculations", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_dayState = null;
_prevClosePrice = 0m;
_cooldownRemaining = 0;
_hasPrevClose = false;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0 && (_entryTime.HasValue || _stopPrice.HasValue || _takePrice.HasValue))
ResetPositionState();
if (_cooldownRemaining > 0)
_cooldownRemaining--;
UpdateDailyState(candle);
var color = CalculateColor(candle);
_colorHistory.Add(color);
TrimHistory();
var offset = Math.Max(1, SignalBar);
if (_colorHistory.Count <= offset)
return;
var currentIndex = _colorHistory.Count - offset;
if (currentIndex >= _colorHistory.Count)
return;
var currentColor = _colorHistory[currentIndex];
var hasBands = TryGetActiveBands(out var upper, out var lower);
var buyEntrySignal = false;
var sellEntrySignal = false;
if (hasBands && _hasPrevClose)
{
buyEntrySignal = AllowBuyEntries && _prevClosePrice <= upper && candle.ClosePrice > upper;
sellEntrySignal = AllowSellEntries && _prevClosePrice >= lower && candle.ClosePrice < lower;
}
var buyExitSignal = AllowBuyExits && (IsLowerBreakout(currentColor) || (hasBands && candle.ClosePrice < lower));
var sellExitSignal = AllowSellExits && (IsUpperBreakout(currentColor) || (hasBands && candle.ClosePrice > upper));
if (Position > 0)
{
var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
var exitByStop = _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value;
var exitByTarget = _takePrice.HasValue && candle.HighPrice >= _takePrice.Value;
if (exitByTime || buyExitSignal || exitByStop || exitByTarget)
{
CloseLong();
}
}
else if (Position < 0)
{
var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
var exitByStop = _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value;
var exitByTarget = _takePrice.HasValue && candle.LowPrice <= _takePrice.Value;
if (exitByTime || sellExitSignal || exitByStop || exitByTarget)
{
CloseShort();
}
}
_prevClosePrice = candle.ClosePrice;
_hasPrevClose = true;
if (_cooldownRemaining > 0)
return;
if (_dayState != null && _dayState.EntryTaken)
return;
if (buyEntrySignal && Position <= 0)
{
EnterLong(candle);
}
else if (sellEntrySignal && Position >= 0)
{
EnterShort(candle);
}
}
private void EnterLong(ICandleMessage candle)
{
var volume = GetOrderVolume();
if (volume <= 0)
return;
var existingShort = Position < 0 ? Math.Abs(Position) : 0m;
var totalVolume = volume + existingShort;
if (totalVolume <= 0)
return;
BuyMarket(totalVolume);
_cooldownRemaining = EntryCooldownBars;
_dayState!.EntryTaken = true;
_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
var pipSize = GetPipSize();
if (pipSize <= 0)
{
_stopPrice = null;
_takePrice = null;
return;
}
_stopPrice = StopLossPoints > 0 ? candle.ClosePrice - pipSize * StopLossPoints : null;
_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice + pipSize * TakeProfitPoints : null;
}
private void EnterShort(ICandleMessage candle)
{
var volume = GetOrderVolume();
if (volume <= 0)
return;
var existingLong = Position > 0 ? Math.Abs(Position) : 0m;
var totalVolume = volume + existingLong;
if (totalVolume <= 0)
return;
SellMarket(totalVolume);
_cooldownRemaining = EntryCooldownBars;
_dayState!.EntryTaken = true;
_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
var pipSize = GetPipSize();
if (pipSize <= 0)
{
_stopPrice = null;
_takePrice = null;
return;
}
_stopPrice = StopLossPoints > 0 ? candle.ClosePrice + pipSize * StopLossPoints : null;
_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice - pipSize * TakeProfitPoints : null;
}
private void CloseLong()
{
var volume = Math.Abs(Position);
if (volume <= 0)
return;
SellMarket(volume);
ResetPositionState();
}
private void CloseShort()
{
var volume = Math.Abs(Position);
if (volume <= 0)
return;
BuyMarket(volume);
ResetPositionState();
}
private void UpdateDailyState(ICandleMessage candle)
{
var destOpen = ToDestinationTime(candle.OpenTime);
var date = destOpen.Date;
if (_dayState == null || _dayState.Date != date)
{
_dayState = new DailySessionState { Date = date };
}
var state = _dayState;
var timeOfDay = destOpen.TimeOfDay;
if (timeOfDay >= Session1Start && timeOfDay < Session1End)
{
UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, true);
state.Session1Completed = false;
}
else if (timeOfDay >= Session1End && timeOfDay < Session2End)
{
if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
state.Session1Completed = true;
UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, false);
state.Session2Completed = false;
}
else
{
if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
state.Session1Completed = true;
if (!state.Session2Completed && state.Session2High.HasValue && state.Session2Low.HasValue)
state.Session2Completed = true;
}
}
private int CalculateColor(ICandleMessage candle)
{
if (!TryGetActiveBands(out var upper, out var lower))
return 2;
if (candle.ClosePrice > upper)
return candle.ClosePrice >= candle.OpenPrice ? 0 : 1;
if (candle.ClosePrice < lower)
return candle.ClosePrice <= candle.OpenPrice ? 4 : 3;
return 2;
}
private bool TryGetActiveBands(out decimal upper, out decimal lower)
{
upper = 0m;
lower = 0m;
var pipSize = GetPipSize();
if (pipSize <= 0)
return false;
if (_dayState == null)
return false;
if (_dayState.Session2Completed && _dayState.Session2High.HasValue && _dayState.Session2Low.HasValue)
{
upper = _dayState.Session2High.Value + pipSize * PipsForEntry;
lower = _dayState.Session2Low.Value - pipSize * PipsForEntry;
return true;
}
if (_dayState.Session1Completed && _dayState.Session1High.HasValue && _dayState.Session1Low.HasValue)
{
upper = _dayState.Session1High.Value + pipSize * PipsForEntry;
lower = _dayState.Session1Low.Value - pipSize * PipsForEntry;
return true;
}
return false;
}
private void UpdateSessionRange(DailySessionState state, decimal high, decimal low, bool isFirstSession)
{
if (isFirstSession)
{
state.Session1High = state.Session1High.HasValue ? Math.Max(state.Session1High.Value, high) : high;
state.Session1Low = state.Session1Low.HasValue ? Math.Min(state.Session1Low.Value, low) : low;
}
else
{
state.Session2High = state.Session2High.HasValue ? Math.Max(state.Session2High.Value, high) : high;
state.Session2Low = state.Session2Low.HasValue ? Math.Min(state.Session2Low.Value, low) : low;
}
}
private decimal GetOrderVolume()
{
var step = Security?.VolumeStep ?? 1m;
if (step <= 0)
step = 1m;
var baseVolume = Volume * MoneyManagement;
if (baseVolume <= 0)
baseVolume = Volume;
var normalized = Math.Round(baseVolume / step) * step;
if (normalized <= 0)
normalized = step;
return normalized;
}
private decimal GetPipSize()
{
if (Security?.PriceStep is decimal step && step > 0m)
{
var decimals = Security.Decimals;
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
return 0.01m;
}
private void ResetPositionState()
{
_entryTime = null;
_stopPrice = null;
_takePrice = null;
}
private void TrimHistory()
{
if (_colorHistory.Count <= MaxHistory)
return;
var excess = _colorHistory.Count - MaxHistory;
_colorHistory.RemoveRange(0, excess);
}
private DateTimeOffset ToDestinationTime(DateTimeOffset time)
{
var shift = TimeSpan.FromHours(LocalTimeZone - DestinationTimeZone);
return time - shift;
}
private static bool IsUpperBreakout(int? color) => color is 0 or 1;
private static bool IsLowerBreakout(int? color) => color is 3 or 4;
private sealed class DailySessionState
{
public DateTime Date { get; set; }
public decimal? Session1High { get; set; }
public decimal? Session1Low { get; set; }
public decimal? Session2High { get; set; }
public decimal? Session2Low { get; set; }
public bool Session1Completed { get; set; }
public bool Session2Completed { get; set; }
public bool EntryTaken { get; set; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class exp_hans_indicator_cloud_system_tm_plus_strategy(Strategy):
_SESSION1_START = 4
_SESSION1_END = 8
_SESSION2_END = 12
def __init__(self):
super(exp_hans_indicator_cloud_system_tm_plus_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Time frame used for Hans calculations", "Data")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss (points)", "Distance to the protective stop in points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit (points)", "Distance to the profit target in points", "Risk")
self._pips_for_entry = self.Param("PipsForEntry", 5) \
.SetDisplay("Pips For Entry", "Offset added above/below the breakout range", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Closed candle offset used for signals", "Indicator")
self._local_time_zone = self.Param("LocalTimeZone", 0) \
.SetDisplay("Local Time Zone", "Broker/server time zone", "Indicator")
self._destination_time_zone = self.Param("DestinationTimeZone", 0) \
.SetDisplay("Destination Time Zone", "Target time zone for sessions", "Indicator")
self._entry_cooldown_bars = self.Param("EntryCooldownBars", 10) \
.SetDisplay("Entry Cooldown", "Bars to wait after an entry signal", "Risk")
self._use_time_exit = self.Param("UseTimeExit", True) \
.SetDisplay("Use Time Exit", "Close positions after the holding period", "Risk")
self._holding_minutes = self.Param("HoldingMinutes", 1500) \
.SetDisplay("Holding Minutes", "Maximum position lifetime in minutes", "Risk")
self._color_history = []
self._s1_high = None
self._s1_low = None
self._s1_completed = False
self._s2_high = None
self._s2_low = None
self._s2_completed = False
self._day_date = None
self._day_entry_taken = False
self._prev_close_price = 0.0
self._has_prev_close = False
self._cooldown_remaining = 0
self._stop_price = None
self._take_price = None
self._entry_time = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def PipsForEntry(self):
return self._pips_for_entry.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def LocalTimeZone(self):
return self._local_time_zone.Value
@property
def DestinationTimeZone(self):
return self._destination_time_zone.Value
@property
def EntryCooldownBars(self):
return self._entry_cooldown_bars.Value
@property
def UseTimeExit(self):
return self._use_time_exit.Value
@property
def HoldingMinutes(self):
return self._holding_minutes.Value
def OnReseted(self):
super(exp_hans_indicator_cloud_system_tm_plus_strategy, self).OnReseted()
self._color_history = []
self._s1_high = None
self._s1_low = None
self._s1_completed = False
self._s2_high = None
self._s2_low = None
self._s2_completed = False
self._day_date = None
self._day_entry_taken = False
self._prev_close_price = 0.0
self._has_prev_close = False
self._cooldown_remaining = 0
self._stop_price = None
self._take_price = None
self._entry_time = None
def OnStarted2(self, time):
super(exp_hans_indicator_cloud_system_tm_plus_strategy, self).OnStarted2(time)
self._color_history = []
self._s1_high = None
self._s1_low = None
self._s1_completed = False
self._s2_high = None
self._s2_low = None
self._s2_completed = False
self._day_date = None
self._day_entry_taken = False
self._prev_close_price = 0.0
self._has_prev_close = False
self._cooldown_remaining = 0
self._stop_price = None
self._take_price = None
self._entry_time = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position == 0 and (self._entry_time is not None or self._stop_price is not None or self._take_price is not None):
self._reset_position_state()
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._update_daily_state(candle)
color = self._calculate_color(candle)
self._color_history.append(color)
self._trim_history()
offset = max(1, self.SignalBar)
if len(self._color_history) <= offset:
self._manage_exits(candle)
self._prev_close_price = float(candle.ClosePrice)
self._has_prev_close = True
return
current_index = len(self._color_history) - offset
if current_index >= len(self._color_history):
self._manage_exits(candle)
self._prev_close_price = float(candle.ClosePrice)
self._has_prev_close = True
return
current_color = self._color_history[current_index]
has_bands, upper, lower = self._try_get_active_bands()
buy_entry_signal = False
sell_entry_signal = False
if has_bands and self._has_prev_close:
buy_entry_signal = self._prev_close_price <= upper and float(candle.ClosePrice) > upper
sell_entry_signal = self._prev_close_price >= lower and float(candle.ClosePrice) < lower
buy_exit_signal = self._is_lower_breakout(current_color) or (has_bands and float(candle.ClosePrice) < lower)
sell_exit_signal = self._is_upper_breakout(current_color) or (has_bands and float(candle.ClosePrice) > upper)
if self.Position > 0:
exit_by_time = self.UseTimeExit and self.HoldingMinutes > 0 and self._entry_time is not None and (candle.CloseTime - self._entry_time).TotalMinutes >= self.HoldingMinutes
exit_by_stop = self._stop_price is not None and float(candle.LowPrice) <= self._stop_price
exit_by_target = self._take_price is not None and float(candle.HighPrice) >= self._take_price
if exit_by_time or buy_exit_signal or exit_by_stop or exit_by_target:
self.SellMarket()
self._reset_position_state()
elif self.Position < 0:
exit_by_time = self.UseTimeExit and self.HoldingMinutes > 0 and self._entry_time is not None and (candle.CloseTime - self._entry_time).TotalMinutes >= self.HoldingMinutes
exit_by_stop = self._stop_price is not None and float(candle.HighPrice) >= self._stop_price
exit_by_target = self._take_price is not None and float(candle.LowPrice) <= self._take_price
if exit_by_time or sell_exit_signal or exit_by_stop or exit_by_target:
self.BuyMarket()
self._reset_position_state()
self._prev_close_price = float(candle.ClosePrice)
self._has_prev_close = True
if self._cooldown_remaining > 0:
return
if self._day_entry_taken:
return
if buy_entry_signal and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self.EntryCooldownBars
self._day_entry_taken = True
self._entry_time = candle.CloseTime
pip_size = self._get_pip_size()
if pip_size > 0.0:
self._stop_price = float(candle.ClosePrice) - pip_size * float(self.StopLossPoints) if self.StopLossPoints > 0 else None
self._take_price = float(candle.ClosePrice) + pip_size * float(self.TakeProfitPoints) if self.TakeProfitPoints > 0 else None
elif sell_entry_signal and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self.EntryCooldownBars
self._day_entry_taken = True
self._entry_time = candle.CloseTime
pip_size = self._get_pip_size()
if pip_size > 0.0:
self._stop_price = float(candle.ClosePrice) + pip_size * float(self.StopLossPoints) if self.StopLossPoints > 0 else None
self._take_price = float(candle.ClosePrice) - pip_size * float(self.TakeProfitPoints) if self.TakeProfitPoints > 0 else None
def _manage_exits(self, candle):
if self.Position > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
self._reset_position_state()
elif self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket()
self._reset_position_state()
elif self.Position < 0:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
self._reset_position_state()
elif self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket()
self._reset_position_state()
def _update_daily_state(self, candle):
dest_hour = candle.OpenTime.Hour + (self.DestinationTimeZone - self.LocalTimeZone)
dest_date = candle.OpenTime.Date
if dest_hour >= 24:
dest_hour -= 24
dest_date = dest_date.AddDays(1)
elif dest_hour < 0:
dest_hour += 24
dest_date = dest_date.AddDays(-1)
if self._day_date is None or self._day_date != dest_date:
self._day_date = dest_date
self._s1_high = None
self._s1_low = None
self._s1_completed = False
self._s2_high = None
self._s2_low = None
self._s2_completed = False
self._day_entry_taken = False
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if dest_hour >= self._SESSION1_START and dest_hour < self._SESSION1_END:
self._s1_high = max(self._s1_high, high) if self._s1_high is not None else high
self._s1_low = min(self._s1_low, low) if self._s1_low is not None else low
self._s1_completed = False
elif dest_hour >= self._SESSION1_END and dest_hour < self._SESSION2_END:
if not self._s1_completed and self._s1_high is not None and self._s1_low is not None:
self._s1_completed = True
self._s2_high = max(self._s2_high, high) if self._s2_high is not None else high
self._s2_low = min(self._s2_low, low) if self._s2_low is not None else low
self._s2_completed = False
else:
if not self._s1_completed and self._s1_high is not None and self._s1_low is not None:
self._s1_completed = True
if not self._s2_completed and self._s2_high is not None and self._s2_low is not None:
self._s2_completed = True
def _calculate_color(self, candle):
has_bands, upper, lower = self._try_get_active_bands()
if not has_bands:
return 2
close = float(candle.ClosePrice)
open_p = float(candle.OpenPrice)
if close > upper:
return 0 if close >= open_p else 1
if close < lower:
return 4 if close <= open_p else 3
return 2
def _try_get_active_bands(self):
pip_size = self._get_pip_size()
if pip_size <= 0.0:
return False, 0.0, 0.0
if self._s2_completed and self._s2_high is not None and self._s2_low is not None:
upper = self._s2_high + pip_size * float(self.PipsForEntry)
lower = self._s2_low - pip_size * float(self.PipsForEntry)
return True, upper, lower
if self._s1_completed and self._s1_high is not None and self._s1_low is not None:
upper = self._s1_high + pip_size * float(self.PipsForEntry)
lower = self._s1_low - pip_size * float(self.PipsForEntry)
return True, upper, lower
return False, 0.0, 0.0
def _get_pip_size(self):
sec = self.Security
if sec is not None and sec.PriceStep is not None:
step = float(sec.PriceStep)
if step > 0.0:
return step
return 0.01
def _reset_position_state(self):
self._entry_time = None
self._stop_price = None
self._take_price = None
def _trim_history(self):
max_len = 1024
if len(self._color_history) > max_len:
excess = len(self._color_history) - max_len
del self._color_history[:excess]
@staticmethod
def _is_upper_breakout(color):
return color == 0 or color == 1
@staticmethod
def _is_lower_breakout(color):
return color == 3 or color == 4
def CreateClone(self):
return exp_hans_indicator_cloud_system_tm_plus_strategy()