Стратегия Crossing of Two iMA
Эта стратегия переносит популярного эксперта MetaTrader 5 «Crossing of two iMA» в высокоуровневый API StockSharp. Сделки открываются при пересечении двух настраиваемых скользящих средних, а дополнительная третья средняя может выступать фильтром направления. Реализация сохраняет гибкость оригинала: поддерживаются ручной объём или риск-менеджмент по проценту капитала, отложенный стиль входа с параметром PriceLevelPips и трейлинг-стоп с регулируемым шагом.
Сигналы рассчитываются только на закрытии завершённых свечей, что повторяет логику эксперта MQL5 «ждать новый бар». Поведение отложенных ордеров (PriceLevelPips) эмулируется внутри стратегии — фактические стоп/лимит заявки не отправляются. Для покупок стоп-вход срабатывает, когда максимум свечи достигает целевого уровня, а лимит-вход — когда минимум опускается до заданной цены. Для коротких сделок применяется зеркальная схема.
Торговые правила
- Индикаторы
- Первая скользящая средняя (период, сдвиг и метод сглаживания задаются параметрами).
- Вторая скользящая средняя (аналогично настраивается).
- Опциональная третья скользящая средняя-фильтр (
UseThirdMovingAverage = true).
- Условия входа
- Основное пересечение (бары 0 и 1)
- Лонг: первая средняя пересекает вторую снизу вверх на текущем баре, а на предыдущем баре находилась ниже. Если фильтр активен, третья средняя должна находиться ниже первой.
- Шорт: первая средняя пересекает вторую сверху вниз; при включённом фильтре третья средняя должна быть выше первой.
- Дополнительное пересечение (бары 0 и 2)
- Позволяет отловить резкие переходы, произошедшие между двумя предыдущими барами. Сигнал игнорируется, если в пределах последних трёх баров уже открывалась сделка (аналог
SearchPositionsв оригинале).
- Позволяет отловить резкие переходы, произошедшие между двумя предыдущими барами. Сигнал игнорируется, если в пределах последних трёх баров уже открывалась сделка (аналог
- Основное пересечение (бары 0 и 1)
- Направление: торгуются лонги и шорты.
- Стопы и цели
- Стоп-лосс и тейк-профит задаются в пунктах и преобразуются в ценовые оффсеты с учётом шага цены и 3/5-значного котирования, как в исходном советнике.
- Трейлинг-стоп активируется, когда
TrailingStopPips > 0. Стоп переносится на расстояние трейлинга после того, как цена продвинулась минимум наTrailingStepPipsпунктов от предыдущего уровня.
- Режим
PriceLevelPips0: вход по рынку.< 0: имитация стоп-ордеров (buy stop выше цены, sell stop ниже). Стоп и тейк смещаются на тот же оффсет.> 0: имитация лимит-ордеров (buy limit ниже цены, sell limit выше). Защитные уровни смещаются симметрично.
Управление капиталом
UseFixedVolume = trueповторяет ручной режим лота: стратегия использует параметрVolumeи перед открытием новой сделки закрывает противоположную позицию.- При
UseFixedVolume = falseриск рассчитывается какPortfolio.CurrentValue * RiskPercent / 100. Объём позиции равенriskAmount / stopDistance. Если стоп-лосс отключён (StopLossPips = 0), расстояние до стопа равно нулю, поэтому стратегия отказывается открывать позицию — это полностью соответствует поведению классаMoneyFixedRiskв MQL5.
Логика трейлинг-стопа
- Для лонга стоп переносится на уровень
Close - TrailingStopPips * pipValue, когда цена проходит в прибыльном направлении минимумTrailingStepPipsпунктов. Стоп никогда не отступает назад. - Для шорта стоп зеркально переносится на
Close + TrailingStopPips * pipValueпри достаточном движении в сторону прибыли. - Проверка тейк-профита и исходного стопа выполняется до перерасчёта трейлинга, что совпадает с приоритетами оригинала.
Значения по умолчанию
- Первая средняя: период
5, сдвиг3, методSmoothed. - Вторая средняя: период
8, сдвиг5, методSmoothed. - Фильтр: включён, период
13, сдвиг8, методSmoothed. - Риск-параметры: стоп
50пунктов, тейк50пунктов, трейлинг10пунктов и шаг4пункта. - Управление капиталом:
UseFixedVolume = true,RiskPercent = 5для альтернативного режима. - Смещение входа:
0пунктов (рыночный вход). - Тип свечей: таймфрейм 1 минута (можно сменить на нужный таймфрейм графика).
Особенности реализации
- Параметры
shiftзадерживают значения скользящих ровно на заданное число баров, поэтому отображение на графиках StockSharp совпадает с MT5. - Хранится лишь минимальный буфер значений (текущий бар и два предыдущих), достаточный для логики «бары [0], [1], [2]» из MQL5 — дополнительных коллекций не создаётся.
- Любой новый сигнал очищает ожидающий вход, имитируя вызов
DeleteAllOrders(). - Поскольку в StockSharp заявки исполняются асинхронно, для расчёта трейлинга и целей используется целевая цена входа. При тестировании на свечных данных это воспроизводит оригинальную логику без необходимости тиковой точности.
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()