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>
/// Evening Star candlestick pattern strategy converted from MQL5 implementation.
/// </summary>
public class EveningStarReversalStrategy : Strategy
{
public enum PatternDirections
{
Long,
Short
}
private readonly StrategyParam<PatternDirections> _direction;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _shift;
private readonly StrategyParam<bool> _considerGap;
private readonly StrategyParam<bool> _candle2Bullish;
private readonly StrategyParam<bool> _checkCandleSizes;
private readonly StrategyParam<bool> _closeOpposite;
private readonly StrategyParam<DataType> _candleType;
private readonly List<CandleSnapshot> _history = new();
private decimal _pipSize;
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _takeProfitPrice;
public PatternDirections Direction
{
get => _direction.Value;
set => _direction.Value = value;
}
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public int Shift
{
get => _shift.Value;
set => _shift.Value = value;
}
public bool ConsiderGap
{
get => _considerGap.Value;
set => _considerGap.Value = value;
}
public bool Candle2Bullish
{
get => _candle2Bullish.Value;
set => _candle2Bullish.Value = value;
}
public bool CheckCandleSizes
{
get => _checkCandleSizes.Value;
set => _checkCandleSizes.Value = value;
}
public bool CloseOppositePositions
{
get => _closeOpposite.Value;
set => _closeOpposite.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public EveningStarReversalStrategy()
{
_direction = Param(nameof(Direction), PatternDirections.Short)
.SetDisplay("Signal Direction", "Side to trade when the pattern appears", "General");
_takeProfitPips = Param(nameof(TakeProfitPips), 150)
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk Management")
.SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk Management")
.SetGreaterThanZero();
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetDisplay("Risk (%)", "Risk per trade as percentage of equity", "Risk Management")
.SetGreaterThanZero();
_shift = Param(nameof(Shift), 1)
.SetDisplay("Shift", "Offset for the bar sequence", "Pattern")
.SetGreaterThanZero();
_considerGap = Param(nameof(ConsiderGap), true)
.SetDisplay("Consider Gap", "Require price gaps between candles", "Pattern");
_candle2Bullish = Param(nameof(Candle2Bullish), true)
.SetDisplay("Middle Candle Bullish", "Should the second candle close above its open", "Pattern");
_checkCandleSizes = Param(nameof(CheckCandleSizes), true)
.SetDisplay("Check Candle Sizes", "Ensure the middle candle has the smallest body", "Pattern");
_closeOpposite = Param(nameof(CloseOppositePositions), true)
.SetDisplay("Close Opposite", "Close the existing opposite position before entry", "Execution");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series to process", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_pipSize = 0m;
_entryPrice = 0m;
_stopPrice = 0m;
_takeProfitPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
// no protection needed
}
private void ProcessCandle(ICandleMessage candle)
{
// Ensure we only process finished candles.
if (candle.State != CandleStates.Finished)
return;
// Store the candle snapshot for pattern evaluation.
_history.Add(new CandleSnapshot(candle.OpenPrice, candle.ClosePrice, candle.HighPrice, candle.LowPrice));
TrimHistory();
// Manage any open trade before searching for a new signal.
HandleActivePosition(candle);
//if (!IsFormedAndOnlineAndAllowTrading())
//return;
// The pattern requires three completed candles with the configured shift.
var requiredCount = Shift + 2;
if (_history.Count < requiredCount)
return;
var lastIndex = _history.Count - Shift;
if (lastIndex < 2 || lastIndex >= _history.Count)
return;
var recent = _history[lastIndex];
var middle = _history[lastIndex - 1];
var first = _history[lastIndex - 2];
// Validate the Evening Star structure and optional filters.
if (!IsPatternValid(first, middle, recent))
return;
var isLong = Direction == PatternDirections.Long;
var entryPrice = recent.Close;
var stopPrice = CalculateStop(entryPrice, isLong);
var takeProfitPrice = CalculateTake(entryPrice, isLong);
// Size the position using the risk percentage from the portfolio value.
var volume = CalculatePositionSize(entryPrice, stopPrice);
if (volume <= 0m)
return;
if (isLong)
{
if (Position < 0 && !CloseOppositePositions)
return;
if (Position < 0 && CloseOppositePositions)
BuyMarket();
BuyMarket();
_entryPrice = entryPrice;
_stopPrice = stopPrice;
_takeProfitPrice = takeProfitPrice;
}
else
{
if (Position > 0 && !CloseOppositePositions)
return;
if (Position > 0 && CloseOppositePositions)
SellMarket();
SellMarket();
_entryPrice = entryPrice;
_stopPrice = stopPrice;
_takeProfitPrice = takeProfitPrice;
}
}
private void HandleActivePosition(ICandleMessage candle)
{
if (Position == 0)
{
// Nothing is open, so cached targets must be cleared.
ResetTargets();
return;
}
if (Position > 0)
{
var stopHit = _stopPrice > 0m && candle.LowPrice <= _stopPrice;
var takeHit = _takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice;
if (stopHit || takeHit)
{
SellMarket();
ResetTargets();
}
}
else if (Position < 0)
{
var stopHit = _stopPrice > 0m && candle.HighPrice >= _stopPrice;
var takeHit = _takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice;
if (stopHit || takeHit)
{
BuyMarket();
ResetTargets();
}
}
}
private bool IsPatternValid(CandleSnapshot first, CandleSnapshot middle, CandleSnapshot recent)
{
// Evening Star requires a bullish candle, a small-bodied candle, then a bearish candle.
if (!(recent.Open > recent.Close && first.Open < first.Close))
return false;
if (CheckCandleSizes)
{
var lastBody = Math.Abs(recent.Open - recent.Close);
var middleBody = Math.Abs(middle.Open - middle.Close);
var firstBody = Math.Abs(first.Open - first.Close);
if (lastBody < middleBody || firstBody < middleBody)
return false;
}
if (Candle2Bullish)
{
if (middle.Open > middle.Close)
return false;
}
else
{
if (middle.Close > middle.Open)
return false;
}
if (ConsiderGap && _pipSize > 0m)
{
var gap = _pipSize;
if (recent.Open >= middle.Close - gap || middle.Open <= first.Close + gap)
return false;
}
return true;
}
private decimal CalculateStop(decimal entryPrice, bool isLong)
{
var distance = StopLossPips * _pipSize;
if (distance <= 0m)
return 0m;
return isLong ? entryPrice - distance : entryPrice + distance;
}
private decimal CalculateTake(decimal entryPrice, bool isLong)
{
var distance = TakeProfitPips * _pipSize;
if (distance <= 0m)
return 0m;
return isLong ? entryPrice + distance : entryPrice - distance;
}
private decimal CalculatePositionSize(decimal entryPrice, decimal stopPrice)
{
// Simplified: always return Volume (from base Strategy)
return Volume;
}
private decimal CalculatePipSize()
{
var step = Security.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var decimals = Security.Decimals;
// Forex symbols use fractional pips; replicate the 3/5 digit adjustment from MQL.
return decimals is 3 or 5 ? step * 10m : step;
}
private void TrimHistory()
{
// Keep only the most recent candles needed for pattern detection.
var maxCount = Math.Max(Shift + 5, 10);
if (_history.Count <= maxCount)
return;
while (_history.Count > maxCount)
try { _history.RemoveAt(0); } catch { break; }
}
private void ResetTargets()
{
_entryPrice = 0m;
_stopPrice = 0m;
_takeProfitPrice = 0m;
}
// Lightweight snapshot to keep only the data required for pattern checks.
private readonly struct CandleSnapshot
{
public CandleSnapshot(decimal open, decimal close, decimal high, decimal low)
{
Open = open;
Close = close;
High = high;
Low = low;
}
public decimal Open { get; }
public decimal Close { get; }
public decimal High { get; }
public decimal Low { 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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class evening_star_reversal_strategy(Strategy):
"""Evening Star candlestick pattern strategy with SL/TP."""
def __init__(self):
super(evening_star_reversal_strategy, self).__init__()
self._direction_long = self.Param("DirectionLong", False) \
.SetDisplay("Long Direction", "True=trade long on pattern, False=short", "General")
self._take_profit_pips = self.Param("TakeProfitPips", 150) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "TP distance in pips", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "SL distance in pips", "Risk")
self._shift = self.Param("Shift", 1) \
.SetGreaterThanZero() \
.SetDisplay("Shift", "Offset for the bar sequence", "Pattern")
self._consider_gap = self.Param("ConsiderGap", True) \
.SetDisplay("Consider Gap", "Require price gaps between candles", "Pattern")
self._candle2_bullish = self.Param("Candle2Bullish", True) \
.SetDisplay("Middle Candle Bullish", "Should second candle close above open", "Pattern")
self._check_sizes = self.Param("CheckCandleSizes", True) \
.SetDisplay("Check Candle Sizes", "Ensure middle candle smallest body", "Pattern")
self._close_opposite = self.Param("CloseOpposite", True) \
.SetDisplay("Close Opposite", "Close opposite position before entry", "Execution")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle series to process", "General")
self._history = []
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
@property
def DirectionLong(self):
return self._direction_long.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def Shift(self):
return self._shift.Value
@property
def ConsiderGap(self):
return self._consider_gap.Value
@property
def Candle2Bullish(self):
return self._candle2_bullish.Value
@property
def CheckCandleSizes(self):
return self._check_sizes.Value
@property
def CloseOpposite(self):
return self._close_opposite.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 0.0
step = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(evening_star_reversal_strategy, self).OnStarted2(time)
self._pip_size = self._calc_pip_size()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
o = float(candle.OpenPrice)
c = float(candle.ClosePrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
self._history.append((o, c, h, lo))
max_count = max(self.Shift + 5, 10)
while len(self._history) > max_count:
self._history.pop(0)
# Manage existing position
self._handle_active_position(candle)
required = self.Shift + 2
if len(self._history) < required:
return
last_idx = len(self._history) - self.Shift
if last_idx < 2 or last_idx >= len(self._history):
return
recent = self._history[last_idx]
middle = self._history[last_idx - 1]
first = self._history[last_idx - 2]
if not self._is_pattern_valid(first, middle, recent):
return
is_long = self.DirectionLong
entry = recent[1] # close
stop = self._calc_stop(entry, is_long)
take = self._calc_take(entry, is_long)
if is_long:
if self.Position < 0 and not self.CloseOpposite:
return
if self.Position < 0 and self.CloseOpposite:
self.BuyMarket()
self.BuyMarket()
self._entry_price = entry
self._stop_price = stop
self._take_profit_price = take
else:
if self.Position > 0 and not self.CloseOpposite:
return
if self.Position > 0 and self.CloseOpposite:
self.SellMarket()
self.SellMarket()
self._entry_price = entry
self._stop_price = stop
self._take_profit_price = take
def _handle_active_position(self, candle):
if self.Position == 0:
self._reset_targets()
return
if self.Position > 0:
stop_hit = self._stop_price > 0 and float(candle.LowPrice) <= self._stop_price
take_hit = self._take_profit_price > 0 and float(candle.HighPrice) >= self._take_profit_price
if stop_hit or take_hit:
self.SellMarket()
self._reset_targets()
elif self.Position < 0:
stop_hit = self._stop_price > 0 and float(candle.HighPrice) >= self._stop_price
take_hit = self._take_profit_price > 0 and float(candle.LowPrice) <= self._take_profit_price
if stop_hit or take_hit:
self.BuyMarket()
self._reset_targets()
def _is_pattern_valid(self, first, middle, recent):
# Evening Star: bullish candle, small-body candle, bearish candle
# recent: (o,c,h,lo)
if not (recent[0] > recent[1] and first[0] < first[1]):
return False
if self.CheckCandleSizes:
last_body = abs(recent[0] - recent[1])
mid_body = abs(middle[0] - middle[1])
first_body = abs(first[0] - first[1])
if last_body < mid_body or first_body < mid_body:
return False
if self.Candle2Bullish:
if middle[0] > middle[1]:
return False
else:
if middle[1] > middle[0]:
return False
if self.ConsiderGap and self._pip_size > 0:
gap = self._pip_size
if recent[0] >= middle[1] - gap or middle[0] <= first[1] + gap:
return False
return True
def _calc_stop(self, entry, is_long):
dist = self.StopLossPips * self._pip_size
if dist <= 0:
return 0.0
return entry - dist if is_long else entry + dist
def _calc_take(self, entry, is_long):
dist = self.TakeProfitPips * self._pip_size
if dist <= 0:
return 0.0
return entry + dist if is_long else entry - dist
def _reset_targets(self):
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
def OnReseted(self):
super(evening_star_reversal_strategy, self).OnReseted()
self._history = []
self._pip_size = 0.0
self._reset_targets()
def CreateClone(self):
return evening_star_reversal_strategy()