Estrategia de Cruce de Dos iMA
Esta estrategia porta el clásico asesor experto de MetaTrader 5 "Crossing of two iMA" a la API de alto nivel de StockSharp. Opera cuando dos medias móviles configurables se cruzan y puede requerir opcionalmente confirmación de una tercera media móvil que actúa como filtro direccional. La implementación mantiene la flexibilidad original al soportar dimensionamiento de posiciones manual o basado en riesgo, offsets estilo entrada pendiente y un trailing stop con paso definido por el usuario.
La conversión procesa señales en el cierre de cada candle finalizado, replicando cómo el experto MQL5 espera una nueva barra. El comportamiento de órdenes pendientes (PriceLevelPips) se simula internamente monitoreando máximos y mínimos de candles, por lo que no se envían órdenes stop/límite reales. Un trigger pendiente largo se activa cuando la barra alcanza el precio elegido para entradas buy stop o baja al precio para entradas buy limit, y la misma lógica simétrica se aplica para configuraciones cortas.
Reglas de trading
- Indicadores
- Media móvil
First(periodo, desplazamiento y método son configurables). - Media móvil
Second(también completamente configurable). - Media móvil
Thirdopcional usada como filtro (UseThirdMovingAverage = true).
- Media móvil
- Criterios de entrada
- Cruce primario (barras 0 y 1)
- Largo: la primera MA cruza por encima de la segunda MA en la barra actual mientras estaba por debajo en la barra anterior. Si el filtro está activo, la tercera MA debe mantenerse por debajo de la primera MA para validar la ruptura larga.
- Corto: la primera MA cruza por debajo de la segunda MA y, si el filtro está habilitado, la tercera MA debe mantenerse por encima de la primera MA.
- Cruce de respaldo (barras 0 y 2)
- Realiza una búsqueda adicional hacia atrás para capturar cruces rápidos ocurridos entre las dos barras anteriores. La estrategia ignora esta señal si ya se abrió otra operación dentro de las últimas tres barras (igual que la búsqueda de historial de MQL5).
- Cruce primario (barras 0 y 1)
- Dirección: tanto largo como corto.
- Stops y objetivos
- El stop loss y take profit se expresan en pips. Se convierten a offsets de precio basados en el tamaño del tick del instrumento y se ajustan para precios de 3/5 dígitos igual que el EA original.
- El trailing stop se activa solo cuando
TrailingStopPips > 0. Mueve el stop por la distancia de trailing una vez que el precio avanza al menosTrailingStepPipsmás allá del nivel de stop anterior.
- Modo de orden pendiente (
PriceLevelPips)0: entrar inmediatamente a mercado.< 0: simular órdenes stop (buy stop por encima del precio, sell stop por debajo). El stop loss y take profit se desplazan en el mismo offset.> 0: simular órdenes límite (buy limit por debajo del precio, sell limit por encima). Los niveles de protección se desplazan en consecuencia.
Gestión de capital
UseFixedVolume = truereplica el modo de lote manual del EA. La estrategia simplemente usaVolume(y cierra posiciones opuestas antes de abrir una nueva).- Cuando
UseFixedVolume = false, la estrategia asigna riesgo comoPortfolio.CurrentValue * RiskPercent / 100. El tamaño de la orden se convierte enriskAmount / stopDistance. Si no se proporciona stop loss (StopLossPips = 0), la distancia de riesgo calculada es cero, por lo que la estrategia se niega a abrir una posición — idéntico al comportamiento original deMoneyFixedRiskque devuelve cero lotes.
Lógica de trailing
- Las posiciones largas tracen el stop a
Close - TrailingStopPips * pipValueuna vez que el precio se ha movido al menosTrailingStepPipsmás allá del stop anterior. El valor de trailing siempre se mueve hacia arriba y nunca afloja el stop. - Las posiciones cortas reflejan este comportamiento moviendo el stop a
Close + TrailingStopPips * pipValuecuando el precio avanza suficientemente a su favor. - El take profit y el stop inicial se verifican antes de los ajustes de trailing, asegurando que las salidas coincidan con las prioridades del EA original.
Parámetros predeterminados
- Primera MA: longitud
5, desplazamiento3, métodoSmoothed. - Segunda MA: longitud
8, desplazamiento5, métodoSmoothed. - Filtro de tercera MA: habilitado, longitud
13, desplazamiento8, métodoSmoothed. - Controles de riesgo: stop loss
50pips, take profit50pips, trailing10pips con paso de4pips. - Gestión de capital:
UseFixedVolume = true,RiskPercent = 5para el modo de dimensionamiento alternativo. - Offset pendiente:
0pips (ejecución a mercado). - Tipo de candle: marco temporal de 1 minuto (puede cambiarse para coincidir con el periodo del gráfico original).
Notas de implementación
- Los parámetros
shiftde la media móvil retrasan los valores de señal exactamente por el número configurado de barras, por lo que el trazado en gráficos StockSharp coincide con el desplazamiento visual MT5. - La estrategia almacena solo el estado mínimo requerido (actual, anterior y dos barras atrás) para satisfacer la lógica "barras [0], [1], [2]" de MQL5. No se recrean colecciones históricas más allá de ese buffer.
- Las entradas pendientes se borran cuando aparece una nueva señal, replicando la llamada
DeleteAllOrders()del EA. - Dado que StockSharp ejecuta órdenes de forma asíncrona, el precio de entrada registrado para cálculos de trailing y objetivo usa el precio de trigger previsto. Los backtests por tanto reproducen la lógica del EA original en datos de candles sin depender de fills a nivel de tick.
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()