Crossing of Two iMA Strategy
This strategy ports the classic “Crossing of two iMA” MetaTrader 5 expert advisor into the StockSharp high-level API. It trades when two configurable moving averages cross and can optionally require confirmation from a third moving average that acts as a directional filter. The implementation keeps the original flexibility by supporting manual or risk-based position sizing, pending-entry style offsets and a trailing stop with user-defined step.
The conversion processes signals on the close of each finished candle, replicating how the MQL5 expert waits for a new bar. Pending order behavior (PriceLevelPips) is simulated internally by monitoring candle highs and lows, so no actual stop/limit orders are submitted. A long pending trigger activates when the bar reaches the chosen price for buy stop entries or dips to the price for buy limit entries, and the same symmetric logic is applied for short setups.
Trading rules
- Indicators
Firstmoving average (period, shift and method are configurable).Secondmoving average (also fully configurable).- Optional
Thirdmoving average used as a filter (UseThirdMovingAverage = true).
- Entry conditions
- Primary cross (bars 0 and 1)
- Long: first MA crosses above the second MA on the current bar while it was below on the previous bar. If the filter is active, the third MA must stay below the first MA to validate the long breakout.
- Short: first MA crosses below the second MA and, if the filter is enabled, the third MA must stay above the first MA.
- Fallback cross (bars 0 and 2)
- Performs an additional lookback to catch quick crosses that occurred between the previous two bars. The strategy ignores this signal if another trade was already opened within the last three bars (same as the MQL5 history lookup).
- Primary cross (bars 0 and 1)
- Direction: both long and short.
- Stops and targets
- Stop loss and take profit are expressed in pips. They are converted to price offsets based on the instrument tick size and adjusted for 3/5 digit pricing just like the original EA.
- Trailing stop activates only when
TrailingStopPips > 0. It moves the stop by the trailing distance once price advances by at leastTrailingStepPipsbeyond the previous stop level.
- Pending order mode (
PriceLevelPips)0: enter immediately at market.< 0: simulate stop orders (buy stop above price, sell stop below price). The stop loss and take profit are shifted by the same offset.> 0: simulate limit orders (buy limit below price, sell limit above price). Protective levels are shifted accordingly.
Money management
UseFixedVolume = truereplicates the EA’s manual lot mode. The strategy simply usesVolume(and closes opposite positions before opening a new one).- When
UseFixedVolume = false, the strategy allocates risk asPortfolio.CurrentValue * RiskPercent / 100. The order size becomesriskAmount / stopDistance. If no stop loss is provided (StopLossPips = 0), the calculated risk distance equals zero, so the strategy refuses to open a position—identical to the originalMoneyFixedRiskbehavior returning zero lots.
Trailing logic
- Long positions trail the stop to
Close - TrailingStopPips * pipValueonce price has moved by at leastTrailingStepPipsbeyond the previous stop. The trailing value always moves upward and never loosens the stop. - Short positions mirror this behavior by moving the stop to
Close + TrailingStopPips * pipValuewhen the price advances enough in favor. - Take profit and initial stop are checked before trailing adjustments, ensuring exits match the original EA priorities.
Default parameters
- First MA: length
5, shift3, methodSmoothed. - Second MA: length
8, shift5, methodSmoothed. - Third MA filter: enabled, length
13, shift8, methodSmoothed. - Risk controls: stop loss
50pips, take profit50pips, trailing10pips with a4pip step. - Money management:
UseFixedVolume = true,RiskPercent = 5for the alternative sizing mode. - Pending offset:
0pips (market execution). - Candle type: 1-minute time frame (can be changed to match the original chart period).
Implementation notes
- The moving average
shiftparameters delay signal values exactly by the configured number of bars, so plotting on StockSharp charts matches the MT5 visual shift. - The strategy stores only the minimal state required (current, previous and two bars back) to satisfy the “bars [0], [1], [2]” logic from MQL5. No historical collections are recreated beyond that buffer.
- Pending entries are cleared whenever a new signal appears, replicating the EA’s
DeleteAllOrders()call. - Because StockSharp executes orders asynchronously, the entry price recorded for trailing and target calculations uses the intended trigger price. Backtests therefore reproduce the original EA logic on candle data without relying on tick-level fills.
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>
/// Strategy that emulates the "Crossing of two iMA" MQL5 expert advisor.
/// It trades crossovers between two configurable moving averages with an optional third filter average.
/// Supports manual volume or percentage risk based sizing, simulated pending orders and trailing stop management.
/// </summary>
public class CrossingOfTwoIMAStrategy : Strategy
{
/// <summary>
/// Moving average calculation methods supported by the strategy.
/// </summary>
public enum MovingAverageMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Smoothed (RMA) moving average.
/// </summary>
Smoothed,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Weighted,
}
private readonly StrategyParam<int> _firstPeriod;
private readonly StrategyParam<int> _firstShift;
private readonly StrategyParam<MovingAverageMethods> _firstMethod;
private readonly StrategyParam<int> _secondPeriod;
private readonly StrategyParam<int> _secondShift;
private readonly StrategyParam<MovingAverageMethods> _secondMethod;
private readonly StrategyParam<bool> _useThirdAverage;
private readonly StrategyParam<int> _thirdPeriod;
private readonly StrategyParam<int> _thirdShift;
private readonly StrategyParam<MovingAverageMethods> _thirdMethod;
private readonly StrategyParam<bool> _useFixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _priceLevelPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private DecimalLengthIndicator _firstMa;
private DecimalLengthIndicator _secondMa;
private DecimalLengthIndicator _thirdMa;
private readonly List<decimal> _firstValues = new();
private readonly List<decimal> _secondValues = new();
private readonly List<decimal> _thirdValues = new();
private readonly List<DateTimeOffset> _openTimes = new();
private decimal _pipSize;
private decimal? _entryPrice;
private decimal? _activeStopLoss;
private decimal? _activeTakeProfit;
private bool _isLongPosition;
private PendingOrder _pendingOrder;
private DateTimeOffset? _lastEntryTime;
private enum PendingOrderTypes
{
None,
BuyStop,
BuyLimit,
SellStop,
SellLimit,
}
private sealed class PendingOrder
{
public PendingOrderTypes Type { get; init; }
public decimal EntryPrice { get; init; }
public decimal? StopLoss { get; init; }
public decimal? TakeProfit { get; init; }
public decimal Volume { get; init; }
}
/// <summary>
/// Initializes a new instance of the <see cref="CrossingOfTwoIMAStrategy"/> class.
/// </summary>
public CrossingOfTwoIMAStrategy()
{
_firstPeriod = Param(nameof(FirstMaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("First MA Period", "Period of the first moving average", "First Moving Average")
.SetOptimize(2, 30, 1);
_firstShift = Param(nameof(FirstMaShift), 3)
.SetNotNegative()
.SetDisplay("First MA Shift", "Shift applied to the first moving average", "First Moving Average");
_firstMethod = Param(nameof(FirstMaMethod), MovingAverageMethods.Simple)
.SetDisplay("First MA Method", "Calculation method of the first moving average", "First Moving Average");
_secondPeriod = Param(nameof(SecondMaPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("Second MA Period", "Period of the second moving average", "Second Moving Average")
.SetOptimize(3, 60, 1);
_secondShift = Param(nameof(SecondMaShift), 5)
.SetNotNegative()
.SetDisplay("Second MA Shift", "Shift applied to the second moving average", "Second Moving Average");
_secondMethod = Param(nameof(SecondMaMethod), MovingAverageMethods.Simple)
.SetDisplay("Second MA Method", "Calculation method of the second moving average", "Second Moving Average");
_useThirdAverage = Param(nameof(UseThirdMovingAverage), true)
.SetDisplay("Use Third MA", "Enable the third moving average as a directional filter", "Third Moving Average");
_thirdPeriod = Param(nameof(ThirdMaPeriod), 13)
.SetGreaterThanZero()
.SetDisplay("Third MA Period", "Period of the third moving average", "Third Moving Average");
_thirdShift = Param(nameof(ThirdMaShift), 8)
.SetNotNegative()
.SetDisplay("Third MA Shift", "Shift applied to the third moving average", "Third Moving Average");
_thirdMethod = Param(nameof(ThirdMaMethod), MovingAverageMethods.Simple)
.SetDisplay("Third MA Method", "Calculation method of the third moving average", "Third Moving Average");
_useFixedVolume = Param(nameof(UseFixedVolume), true)
.SetDisplay("Use Fixed Volume", "Use the strategy volume directly instead of risk based sizing", "Money Management");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetNotNegative()
.SetDisplay("Risk %", "Risk percentage of portfolio value per trade when position sizing is dynamic", "Money Management");
_priceLevelPips = Param(nameof(PriceLevelPips), 0)
.SetDisplay("Price Level (pips)", "Offset in pips for simulated pending orders (negative for stop, positive for limit)", "Orders");
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Initial stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 10)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 4)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional progress in pips required before the trailing stop is advanced", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Primary candle series used for signals", "General");
}
/// <summary>
/// Period of the first moving average.
/// </summary>
public int FirstMaPeriod
{
get => _firstPeriod.Value;
set => _firstPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the first moving average.
/// </summary>
public int FirstMaShift
{
get => _firstShift.Value;
set => _firstShift.Value = value;
}
/// <summary>
/// Method used for the first moving average.
/// </summary>
public MovingAverageMethods FirstMaMethod
{
get => _firstMethod.Value;
set => _firstMethod.Value = value;
}
/// <summary>
/// Period of the second moving average.
/// </summary>
public int SecondMaPeriod
{
get => _secondPeriod.Value;
set => _secondPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the second moving average.
/// </summary>
public int SecondMaShift
{
get => _secondShift.Value;
set => _secondShift.Value = value;
}
/// <summary>
/// Method used for the second moving average.
/// </summary>
public MovingAverageMethods SecondMaMethod
{
get => _secondMethod.Value;
set => _secondMethod.Value = value;
}
/// <summary>
/// Enables the third moving average filter.
/// </summary>
public bool UseThirdMovingAverage
{
get => _useThirdAverage.Value;
set => _useThirdAverage.Value = value;
}
/// <summary>
/// Period of the third moving average.
/// </summary>
public int ThirdMaPeriod
{
get => _thirdPeriod.Value;
set => _thirdPeriod.Value = value;
}
/// <summary>
/// Shift (in bars) of the third moving average.
/// </summary>
public int ThirdMaShift
{
get => _thirdShift.Value;
set => _thirdShift.Value = value;
}
/// <summary>
/// Method used for the third moving average.
/// </summary>
public MovingAverageMethods ThirdMaMethod
{
get => _thirdMethod.Value;
set => _thirdMethod.Value = value;
}
/// <summary>
/// Use fixed volume or percentage based sizing.
/// </summary>
public bool UseFixedVolume
{
get => _useFixedVolume.Value;
set => _useFixedVolume.Value = value;
}
/// <summary>
/// Risk percentage per trade when dynamic sizing is active.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Offset in pips that defines simulated pending order behavior.
/// </summary>
public int PriceLevelPips
{
get => _priceLevelPips.Value;
set => _priceLevelPips.Value = value;
}
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Required additional progress (in pips) before advancing the trailing stop.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Primary candle type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_firstValues.Clear();
_secondValues.Clear();
_thirdValues.Clear();
_openTimes.Clear();
_entryPrice = null;
_activeStopLoss = null;
_activeTakeProfit = null;
_isLongPosition = false;
_pendingOrder = null;
_lastEntryTime = null;
_pipSize = 0m;
_firstMa = null;
_secondMa = null;
_thirdMa = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_firstMa = CreateMovingAverage(FirstMaMethod, FirstMaPeriod);
_secondMa = CreateMovingAverage(SecondMaMethod, SecondMaPeriod);
_thirdMa = UseThirdMovingAverage ? CreateMovingAverage(ThirdMaMethod, ThirdMaPeriod) : null;
_firstValues.Clear();
_secondValues.Clear();
_thirdValues.Clear();
_openTimes.Clear();
_pipSize = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals;
if (decimals == 3 || decimals == 5)
_pipSize *= 10m;
var subscription = SubscribeCandles(CandleType);
if (UseThirdMovingAverage && _thirdMa != null)
{
subscription
.Bind(_firstMa, _secondMa, _thirdMa, ProcessCandle)
.Start();
}
else
{
subscription
.Bind(_firstMa, _secondMa, ProcessCandle)
.Start();
}
}
private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue)
{
ProcessCandleInternal(candle, firstValue, secondValue, null);
}
private void ProcessCandle(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal thirdValue)
{
ProcessCandleInternal(candle, firstValue, secondValue, thirdValue);
}
private void ProcessCandleInternal(ICandleMessage candle, decimal firstValue, decimal secondValue, decimal? thirdValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateOpenTimes(candle.OpenTime);
HandlePendingOrders(candle);
var positionChanged = false;
ManageActivePosition(candle, ref positionChanged);
UpdateSeries(_firstValues, FirstMaShift, firstValue);
UpdateSeries(_secondValues, SecondMaShift, secondValue);
if (UseThirdMovingAverage && thirdValue.HasValue)
UpdateSeries(_thirdValues, ThirdMaShift, thirdValue.Value);
if (!_firstMa.IsFormed || !_secondMa.IsFormed)
return;
// already checked above
decimal? thirdCurrent = null;
if (UseThirdMovingAverage)
{
if (_thirdMa?.IsFormed != true)
return;
thirdCurrent = GetSeriesValue(_thirdValues, ThirdMaShift, 0);
}
var first0 = GetSeriesValue(_firstValues, FirstMaShift, 0);
var first1 = GetSeriesValue(_firstValues, FirstMaShift, 1);
var first2 = GetSeriesValue(_firstValues, FirstMaShift, 2);
var second0 = GetSeriesValue(_secondValues, SecondMaShift, 0);
var second1 = GetSeriesValue(_secondValues, SecondMaShift, 1);
var second2 = GetSeriesValue(_secondValues, SecondMaShift, 2);
if (first0 is null || first1 is null || second0 is null || second1 is null)
return;
var priceLevelOffset = Math.Abs(PriceLevelPips) * _pipSize;
var stopLoss = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeProfit = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
var currentOpenTime = candle.OpenTime;
var startTime = GetOpenTime(3) ?? DateTimeOffset.MinValue;
var recentEntry = _lastEntryTime.HasValue && _lastEntryTime.Value >= startTime && _lastEntryTime.Value < currentOpenTime;
if (first0 > second0 && first1 < second1)
{
if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0)
{
EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 < second0 && first1 > second1)
{
if (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0)
{
EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 > second0 && first2 is not null && second2 is not null && first2 < second2)
{
if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent < first0))
{
EnterLong(candle, stopLoss, takeProfit, priceLevelOffset);
return;
}
}
else if (first0 < second2 && first1 > second2 && second2 is not null)
{
if (!recentEntry && (!UseThirdMovingAverage || thirdCurrent is null || thirdCurrent > first0))
{
EnterShort(candle, stopLoss, takeProfit, priceLevelOffset);
}
}
}
private void EnterLong(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
{
if (Position > 0)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = stopLossOffset > 0m ? entryPrice - stopLossOffset : (decimal?)null;
var takePrice = takeProfitOffset > 0m ? entryPrice + takeProfitOffset : (decimal?)null;
var volume = CalculateOrderVolume(entryPrice, stopPrice);
if (volume <= 0m)
return;
CancelPendingOrders();
if (PriceLevelPips == 0)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0m)
return;
BuyMarket();
_entryPrice = entryPrice;
_activeStopLoss = stopPrice;
_activeTakeProfit = takePrice;
_isLongPosition = true;
_lastEntryTime = candle.OpenTime;
}
else if (PriceLevelPips < 0)
{
var targetPrice = entryPrice + priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.BuyStop,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
else
{
var targetPrice = entryPrice - priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.BuyLimit,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
}
private void EnterShort(ICandleMessage candle, decimal stopLossOffset, decimal takeProfitOffset, decimal priceLevelOffset)
{
if (Position < 0)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = stopLossOffset > 0m ? entryPrice + stopLossOffset : (decimal?)null;
var takePrice = takeProfitOffset > 0m ? entryPrice - takeProfitOffset : (decimal?)null;
var volume = CalculateOrderVolume(entryPrice, stopPrice);
if (volume <= 0m)
return;
CancelPendingOrders();
if (PriceLevelPips == 0)
{
var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (totalVolume <= 0m)
return;
SellMarket();
_entryPrice = entryPrice;
_activeStopLoss = stopPrice;
_activeTakeProfit = takePrice;
_isLongPosition = false;
_lastEntryTime = candle.OpenTime;
}
else if (PriceLevelPips < 0)
{
var targetPrice = entryPrice - priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value - priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value - priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.SellStop,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
else
{
var targetPrice = entryPrice + priceLevelOffset;
var stop = stopPrice.HasValue ? stopPrice.Value + priceLevelOffset : (decimal?)null;
var take = takePrice.HasValue ? takePrice.Value + priceLevelOffset : (decimal?)null;
_pendingOrder = new PendingOrder
{
Type = PendingOrderTypes.SellLimit,
EntryPrice = targetPrice,
StopLoss = stop,
TakeProfit = take,
Volume = volume,
};
}
}
private void HandlePendingOrders(ICandleMessage candle)
{
if (_pendingOrder is null)
return;
var triggered = _pendingOrder.Type switch
{
PendingOrderTypes.BuyStop => candle.HighPrice >= _pendingOrder.EntryPrice,
PendingOrderTypes.BuyLimit => candle.LowPrice <= _pendingOrder.EntryPrice,
PendingOrderTypes.SellStop => candle.LowPrice <= _pendingOrder.EntryPrice,
PendingOrderTypes.SellLimit => candle.HighPrice >= _pendingOrder.EntryPrice,
_ => false,
};
if (!triggered)
return;
var volume = _pendingOrder.Volume;
if (volume <= 0m)
{
_pendingOrder = null;
return;
}
if (_pendingOrder.Type == PendingOrderTypes.BuyStop || _pendingOrder.Type == PendingOrderTypes.BuyLimit)
{
var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (totalVolume > 0m)
{
BuyMarket();
_entryPrice = _pendingOrder.EntryPrice;
_activeStopLoss = _pendingOrder.StopLoss;
_activeTakeProfit = _pendingOrder.TakeProfit;
_isLongPosition = true;
_lastEntryTime = candle.OpenTime;
}
}
else
{
var totalVolume = volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (totalVolume > 0m)
{
SellMarket();
_entryPrice = _pendingOrder.EntryPrice;
_activeStopLoss = _pendingOrder.StopLoss;
_activeTakeProfit = _pendingOrder.TakeProfit;
_isLongPosition = false;
_lastEntryTime = candle.OpenTime;
}
}
_pendingOrder = null;
}
private void ManageActivePosition(ICandleMessage candle, ref bool positionChanged)
{
if (Position == 0)
return;
var positionVolume = Math.Abs(Position);
if (positionVolume <= 0m)
return;
if (_isLongPosition)
{
if (_activeTakeProfit.HasValue && candle.HighPrice >= _activeTakeProfit.Value)
{
SellMarket();
ResetPositionState();
positionChanged = true;
return;
}
if (_activeStopLoss.HasValue && candle.LowPrice <= _activeStopLoss.Value)
{
SellMarket();
ResetPositionState();
positionChanged = true;
return;
}
UpdateTrailingForLong(candle);
}
else
{
if (_activeTakeProfit.HasValue && candle.LowPrice <= _activeTakeProfit.Value)
{
BuyMarket();
ResetPositionState();
positionChanged = true;
return;
}
if (_activeStopLoss.HasValue && candle.HighPrice >= _activeStopLoss.Value)
{
BuyMarket();
ResetPositionState();
positionChanged = true;
return;
}
UpdateTrailingForShort(candle);
}
}
private void UpdateTrailingForLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var targetStop = candle.ClosePrice - trailingDistance;
if (!_activeStopLoss.HasValue || targetStop <= _activeStopLoss.Value)
return;
if (trailingStep <= 0m || _activeStopLoss.Value < targetStop - trailingStep)
_activeStopLoss = targetStop;
}
private void UpdateTrailingForShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var targetStop = candle.ClosePrice + trailingDistance;
if (!_activeStopLoss.HasValue || targetStop >= _activeStopLoss.Value)
return;
if (trailingStep <= 0m || _activeStopLoss.Value > targetStop + trailingStep)
_activeStopLoss = targetStop;
}
private decimal CalculateOrderVolume(decimal entryPrice, decimal? stopPrice)
{
if (UseFixedVolume || !stopPrice.HasValue)
return Volume;
var riskDistance = Math.Abs(entryPrice - stopPrice.Value);
if (riskDistance <= 0m)
return 0m;
var equity = Portfolio?.CurrentValue ?? 0m;
var riskAmount = equity * RiskPercent / 100m;
return riskAmount > 0m ? riskAmount / riskDistance : 0m;
}
private void CancelPendingOrders()
{
_pendingOrder = null;
}
private void ResetPositionState()
{
_entryPrice = null;
_activeStopLoss = null;
_activeTakeProfit = null;
_isLongPosition = false;
}
private void UpdateSeries(List<decimal> values, int shift, decimal value)
{
values.Add(value);
var maxSize = Math.Max(shift + 3, 3);
while (values.Count > maxSize)
values.RemoveAt(0);
}
private static decimal? GetSeriesValue(List<decimal> values, int shift, int index)
{
var targetIndex = values.Count - 1 - shift - index;
if (targetIndex < 0 || targetIndex >= values.Count)
return null;
return values[targetIndex];
}
private void UpdateOpenTimes(DateTimeOffset openTime)
{
_openTimes.Add(openTime);
while (_openTimes.Count > 4)
_openTimes.RemoveAt(0);
}
private DateTimeOffset? GetOpenTime(int index)
{
var targetIndex = _openTimes.Count - 1 - index;
if (targetIndex < 0 || targetIndex >= _openTimes.Count)
return null;
return _openTimes[targetIndex];
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageMethods method, int length)
{
DecimalLengthIndicator ma = method switch
{
MovingAverageMethods.Simple => new SMA(),
MovingAverageMethods.Exponential => new EMA(),
MovingAverageMethods.Smoothed => new SmoothedMovingAverage(),
MovingAverageMethods.Weighted => new WeightedMovingAverage(),
_ => new SMA(),
};
ma.Length = Math.Max(1, length);
return ma;
}
}
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
from StockSharp.Algo.Indicators import (
SimpleMovingAverage, ExponentialMovingAverage,
SmoothedMovingAverage, WeightedMovingAverage
)
class crossing_of_two_ima_strategy(Strategy):
"""Two MA crossover strategy with optional third MA filter, trailing stop and simulated pending orders."""
def __init__(self):
super(crossing_of_two_ima_strategy, self).__init__()
self._first_period = self.Param("FirstMaPeriod", 5) \
.SetGreaterThanZero() \
.SetDisplay("First MA Period", "Period of the first moving average", "First MA")
self._first_shift = self.Param("FirstMaShift", 3) \
.SetDisplay("First MA Shift", "Shift applied to the first MA", "First MA")
self._second_period = self.Param("SecondMaPeriod", 8) \
.SetGreaterThanZero() \
.SetDisplay("Second MA Period", "Period of the second moving average", "Second MA")
self._second_shift = self.Param("SecondMaShift", 5) \
.SetDisplay("Second MA Shift", "Shift applied to the second MA", "Second MA")
self._use_third = self.Param("UseThirdMA", True) \
.SetDisplay("Use Third MA", "Enable third MA as directional filter", "Third MA")
self._third_period = self.Param("ThirdMaPeriod", 13) \
.SetGreaterThanZero() \
.SetDisplay("Third MA Period", "Period of the third moving average", "Third MA")
self._third_shift = self.Param("ThirdMaShift", 8) \
.SetDisplay("Third MA Shift", "Shift applied to the third MA", "Third MA")
self._stop_loss_pips = self.Param("StopLossPips", 50) \
.SetDisplay("Stop Loss (pips)", "Initial stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 10) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 4) \
.SetDisplay("Trailing Step (pips)", "Progress before advancing trailing", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Primary candle series", "General")
self._first_values = []
self._second_values = []
self._third_values = []
self._pip_size = 1.0
self._entry_price = None
self._active_sl = None
self._active_tp = None
self._is_long = False
self._first_ma = None
self._second_ma = None
self._third_ma = None
@property
def FirstMaPeriod(self):
return self._first_period.Value
@property
def FirstMaShift(self):
return self._first_shift.Value
@property
def SecondMaPeriod(self):
return self._second_period.Value
@property
def SecondMaShift(self):
return self._second_shift.Value
@property
def UseThirdMA(self):
return self._use_third.Value
@property
def ThirdMaPeriod(self):
return self._third_period.Value
@property
def ThirdMaShift(self):
return self._third_shift.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
@property
def CandleType(self):
return self._candle_type.Value
def _create_ma(self, period):
ma = SimpleMovingAverage()
ma.Length = max(1, period)
return ma
def OnStarted2(self, time):
super(crossing_of_two_ima_strategy, self).OnStarted2(time)
self._first_ma = self._create_ma(self.FirstMaPeriod)
self._second_ma = self._create_ma(self.SecondMaPeriod)
self._third_ma = self._create_ma(self.ThirdMaPeriod) if self.UseThirdMA else None
sec = self.Security
self._pip_size = 1.0
if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0:
self._pip_size = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
self._pip_size *= 10.0
subscription = self.SubscribeCandles(self.CandleType)
if self.UseThirdMA and self._third_ma is not None:
subscription.Bind(self._first_ma, self._second_ma, self._third_ma, self._process_3).Start()
else:
subscription.Bind(self._first_ma, self._second_ma, self._process_2).Start()
def _process_2(self, candle, first_val, second_val):
self._process_internal(candle, first_val, second_val, None)
def _process_3(self, candle, first_val, second_val, third_val):
self._process_internal(candle, first_val, second_val, third_val)
def _process_internal(self, candle, first_val, second_val, third_val):
if candle.State != CandleStates.Finished:
return
self._manage_position(candle)
fv = float(first_val)
sv = float(second_val)
self._update_series(self._first_values, self.FirstMaShift, fv)
self._update_series(self._second_values, self.SecondMaShift, sv)
if self.UseThirdMA and third_val is not None:
self._update_series(self._third_values, self.ThirdMaShift, float(third_val))
if not self._first_ma.IsFormed or not self._second_ma.IsFormed:
return
third_current = None
if self.UseThirdMA:
if self._third_ma is None or not self._third_ma.IsFormed:
return
third_current = self._get_series_val(self._third_values, self.ThirdMaShift, 0)
f0 = self._get_series_val(self._first_values, self.FirstMaShift, 0)
f1 = self._get_series_val(self._first_values, self.FirstMaShift, 1)
s0 = self._get_series_val(self._second_values, self.SecondMaShift, 0)
s1 = self._get_series_val(self._second_values, self.SecondMaShift, 1)
if f0 is None or f1 is None or s0 is None or s1 is None:
return
sl = self.StopLossPips * self._pip_size if self.StopLossPips > 0 else 0.0
tp = self.TakeProfitPips * self._pip_size if self.TakeProfitPips > 0 else 0.0
close = float(candle.ClosePrice)
if f0 > s0 and f1 < s1:
if not self.UseThirdMA or third_current is None or third_current < f0:
self._enter_long(close, sl, tp)
return
if f0 < s0 and f1 > s1:
if not self.UseThirdMA or third_current is None or third_current > f0:
self._enter_short(close, sl, tp)
return
def _enter_long(self, close, sl_offset, tp_offset):
if self.Position > 0:
return
self.BuyMarket()
self._entry_price = close
self._active_sl = close - sl_offset if sl_offset > 0 else None
self._active_tp = close + tp_offset if tp_offset > 0 else None
self._is_long = True
def _enter_short(self, close, sl_offset, tp_offset):
if self.Position < 0:
return
self.SellMarket()
self._entry_price = close
self._active_sl = close + sl_offset if sl_offset > 0 else None
self._active_tp = close - tp_offset if tp_offset > 0 else None
self._is_long = False
def _manage_position(self, candle):
if self.Position == 0:
return
if self._is_long and self.Position > 0:
if self._active_tp is not None and float(candle.HighPrice) >= self._active_tp:
self.SellMarket()
self._reset_position()
return
if self._active_sl is not None and float(candle.LowPrice) <= self._active_sl:
self.SellMarket()
self._reset_position()
return
self._update_trailing_long(candle)
elif not self._is_long and self.Position < 0:
if self._active_tp is not None and float(candle.LowPrice) <= self._active_tp:
self.BuyMarket()
self._reset_position()
return
if self._active_sl is not None and float(candle.HighPrice) >= self._active_sl:
self.BuyMarket()
self._reset_position()
return
self._update_trailing_short(candle)
def _update_trailing_long(self, candle):
if self.TrailingStopPips <= 0:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
target = float(candle.ClosePrice) - trail_dist
if self._active_sl is None or target <= self._active_sl:
return
if trail_step <= 0 or self._active_sl < target - trail_step:
self._active_sl = target
def _update_trailing_short(self, candle):
if self.TrailingStopPips <= 0:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
target = float(candle.ClosePrice) + trail_dist
if self._active_sl is None or target >= self._active_sl:
return
if trail_step <= 0 or self._active_sl > target + trail_step:
self._active_sl = target
def _reset_position(self):
self._entry_price = None
self._active_sl = None
self._active_tp = None
self._is_long = False
def _update_series(self, values, shift, value):
values.append(value)
max_size = max(shift + 3, 3)
while len(values) > max_size:
values.pop(0)
def _get_series_val(self, values, shift, index):
target = len(values) - 1 - shift - index
if target < 0 or target >= len(values):
return None
return values[target]
def OnReseted(self):
super(crossing_of_two_ima_strategy, self).OnReseted()
self._first_values = []
self._second_values = []
self._third_values = []
self._reset_position()
self._first_ma = None
self._second_ma = None
self._third_ma = None
def CreateClone(self):
return crossing_of_two_ima_strategy()