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>
/// 5/8 exponential moving average crossover strategy converted from MetaTrader.
/// Uses a fast EMA on close prices and a slower EMA on open prices with manual stop, take profit, and trailing logic.
/// </summary>
public class FiveEightMaCrossStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _fastMa = null!;
private ExponentialMovingAverage _slowMa = null!;
private decimal _prevFast;
private decimal _prevSlow;
private bool _isInitialized;
private decimal _pointValue;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _trailDistance;
private decimal _maxPrice;
private decimal _minPrice;
/// <summary>
/// Fast EMA length.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Slow EMA length.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// Take profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="FiveEightMaCrossStrategy"/>.
/// </summary>
public FiveEightMaCrossStrategy()
{
_fastLength = Param(nameof(FastLength), 8)
.SetGreaterThanZero()
.SetDisplay("Fast EMA Length", "Length of the EMA calculated on closing prices", "Indicators")
.SetOptimize(3, 20, 1);
_slowLength = Param(nameof(SlowLength), 21)
.SetGreaterThanZero()
.SetDisplay("Slow EMA Length", "Length of the EMA calculated on opening prices", "Indicators")
.SetOptimize(5, 30, 1);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 40m)
.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(10m, 100m, 10m);
_stopLossPoints = Param(nameof(StopLossPoints), 0m)
.SetDisplay("Stop Loss (points)", "Stop loss distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(0m, 100m, 10m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 0m)
.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(0m, 100m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for calculations", "General");
Volume = 0.1m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null!;
_slowMa = null!;
_prevFast = 0m;
_prevSlow = 0m;
_isInitialized = false;
_pointValue = 0m;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = new EMA { Length = FastLength };
_slowMa = new EMA { Length = SlowLength };
_pointValue = CalculatePointValue();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private decimal CalculatePointValue()
{
var step = Security?.PriceStep;
if (step == null || step.Value <= 0m)
return 1m;
var stepValue = step.Value;
var stepDouble = (double)stepValue;
if (stepDouble <= 0d)
return stepValue;
var digitsDouble = Math.Log10(1d / stepDouble);
var digits = (int)Math.Round(digitsDouble, MidpointRounding.AwayFromZero);
var multiplier = (digits == 3 || digits == 5) ? 10m : 1m;
return stepValue * multiplier;
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only finished candles to avoid repainting.
if (candle.State != CandleStates.Finished)
return;
// Feed indicators with the corresponding price source.
var fastValue = _fastMa.Process(new DecimalIndicatorValue(_fastMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
var slowValue = _slowMa.Process(new DecimalIndicatorValue(_slowMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
if (!_fastMa.IsFormed || !_slowMa.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
HandleRiskManagement(candle);
// indicators are processed manually
if (!_isInitialized)
{
_prevFast = fastValue;
_prevSlow = slowValue;
_isInitialized = true;
return;
}
var crossUp = _prevFast <= _prevSlow && fastValue > slowValue;
var crossDown = _prevFast >= _prevSlow && fastValue < slowValue;
if (crossUp && Position <= 0)
{
EnterLong(candle);
}
else if (crossDown && Position >= 0)
{
EnterShort(candle);
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
private void EnterLong(ICandleMessage candle)
{
var positionVolume = Position < 0 ? Math.Abs(Position) : 0m;
var volume = Volume + positionVolume;
if (volume <= 0m)
return;
ResetPositionState();
// Enter long position with volume including any short covering.
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_takePrice = TakeProfitPoints > 0m ? _entryPrice + TakeProfitPoints * _pointValue : null;
_stopPrice = StopLossPoints > 0m ? _entryPrice - StopLossPoints * _pointValue : null;
_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
_maxPrice = candle.HighPrice;
_minPrice = candle.LowPrice;
if (_trailDistance > 0m)
{
var trailStart = _entryPrice.Value - _trailDistance;
if (_stopPrice == null || trailStart > _stopPrice.Value)
_stopPrice = trailStart;
}
}
private void EnterShort(ICandleMessage candle)
{
var positionVolume = Position > 0 ? Position : 0m;
var volume = Volume + positionVolume;
if (volume <= 0m)
return;
ResetPositionState();
// Enter short position with volume including any long exit.
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_takePrice = TakeProfitPoints > 0m ? _entryPrice - TakeProfitPoints * _pointValue : null;
_stopPrice = StopLossPoints > 0m ? _entryPrice + StopLossPoints * _pointValue : null;
_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
_maxPrice = candle.HighPrice;
_minPrice = candle.LowPrice;
if (_trailDistance > 0m)
{
var trailStart = _entryPrice.Value + _trailDistance;
if (_stopPrice == null || trailStart < _stopPrice.Value)
_stopPrice = trailStart;
}
}
private void HandleRiskManagement(ICandleMessage candle)
{
if (Position > 0 && _entryPrice.HasValue)
{
// Update trailing stop using the highest price reached since entry.
_maxPrice = Math.Max(_maxPrice, candle.HighPrice);
if (_trailDistance > 0m)
{
var trailCandidate = _maxPrice - _trailDistance;
if (_stopPrice == null || trailCandidate > _stopPrice.Value)
_stopPrice = trailCandidate;
}
if (_takePrice.HasValue && candle.ClosePrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.ClosePrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPositionState();
return;
}
}
else if (Position < 0 && _entryPrice.HasValue)
{
// Update trailing stop using the lowest price reached since entry.
_minPrice = Math.Min(_minPrice, candle.LowPrice);
if (_trailDistance > 0m)
{
var trailCandidate = _minPrice + _trailDistance;
if (_stopPrice == null || trailCandidate < _stopPrice.Value)
_stopPrice = trailCandidate;
}
if (_takePrice.HasValue && candle.ClosePrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.ClosePrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
}
}
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_trailDistance = 0m;
_maxPrice = 0m;
_minPrice = 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 System import Decimal
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class five_eight_ma_cross_strategy(Strategy):
def __init__(self):
super(five_eight_ma_cross_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 8)
self._slow_length = self.Param("SlowLength", 21)
self._take_profit_points = self.Param("TakeProfitPoints", 40.0)
self._stop_loss_points = self.Param("StopLossPoints", 0.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 0.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = 1.0
self._entry_price = None
self._stop_price = None
self._take_price = None
self._trail_distance = 0.0
self._max_price = 0.0
self._min_price = 0.0
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.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 StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@TrailingStopPoints.setter
def TrailingStopPoints(self, value):
self._trailing_stop_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(five_eight_ma_cross_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = self._calculate_point_value()
self._reset_position_state()
self._fast_ma = ExponentialMovingAverage()
self._fast_ma.Length = self.FastLength
self._slow_ma = ExponentialMovingAverage()
self._slow_ma.Length = self.SlowLength
self.Volume = 0.1
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def _calculate_point_value(self):
if self.Security is None or self.Security.PriceStep is None:
return 1.0
step = float(self.Security.PriceStep)
if step <= 0.0:
return 1.0
import math
digits = int(round(math.log10(1.0 / step)))
multiplier = 10.0 if (digits == 3 or digits == 5) else 1.0
return step * multiplier
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
fast_result = process_float(self._fast_ma, candle.ClosePrice, candle.OpenTime, True)
slow_result = process_float(self._slow_ma, candle.OpenPrice, candle.OpenTime, True)
fast_val = fast_result.Value
slow_val = slow_result.Value
if not self._fast_ma.IsFormed or not self._slow_ma.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
self._handle_risk_management(candle)
if not self._is_initialized:
self._prev_fast = fast_val
self._prev_slow = slow_val
self._is_initialized = True
return
cross_up = self._prev_fast <= self._prev_slow and fast_val > slow_val
cross_down = self._prev_fast >= self._prev_slow and fast_val < slow_val
if cross_up and self.Position <= 0:
self._enter_long(candle)
elif cross_down and self.Position >= 0:
self._enter_short(candle)
self._prev_fast = fast_val
self._prev_slow = slow_val
def _enter_long(self, candle):
pos_vol = abs(float(self.Position)) if self.Position < 0 else 0.0
volume = float(self.Volume) + pos_vol
if volume <= 0:
return
self._reset_position_state()
self.BuyMarket(volume)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_pts = float(self.TakeProfitPoints)
sl_pts = float(self.StopLossPoints)
trail_pts = float(self.TrailingStopPoints)
self._entry_price = close
self._take_price = close + tp_pts * self._point_value if tp_pts > 0.0 else None
self._stop_price = close - sl_pts * self._point_value if sl_pts > 0.0 else None
self._trail_distance = trail_pts * self._point_value if trail_pts > 0.0 else 0.0
self._max_price = high
self._min_price = low
if self._trail_distance > 0.0:
trail_start = close - self._trail_distance
if self._stop_price is None or trail_start > self._stop_price:
self._stop_price = trail_start
def _enter_short(self, candle):
pos_vol = float(self.Position) if self.Position > 0 else 0.0
volume = float(self.Volume) + pos_vol
if volume <= 0:
return
self._reset_position_state()
self.SellMarket(volume)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_pts = float(self.TakeProfitPoints)
sl_pts = float(self.StopLossPoints)
trail_pts = float(self.TrailingStopPoints)
self._entry_price = close
self._take_price = close - tp_pts * self._point_value if tp_pts > 0.0 else None
self._stop_price = close + sl_pts * self._point_value if sl_pts > 0.0 else None
self._trail_distance = trail_pts * self._point_value if trail_pts > 0.0 else 0.0
self._max_price = high
self._min_price = low
if self._trail_distance > 0.0:
trail_start = close + self._trail_distance
if self._stop_price is None or trail_start < self._stop_price:
self._stop_price = trail_start
def _handle_risk_management(self, candle):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0 and self._entry_price is not None:
self._max_price = max(self._max_price, high)
if self._trail_distance > 0.0:
trail_candidate = self._max_price - self._trail_distance
if self._stop_price is None or trail_candidate > self._stop_price:
self._stop_price = trail_candidate
if self._take_price is not None and close >= self._take_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position_state()
return
if self._stop_price is not None and close <= self._stop_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position_state()
return
elif self.Position < 0 and self._entry_price is not None:
self._min_price = min(self._min_price, low)
if self._trail_distance > 0.0:
trail_candidate = self._min_price + self._trail_distance
if self._stop_price is None or trail_candidate < self._stop_price:
self._stop_price = trail_candidate
if self._take_price is not None and close <= self._take_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position_state()
return
if self._stop_price is not None and close >= self._stop_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
self._trail_distance = 0.0
self._max_price = 0.0
self._min_price = 0.0
def OnReseted(self):
super(five_eight_ma_cross_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = 1.0
self._reset_position_state()
def CreateClone(self):
return five_eight_ma_cross_strategy()