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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Moving average crossover strategy with adjustable gap, session control, and optional trailing stop.
/// </summary>
public class AdjustableMovingAverageStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<MovingAverageMethods> _maMethod;
private readonly StrategyParam<decimal> _minGapPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingPoints;
private readonly StrategyParam<EntryModes> _entryMode;
private readonly StrategyParam<TimeSpan> _sessionStart;
private readonly StrategyParam<TimeSpan> _sessionEnd;
private readonly StrategyParam<bool> _closeOutsideSession;
private readonly StrategyParam<bool> _trailOutsideSession;
private readonly StrategyParam<decimal> _fixedLot;
private readonly StrategyParam<bool> _enableAutoLot;
private readonly StrategyParam<decimal> _lotPer10k;
private readonly StrategyParam<int> _maxSlippage;
private readonly StrategyParam<string> _tradeComment;
private DecimalLengthIndicator _fastMa;
private DecimalLengthIndicator _slowMa;
private decimal _pointValue;
private decimal _minGapThreshold;
private int _previousSignal;
private bool _hasInitialSignal;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public AdjustableMovingAverageStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle timeframe", "Timeframe used to build moving averages", "General")
;
_fastPeriod = Param(nameof(FastPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast period", "Short moving average length", "Moving averages")
.SetOptimize(2, 30, 1);
_slowPeriod = Param(nameof(SlowPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("Slow period", "Long moving average length", "Moving averages")
.SetOptimize(3, 60, 1);
_maMethod = Param(nameof(MaMethod), MovingAverageMethods.Exponential)
.SetDisplay("MA method", "Moving average calculation method", "Moving averages")
;
_minGapPoints = Param(nameof(MinGapPoints), 3m)
.SetNotNegative()
.SetDisplay("Minimum gap (points)", "Required distance between fast and slow MAs before signalling", "Trading")
.SetOptimize(0m, 20m, 1m);
_stopLossPoints = Param(nameof(StopLossPoints), 0m)
.SetNotNegative()
.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
.SetNotNegative()
.SetDisplay("Take profit (points)", "Profit target distance in price points", "Risk management");
_trailingPoints = Param(nameof(TrailStopPoints), 0m)
.SetNotNegative()
.SetDisplay("Trailing stop (points)", "Trailing stop distance in price points", "Risk management");
_entryMode = Param(nameof(Mode), EntryModes.Both)
.SetDisplay("Entry mode", "Allowed trade direction", "Trading");
_sessionStart = Param(nameof(SessionStart), TimeSpan.Zero)
.SetDisplay("Session start", "Trading session start time (platform time)", "Session");
_sessionEnd = Param(nameof(SessionEnd), new TimeSpan(23, 59, 0))
.SetDisplay("Session end", "Trading session end time (platform time)", "Session");
_closeOutsideSession = Param(nameof(CloseOutsideSession), true)
.SetDisplay("Close outside session", "Allow closing positions when the session filter is inactive", "Session");
_trailOutsideSession = Param(nameof(TrailOutsideSession), true)
.SetDisplay("Trail outside session", "Continue trailing even when trading session is closed", "Session");
_fixedLot = Param(nameof(FixedLot), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Fixed lot", "Volume used when auto lot sizing is disabled", "Money management");
_enableAutoLot = Param(nameof(EnableAutoLot), false)
.SetDisplay("Enable auto lot", "Approximate AccountFreeMargin based sizing", "Money management");
_lotPer10k = Param(nameof(LotPer10kFreeMargin), 1m)
.SetGreaterThanZero()
.SetDisplay("Lots per 10k", "Lots per 10,000 of account value when auto lot is enabled", "Money management");
_maxSlippage = Param(nameof(MaxSlippage), 3)
.SetNotNegative()
.SetDisplay("Max slippage", "Placeholder parameter retained from the MQL version", "Trading");
_tradeComment = Param(nameof(TradeComment), "AdjustableMovingAverageEA")
.SetDisplay("Trade comment", "Tag applied to diagnostic messages", "General");
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Fast moving average length.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow moving average length.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Moving average calculation method.
/// </summary>
public MovingAverageMethods MaMethod
{
get => _maMethod.Value;
set => _maMethod.Value = value;
}
/// <summary>
/// Minimum distance between fast and slow moving averages in instrument points.
/// </summary>
public decimal MinGapPoints
{
get => _minGapPoints.Value;
set => _minGapPoints.Value = value;
}
/// <summary>
/// Stop-loss distance in instrument points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in instrument points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in instrument points.
/// </summary>
public decimal TrailStopPoints
{
get => _trailingPoints.Value;
set => _trailingPoints.Value = value;
}
/// <summary>
/// Allowed trade direction.
/// </summary>
public EntryModes Mode
{
get => _entryMode.Value;
set => _entryMode.Value = value;
}
/// <summary>
/// Session start time in platform time zone.
/// </summary>
public TimeSpan SessionStart
{
get => _sessionStart.Value;
set => _sessionStart.Value = value;
}
/// <summary>
/// Session end time in platform time zone.
/// </summary>
public TimeSpan SessionEnd
{
get => _sessionEnd.Value;
set => _sessionEnd.Value = value;
}
/// <summary>
/// Close positions even when the session filter is inactive.
/// </summary>
public bool CloseOutsideSession
{
get => _closeOutsideSession.Value;
set => _closeOutsideSession.Value = value;
}
/// <summary>
/// Continue updating the trailing stop outside the session window.
/// </summary>
public bool TrailOutsideSession
{
get => _trailOutsideSession.Value;
set => _trailOutsideSession.Value = value;
}
/// <summary>
/// Fixed order volume used when auto lot sizing is disabled.
/// </summary>
public decimal FixedLot
{
get => _fixedLot.Value;
set => _fixedLot.Value = value;
}
/// <summary>
/// Toggle automatic lot sizing based on approximate free margin.
/// </summary>
public bool EnableAutoLot
{
get => _enableAutoLot.Value;
set => _enableAutoLot.Value = value;
}
/// <summary>
/// Lots allocated per 10,000 units of portfolio value.
/// </summary>
public decimal LotPer10kFreeMargin
{
get => _lotPer10k.Value;
set => _lotPer10k.Value = value;
}
/// <summary>
/// Placeholder for the original slippage tolerance.
/// </summary>
public int MaxSlippage
{
get => _maxSlippage.Value;
set => _maxSlippage.Value = value;
}
/// <summary>
/// Comment attached to log messages when orders are placed.
/// </summary>
public string TradeComment
{
get => _tradeComment.Value;
set => _tradeComment.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null;
_slowMa = null;
_pointValue = 0m;
_minGapThreshold = 0m;
_previousSignal = 0;
_hasInitialSignal = false;
_longTrailingStop = null;
_shortTrailingStop = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastLength = Math.Min(FastPeriod, SlowPeriod);
var slowLength = Math.Max(FastPeriod, SlowPeriod);
if (fastLength == slowLength)
{
LogWarning("Fast and slow periods must differ.");
Stop();
return;
}
_fastMa = CreateMovingAverage(MaMethod, fastLength);
_slowMa = CreateMovingAverage(MaMethod, slowLength);
_pointValue = CalculatePointValue();
_minGapThreshold = MinGapPoints * _pointValue;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastMa, _slowMa, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
var inSession = InSession(candle.OpenTime);
var allowTrading = inSession && true;
UpdateTrailing(candle, inSession || TrailOutsideSession);
HandleProtectiveExits(candle);
if (_fastMa == null || _slowMa == null)
return;
if (!_fastMa.IsFormed || !_slowMa.IsFormed)
return;
var gapUp = fast - slow;
var gapDown = slow - fast;
if (!_hasInitialSignal)
{
if (gapUp >= _minGapThreshold)
{
_previousSignal = 1;
_hasInitialSignal = true;
}
else if (gapDown >= _minGapThreshold)
{
_previousSignal = -1;
_hasInitialSignal = true;
}
return;
}
if (_previousSignal > 0)
{
if (gapDown >= _minGapThreshold)
{
if (CloseOutsideSession || inSession)
CloseCurrentPosition();
if (allowTrading && Mode != EntryModes.BuyOnly)
{
OpenShort(candle.ClosePrice);
}
_previousSignal = -1;
ResetTrailing();
}
}
else if (_previousSignal < 0)
{
if (gapUp >= _minGapThreshold)
{
if (CloseOutsideSession || inSession)
CloseCurrentPosition();
if (allowTrading && Mode != EntryModes.SellOnly)
{
OpenLong(candle.ClosePrice);
}
_previousSignal = 1;
ResetTrailing();
}
}
}
private void OpenLong(decimal price)
{
var volume = CalculateOrderVolume(price);
if (volume <= 0m)
return;
BuyMarket(volume);
LogInfo($"{TradeComment}: opened long, volume={volume:0.###}");
}
private void OpenShort(decimal price)
{
var volume = CalculateOrderVolume(price);
if (volume <= 0m)
return;
SellMarket(volume);
LogInfo($"{TradeComment}: opened short, volume={volume:0.###}");
}
private void CloseCurrentPosition()
{
if (Position > 0m)
{
SellMarket(Position);
LogInfo($"{TradeComment}: closed existing long");
}
else if (Position < 0m)
{
BuyMarket(-Position);
LogInfo($"{TradeComment}: closed existing short");
}
}
private void UpdateTrailing(ICandleMessage candle, bool allowUpdate)
{
if (TrailStopPoints <= 0m || _pointValue <= 0m)
return;
var distance = TrailStopPoints * _pointValue;
if (Position > 0m)
{
if (allowUpdate)
{
var move = candle.ClosePrice - 0m;
if (move >= distance)
{
var newStop = candle.ClosePrice - distance;
if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
_longTrailingStop = newStop;
}
}
if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
{
SellMarket(Position);
LogInfo($"{TradeComment}: trailing stop hit (long)");
ResetTrailing();
}
}
else if (Position < 0m)
{
var absPosition = -Position;
if (allowUpdate)
{
var move = 0m - candle.ClosePrice;
if (move >= distance)
{
var newStop = candle.ClosePrice + distance;
if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
_shortTrailingStop = newStop;
}
}
if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
{
BuyMarket(absPosition);
LogInfo($"{TradeComment}: trailing stop hit (short)");
ResetTrailing();
}
}
else
{
ResetTrailing();
}
}
private void HandleProtectiveExits(ICandleMessage candle)
{
if (_pointValue <= 0m)
return;
if (Position > 0m)
{
var stop = StopLossPoints > 0m ? 0m - StopLossPoints * _pointValue : (decimal?)null;
var target = TakeProfitPoints > 0m ? 0m + TakeProfitPoints * _pointValue : (decimal?)null;
if (stop.HasValue && candle.LowPrice <= stop.Value)
{
SellMarket(Position);
LogInfo($"{TradeComment}: stop-loss hit (long)");
ResetTrailing();
return;
}
if (target.HasValue && candle.HighPrice >= target.Value)
{
SellMarket(Position);
LogInfo($"{TradeComment}: take-profit hit (long)");
ResetTrailing();
}
}
else if (Position < 0m)
{
var absPosition = -Position;
var stop = StopLossPoints > 0m ? 0m + StopLossPoints * _pointValue : (decimal?)null;
var target = TakeProfitPoints > 0m ? 0m - TakeProfitPoints * _pointValue : (decimal?)null;
if (stop.HasValue && candle.HighPrice >= stop.Value)
{
BuyMarket(absPosition);
LogInfo($"{TradeComment}: stop-loss hit (short)");
ResetTrailing();
return;
}
if (target.HasValue && candle.LowPrice <= target.Value)
{
BuyMarket(absPosition);
LogInfo($"{TradeComment}: take-profit hit (short)");
ResetTrailing();
}
}
else
{
ResetTrailing();
}
}
private decimal CalculateOrderVolume(decimal price)
{
var desired = FixedLot;
if (EnableAutoLot)
{
var equity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
if (equity is decimal value && value > 0m && price > 0m)
{
var lots = Math.Round((value / 10000m) * LotPer10kFreeMargin, 1, MidpointRounding.AwayFromZero);
if (lots > 0m)
desired = lots;
}
}
var adjusted = AdjustVolume(desired);
return adjusted > 0m ? adjusted : 0m;
}
private decimal AdjustVolume(decimal volume)
{
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 1m;
if (step > 0m)
{
var steps = Math.Max(1m, Math.Round(volume / step, 0, MidpointRounding.AwayFromZero));
volume = steps * step;
}
var minVolume = security.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = security.MaxVolume ?? decimal.MaxValue;
if (volume > maxVolume)
volume = maxVolume;
return volume;
}
private bool InSession(DateTimeOffset time)
{
var start = SessionStart;
var end = SessionEnd;
var current = time.TimeOfDay;
if (end < start)
{
return current >= start || current <= end;
}
return current >= start && current <= end;
}
private decimal CalculatePointValue()
{
var step = Security?.PriceStep ?? Security?.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var point = step;
if (point == 0.00001m || point == 0.001m)
point *= 10m;
return point;
}
private DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
DecimalLengthIndicator indicator = method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = length },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = length },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = length },
_ => new ExponentialMovingAverage { Length = length }
};
return indicator;
}
private void ResetTrailing()
{
_longTrailingStop = null;
_shortTrailingStop = null;
}
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed moving average.
/// </summary>
Smoothed,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Weighted
}
/// <summary>
/// Directional filter for new positions.
/// </summary>
public enum EntryModes
{
/// <summary>
/// Allow both long and short entries.
/// </summary>
Both,
/// <summary>
/// Allow only long entries.
/// </summary>
BuyOnly,
/// <summary>
/// Allow only short entries.
/// </summary>
SellOnly
}
}
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
from StockSharp.Algo.Indicators import SimpleMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class adjustable_moving_average_strategy(Strategy):
"""Moving average crossover with adjustable gap filter."""
def __init__(self):
super(adjustable_moving_average_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle timeframe", "Timeframe used to build moving averages", "General")
self._fast_period = self.Param("FastPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Fast period", "Short moving average length", "Moving averages")
self._slow_period = self.Param("SlowPeriod", 30) \
.SetGreaterThanZero() \
.SetDisplay("Slow period", "Long moving average length", "Moving averages")
self._min_gap_points = self.Param("MinGapPoints", 3.0) \
.SetNotNegative() \
.SetDisplay("Minimum gap (points)", "Required distance between fast and slow MAs", "Trading")
self._fixed_lot = self.Param("FixedLot", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Fixed lot", "Volume used for orders", "Money management")
self._point_value = 0.0
self._min_gap_threshold = 0.0
self._previous_signal = 0
self._has_initial_signal = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
@property
def MinGapPoints(self):
return self._min_gap_points.Value
@property
def FixedLot(self):
return self._fixed_lot.Value
def OnReseted(self):
super(adjustable_moving_average_strategy, self).OnReseted()
self._point_value = 0.0
self._min_gap_threshold = 0.0
self._previous_signal = 0
self._has_initial_signal = False
def OnStarted2(self, time):
super(adjustable_moving_average_strategy, self).OnStarted2(time)
fast_len = min(self.FastPeriod, self.SlowPeriod)
slow_len = max(self.FastPeriod, self.SlowPeriod)
fast_ma = ExponentialMovingAverage()
fast_ma.Length = fast_len
slow_ma = ExponentialMovingAverage()
slow_ma.Length = slow_len
self._point_value = self._calc_point_value()
self._min_gap_threshold = float(self.MinGapPoints) * self._point_value
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ma, slow_ma, self._process_candle).Start()
def _calc_point_value(self):
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step > 0:
if step == 0.00001 or step == 0.001:
return step * 10.0
return step
return 0.0
def _process_candle(self, candle, fast, slow):
if candle.State != CandleStates.Finished:
return
fv = float(fast)
sv = float(slow)
gap_up = fv - sv
gap_down = sv - fv
if not self._has_initial_signal:
if gap_up >= self._min_gap_threshold:
self._previous_signal = 1
self._has_initial_signal = True
elif gap_down >= self._min_gap_threshold:
self._previous_signal = -1
self._has_initial_signal = True
return
if self._previous_signal > 0:
if gap_down >= self._min_gap_threshold:
self._close_position()
self.SellMarket(self.FixedLot)
self._previous_signal = -1
elif self._previous_signal < 0:
if gap_up >= self._min_gap_threshold:
self._close_position()
self.BuyMarket(self.FixedLot)
self._previous_signal = 1
def _close_position(self):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
def CreateClone(self):
return adjustable_moving_average_strategy()