Strategie Kreuzung Zweier iMA
Diese Strategie portiert den klassischen MetaTrader 5 Expert Advisor "Crossing of two iMA" in die High-Level-API von StockSharp. Sie handelt, wenn sich zwei konfigurierbare gleitende Durchschnitte kreuzen, und kann optional Bestätigung von einem dritten gleitenden Durchschnitt erfordern, der als Richtungsfilter fungiert. Die Implementierung behält die ursprüngliche Flexibilität, indem sie manuelle oder risikobasierte Positionsgrößenbestimmung, Offsets im Stil ausstehender Einstiege und einen Trailing Stop mit benutzerdefiniertem Schritt unterstützt.
Die Konvertierung verarbeitet Signale beim Schließen jeder abgeschlossenen Kerze und repliziert, wie der MQL5-Experte auf einen neuen Balken wartet. Das Verhalten von ausstehenden Aufträgen (PriceLevelPips) wird intern durch Überwachung von Kerzenhochs und -tiefs simuliert, sodass keine tatsächlichen Stop-/Limit-Aufträge gesendet werden. Ein ausstehender Long-Trigger wird aktiviert, wenn der Balken den gewählten Preis für Buy-Stop-Einstiege erreicht oder auf den Preis für Buy-Limit-Einstiege fällt, und die gleiche symmetrische Logik wird für Short-Setups angewendet.
Handelsregeln
- Indikatoren
- Erster gleitender Durchschnitt
First(Periode, Versatz und Methode sind konfigurierbar). - Zweiter gleitender Durchschnitt
Second(ebenfalls vollständig konfigurierbar). - Optionaler dritter gleitender Durchschnitt
Thirdals Filter (UseThirdMovingAverage = true).
- Erster gleitender Durchschnitt
- Einstiegskriterien
- Primärer Kreuzung (Balken 0 und 1)
- Long: der erste MA kreuzt auf dem aktuellen Balken von unten nach oben über den zweiten MA, während er auf dem vorherigen Balken darunter war. Wenn der Filter aktiv ist, muss der dritte MA unter dem ersten MA bleiben, um den Long-Ausbruch zu validieren.
- Short: der erste MA kreuzt unter den zweiten MA und, wenn der Filter aktiviert ist, muss der dritte MA über dem ersten MA bleiben.
- Zusatzkreuzung (Balken 0 und 2)
- Führt eine zusätzliche Rückwärtssuche durch, um schnelle Kreuzungen zu erfassen, die zwischen den beiden vorherigen Balken aufgetreten sind. Die Strategie ignoriert dieses Signal, wenn innerhalb der letzten drei Balken bereits ein Trade eröffnet wurde (identisch mit der MQL5-Verlaufssuche).
- Primärer Kreuzung (Balken 0 und 1)
- Richtung: sowohl Long als auch Short.
- Stops und Ziele
- Stop-Loss und Take-Profit werden in Pips ausgedrückt. Sie werden basierend auf der Tick-Größe des Instruments in Preisoffsets umgerechnet und für 3/5-stellige Preisangaben wie der ursprüngliche EA angepasst.
- Trailing Stop aktiviert sich nur, wenn
TrailingStopPips > 0. Er bewegt den Stop um die Trailing-Distanz, sobald der Preis sich um mindestensTrailingStepPipsüber das vorherige Stop-Niveau hinaus bewegt.
- Pending-Order-Modus (
PriceLevelPips)0: sofort zum Marktpreis einsteigen.< 0: Stop-Orders simulieren (Buy Stop über Preis, Sell Stop unter Preis). Stop-Loss und Take-Profit werden um denselben Offset verschoben.> 0: Limit-Orders simulieren (Buy Limit unter Preis, Sell Limit über Preis). Schutzniveaus werden entsprechend verschoben.
Geldmanagement
UseFixedVolume = truerepliziert den manuellen Lot-Modus des EA. Die Strategie verwendet einfachVolume(und schließt Gegenpositionen, bevor eine neue geöffnet wird).- Wenn
UseFixedVolume = false, weist die Strategie Risiko alsPortfolio.CurrentValue * RiskPercent / 100zu. Die Auftragsgröße wird zuriskAmount / stopDistance. Wenn kein Stop-Loss angegeben wird (StopLossPips = 0), ist die berechnete Risikoentfernung null, sodass die Strategie die Positionseröffnung verweigert — identisch mit dem ursprünglichenMoneyFixedRisk-Verhalten, das null Lots zurückgibt.
Trailing-Logik
- Long-Positionen verfolgen den Stop auf
Close - TrailingStopPips * pipValue, sobald sich der Preis mindestensTrailingStepPipsüber den vorherigen Stop hinaus bewegt hat. Der Trailing-Wert bewegt sich immer nach oben und lockert den Stop nie. - Short-Positionen spiegeln dieses Verhalten, indem der Stop auf
Close + TrailingStopPips * pipValuebewegt wird, wenn der Preis ausreichend zu seinen Gunsten voranschreitet. - Take-Profit und initialer Stop werden vor Trailing-Anpassungen überprüft, um sicherzustellen, dass Ausstiege mit den ursprünglichen EA-Prioritäten übereinstimmen.
Standardparameter
- Erster MA: Länge
5, Versatz3, MethodeSmoothed. - Zweiter MA: Länge
8, Versatz5, MethodeSmoothed. - Dritter MA-Filter: aktiviert, Länge
13, Versatz8, MethodeSmoothed. - Risikokontrollen: Stop-Loss
50Pips, Take-Profit50Pips, Trailing10Pips mit4Pip-Schritt. - Geldmanagement:
UseFixedVolume = true,RiskPercent = 5für den alternativen Dimensionierungsmodus. - Ausstehender Offset:
0Pips (Marktausführung). - Kerzentyp: 1-Minuten-Zeitrahmen (kann geändert werden, um dem ursprünglichen Chartperiode zu entsprechen).
Implementierungshinweise
- Die
shift-Parameter des gleitenden Durchschnitts verzögern Signalwerte genau um die konfigurierte Anzahl von Balken, sodass das Plotting auf StockSharp-Charts mit dem MT5-visuellen Versatz übereinstimmt. - Die Strategie speichert nur den minimal erforderlichen Zustand (aktuell, vorherig und zwei Balken zurück), um die "Balken [0], [1], [2]"-Logik aus MQL5 zu erfüllen. Keine historischen Sammlungen werden über diesen Puffer hinaus neu erstellt.
- Ausstehende Einstiege werden gelöscht, wenn ein neues Signal erscheint, was den
DeleteAllOrders()-Aufruf des EA repliziert. - Da StockSharp Aufträge asynchron ausführt, verwendet der für Trailing- und Zielberechnungen erfasste Einstandspreis den beabsichtigten Trigger-Preis. Backtests reproduzieren daher die ursprüngliche EA-Logik auf Kerzendaten ohne Abhängigkeit von 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()