This strategy ports the MetaTrader 5 expert advisor Expert_AMS_ES_Stoch (Morning/Evening Star with Stochastic confirmation) into StockSharp. The implementation keeps the original candlestick pattern recognition and stochastic confirmation rules while using the high-level candle subscription API so every decision is made on finished bars.
Strategy Logic
Indicators
Standard Stochastic oscillator with configurable %K, %D and slowing periods.
Simple moving average of the candle body size (absolute open-close) to classify candles as "long" or "small" just like the MQL version.
Long Entry
Morning Star pattern across the last three completed candles:
Two bars ago: long bearish body whose size exceeds the body average.
Previous bar: small-bodied candle that closes and opens below the prior candle.
Current bar: bullish close above the midpoint of the first candle.
Stochastic signal line (%D) is below the oversold threshold (default 30).
Existing short exposure is flattened before opening the long position.
Short Entry
Evening Star pattern mirroring the rules above.
Stochastic %D is above the overbought threshold (default 70).
Existing long exposure is closed before opening the short trade.
Position Exit
Shorts are closed when %D crosses above either the fast recovery level (20) or the extreme level (80).
Longs are closed when %D crosses below either 80 or 20.
These crossings reproduce the "close conditions" from the MQL signal module.
Parameters
Name
Description
CandleType
Timeframe (or other DataType) used for pattern detection and all indicators.
%K, %D and slowing periods of the stochastic oscillator.
StochasticOverbought, StochasticOversold
Signal-line thresholds used to confirm Evening/Morning Star entries.
PatternAveragePeriod
Number of finished candles used to average the body size (|open-close|).
ShortExitLevel, LongExitLevel
%D levels that force short/long exits when crossed in the opposite direction.
Implementation Notes
Candles are processed through SubscribeCandles().BindEx(...); the code only works with finished candles and never calls GetValue() on indicators.
Body-size averaging relies on SimpleMovingAverage fed with absolute candle bodies to reproduce the AvgBody() helper from the MQL library.
Pattern checks are implemented with dedicated helper methods to keep the decision logic readable and to mirror the original CCandlePattern rules.
Before entering in the opposite direction the strategy closes any existing exposure to match the Expert Advisor's behaviour of operating one net position at a time.
Differences from the MQL5 Expert
Money management, trailing stop and fixed lot settings from the MetaTrader framework are not reproduced; StockSharp order volume is controlled by the strategy Volume property.
The Stochastic oscillator uses StockSharp's indicator implementation; thresholds remain configurable so you can fine-tune the behaviour if the original broker feed produced slightly different values.
Logging provides detailed explanations (in English) for every entry and exit to aid debugging and backtesting.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Morning/Evening Star pattern strategy with Stochastic confirmation.
/// Buys on morning star + oversold stochastic, sells on evening star + overbought stochastic.
/// </summary>
public class MorningEveningStochasticStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _stochPeriod;
private readonly StrategyParam<decimal> _oversold;
private readonly StrategyParam<decimal> _overbought;
private readonly StrategyParam<int> _signalCooldownCandles;
private readonly List<ICandleMessage> _candles = new();
private decimal _prevK;
private bool _hasPrevK;
private int _candlesSinceTrade;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int StochPeriod { get => _stochPeriod.Value; set => _stochPeriod.Value = value; }
public decimal Oversold { get => _oversold.Value; set => _oversold.Value = value; }
public decimal Overbought { get => _overbought.Value; set => _overbought.Value = value; }
public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }
public MorningEveningStochasticStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_stochPeriod = Param(nameof(StochPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Stoch Period", "Stochastic K period", "Indicators");
_oversold = Param(nameof(Oversold), 30m)
.SetDisplay("Oversold", "Stochastic oversold level", "Signals");
_overbought = Param(nameof(Overbought), 70m)
.SetDisplay("Overbought", "Stochastic overbought level", "Signals");
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 6)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_candles.Clear();
_prevK = 0m;
_hasPrevK = false;
_candlesSinceTrade = SignalCooldownCandles;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_candles.Clear();
_hasPrevK = false;
_candlesSinceTrade = SignalCooldownCandles;
var stoch = new StochasticOscillator { K = { Length = StochPeriod }, D = { Length = 3 } };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(stoch, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochValue)
{
if (candle.State != CandleStates.Finished) return;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
var stochTyped = stochValue as StochasticOscillatorValue;
if (stochTyped?.K is not decimal kValue) return;
_candles.Add(candle);
if (_candles.Count > 5)
_candles.RemoveAt(0);
if (_candles.Count >= 3)
{
var c3 = _candles[^1]; // current
var c2 = _candles[^2]; // middle (star)
var c1 = _candles[^3]; // first
var body1 = Math.Abs(c1.ClosePrice - c1.OpenPrice);
var body2 = Math.Abs(c2.ClosePrice - c2.OpenPrice);
var body3 = Math.Abs(c3.ClosePrice - c3.OpenPrice);
// Morning Star: bearish + small body + bullish, close above midpoint of first
var isMorningStar = c1.OpenPrice > c1.ClosePrice // first bearish
&& body2 < body1 * 0.5m // small middle body
&& c3.ClosePrice > c3.OpenPrice // third bullish
&& c3.ClosePrice > (c1.OpenPrice + c1.ClosePrice) / 2m;
// Evening Star: bullish + small body + bearish, close below midpoint of first
var isEveningStar = c1.ClosePrice > c1.OpenPrice // first bullish
&& body2 < body1 * 0.5m // small middle body
&& c3.OpenPrice > c3.ClosePrice // third bearish
&& c3.ClosePrice < (c1.OpenPrice + c1.ClosePrice) / 2m;
if (isMorningStar && kValue < Oversold && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
{
BuyMarket();
_candlesSinceTrade = 0;
}
else if (isEveningStar && kValue > Overbought && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
{
SellMarket();
_candlesSinceTrade = 0;
}
}
// Exit on stochastic cross
if (_hasPrevK)
{
if (Position > 0 && _prevK >= Overbought && kValue < Overbought && _candlesSinceTrade >= SignalCooldownCandles)
{
SellMarket();
_candlesSinceTrade = 0;
}
else if (Position < 0 && _prevK <= Oversold && kValue > Oversold && _candlesSinceTrade >= SignalCooldownCandles)
{
BuyMarket();
_candlesSinceTrade = 0;
}
}
_prevK = kValue;
_hasPrevK = true;
}
}
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.Indicators import StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class morning_evening_stochastic_strategy(Strategy):
def __init__(self):
super(morning_evening_stochastic_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._stoch_period = self.Param("StochPeriod", 14)
self._oversold = self.Param("Oversold", 30.0)
self._overbought = self.Param("Overbought", 70.0)
self._signal_cooldown_candles = self.Param("SignalCooldownCandles", 6)
self._candles = []
self._prev_k = 0.0
self._has_prev_k = False
self._candles_since_trade = 6
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StochPeriod(self):
return self._stoch_period.Value
@StochPeriod.setter
def StochPeriod(self, value):
self._stoch_period.Value = value
@property
def Oversold(self):
return self._oversold.Value
@Oversold.setter
def Oversold(self, value):
self._oversold.Value = value
@property
def Overbought(self):
return self._overbought.Value
@Overbought.setter
def Overbought(self, value):
self._overbought.Value = value
@property
def SignalCooldownCandles(self):
return self._signal_cooldown_candles.Value
@SignalCooldownCandles.setter
def SignalCooldownCandles(self, value):
self._signal_cooldown_candles.Value = value
def OnReseted(self):
super(morning_evening_stochastic_strategy, self).OnReseted()
self._candles.clear()
self._prev_k = 0.0
self._has_prev_k = False
self._candles_since_trade = self.SignalCooldownCandles
def OnStarted2(self, time):
super(morning_evening_stochastic_strategy, self).OnStarted2(time)
self._candles.clear()
self._has_prev_k = False
self._candles_since_trade = self.SignalCooldownCandles
stoch = StochasticOscillator()
stoch.K.Length = self.StochPeriod
stoch.D.Length = 3
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(stoch, self._process_candle).Start()
def _process_candle(self, candle, stoch_value):
if candle.State != CandleStates.Finished:
return
if self._candles_since_trade < self.SignalCooldownCandles:
self._candles_since_trade += 1
k_val = stoch_value.K
if k_val is None:
return
k_value = float(k_val)
self._candles.append(candle)
if len(self._candles) > 5:
self._candles.pop(0)
if len(self._candles) >= 3:
c3 = self._candles[-1]
c2 = self._candles[-2]
c1 = self._candles[-3]
body1 = abs(float(c1.ClosePrice) - float(c1.OpenPrice))
body2 = abs(float(c2.ClosePrice) - float(c2.OpenPrice))
is_morning_star = (float(c1.OpenPrice) > float(c1.ClosePrice)
and body2 < body1 * 0.5
and float(c3.ClosePrice) > float(c3.OpenPrice)
and float(c3.ClosePrice) > (float(c1.OpenPrice) + float(c1.ClosePrice)) / 2.0)
is_evening_star = (float(c1.ClosePrice) > float(c1.OpenPrice)
and body2 < body1 * 0.5
and float(c3.OpenPrice) > float(c3.ClosePrice)
and float(c3.ClosePrice) < (float(c1.OpenPrice) + float(c1.ClosePrice)) / 2.0)
if is_morning_star and k_value < self.Oversold and self.Position <= 0 and self._candles_since_trade >= self.SignalCooldownCandles:
self.BuyMarket()
self._candles_since_trade = 0
elif is_evening_star and k_value > self.Overbought and self.Position >= 0 and self._candles_since_trade >= self.SignalCooldownCandles:
self.SellMarket()
self._candles_since_trade = 0
if self._has_prev_k:
if self.Position > 0 and self._prev_k >= self.Overbought and k_value < self.Overbought and self._candles_since_trade >= self.SignalCooldownCandles:
self.SellMarket()
self._candles_since_trade = 0
elif self.Position < 0 and self._prev_k <= self.Oversold and k_value > self.Oversold and self._candles_since_trade >= self.SignalCooldownCandles:
self.BuyMarket()
self._candles_since_trade = 0
self._prev_k = k_value
self._has_prev_k = True
def CreateClone(self):
return morning_evening_stochastic_strategy()