Universelle MA Cross V4-Strategie
Überblick
Die Universal MA Cross V4-Strategie ist eine High-Level-StockSharp-Portierung des MetaTrader 4-Expertenberaters „Universal MACross EA v4“. Der Algorithmus folgt der Interaktion zwischen einem konfigurierbaren schnellen gleitenden Durchschnitt und einem langsam gleitenden Durchschnitt. Es unterstützt mehrere gleitende Durchschnittstypen, auswählbare Preisquellen, ein stündliches Handelsfenster und flexibles Positionsmanagement, einschließlich Stop-and-Reverse-Verhalten, Schutzziele und Trailing Stops. Die Strategie ist für die balkenbasierte Ausführung unter Verwendung des StockSharp-High-Level-API mit Kerzenabonnements konzipiert.
Handelslogik
Indikatorverarbeitung
- Für jede fertige Kerze werden zwei gleitende Durchschnitte ausgewertet. Jeder gleitende Durchschnitt kann seine eigene Länge, Glättungsmethode (einfach, exponentiell, geglättet oder linear gewichtet) und Preisquelle (Schluss, Eröffnung, Hoch, Tief, Median, typisch oder gewichtet) verwenden.
- Der Filter MinCrossDistancePoints erfordert, dass die schnellen und langsamen Durchschnittswerte am Crossover-Balken um mindestens die angegebene Anzahl von Preisschritten voneinander abweichen. Wenn ConfirmedOnEntry aktiviert ist, wird die Divergenz bei der zuvor abgeschlossenen Kerze validiert, wodurch der „bestätigte“ Modus des ursprünglichen EA reproduziert wird.
- Durch die Einstellung ReverseCondition werden bullische und bärische Signale ausgetauscht, ohne die Indikatorkonfiguration zu ändern.
Einreisebestimmungen
- Ein Long-Einstieg liegt vor, wenn der schnelle Durchschnitt den langsamen Durchschnitt um mindestens MinCrossDistancePoints überschreitet. Ein kurzer Einstieg erfordert das entgegengesetzte Kreuz.
- Wenn StopAndReverse wahr ist, schließt ein entgegengesetztes Signal die aktive Position, bevor neue Einträge berücksichtigt werden.
- OneEntryPerBar verhindert mehrere Einträge innerhalb derselben Kerze, indem es den Zeitstempel der letzten Bestellung verfolgt.
- Die Ordergröße wird durch TradeVolume gesteuert. StockSharp wendet dieses Volumen automatisch auf die generierten Marktaufträge an.
Positionsmanagement
- Stop-Loss- und Take-Profit-Abstände werden in Punkten durch StopLossPoints und TakeProfitPoints definiert. Sie werden über die Instrumentenpreisstufe in absolute Preise umgerechnet. Wenn PureSar aktiv ist, ist die gesamte Schutzlogik deaktiviert, genau wie die Option „Pure SAR“ in der Version MQL.
- Die Trailing-Stop-Verwaltung spiegelt die MQL-Implementierung wider: Sobald sich der Preis weiter als TrailingStopPoints vom Einstiegsniveau entfernt, wird der Stop um die gleiche Distanz hinter den Markt gezogen. Trailing Stops werden ignoriert, wenn PureSar aktiviert ist.
- Die Schutzniveaus werden bei jeder geschlossenen Kerze überwacht. Wenn die Kerzenspanne gegen den aktiven Stop oder das aktive Ziel verstößt, schließt die Strategie die Position per Marktauftrag, um ein deterministisches Verhalten anhand historischer Daten aufrechtzuerhalten.
Sitzungsfilter
- Das Flag UseHourTrade beschränkt den Handel auf das inklusive Fenster zwischen StartHour und EndHour (0–23). Die Sitzungsgrenzen beginnen um Mitternacht, wenn die Endstunde kleiner als die Startstunde ist. Die Positionsverwaltung, einschließlich Trailing Stops, bleibt außerhalb der Sitzung aktiv, es sind jedoch keine neuen Eingaben zulässig.
Parameter
| Parameter | Beschreibung |
|---|---|
FastMaPeriod, SlowMaPeriod |
Längen der schnellen und langsamen gleitenden Durchschnitte. |
FastMaType, SlowMaType |
Methoden des gleitenden Durchschnitts: Einfach, exponentiell, geglättet oder linear gewichtet. |
FastPriceType, SlowPriceType |
Preisquellen flossen in jeden gleitenden Durchschnitt ein. |
StopLossPoints, TakeProfitPoints |
Schutzabstände in Preisstufen. Zum Deaktivieren auf 0 setzen. |
TrailingStopPoints |
Trailing-Stop-Distanz in Preisschritten. Auf 0 setzen, um das Nachstellen zu deaktivieren. |
MinCrossDistancePoints |
Mindestabstand zwischen den Durchschnittswerten, der zur Validierung eines Kreuzes erforderlich ist. |
ReverseCondition |
Tauschen Sie bullische und bärische Regeln aus, ohne die Indikatoren zu ändern. |
ConfirmedOnEntry |
Validieren Sie die Signale des zuvor geschlossenen Balkens. Zur sofortigen Bestätigung deaktivieren. |
OneEntryPerBar |
Erlauben Sie höchstens eine neue Position pro Kerze. |
StopAndReverse |
Schließen und kehren Sie die aktuelle Position um, wenn das entgegengesetzte Signal erscheint. |
PureSar |
Deaktivieren Sie die Stop-Loss-, Take-Profit- und Trailing-Stop-Logik. |
UseHourTrade, StartHour, EndHour |
Sitzungsfilter, der Einträge auf einen bestimmten Stundenbereich beschränkt. |
TradeVolume |
Von BuyMarket und SellMarket verwendetes Bestellvolumen. |
CandleType |
Für Indikatorberechnungen abonnierte Kerzenserie. |
Konvertierungshinweise
- Preisbasierte Entfernungen werden in MetaTrader Punkten ausgedrückt. Der Helfer
GetPriceOffsetwandelt diese Werte mithilfe der Sicherheitspreisschrittweite oder der Dezimalgenauigkeit in StockSharp-Preise um. Dadurch bleibt das Strategieverhalten unabhängig vom Instrument am ursprünglichen EA ausgerichtet. - Trailing Stops werden intern verwaltet, da StockSharp High-Level-Strategien auf fertige Kerzen angewendet werden. Dieser deterministische Ansatz stellt sicher, dass Backtests mit Kerzen die beabsichtigte MT4-Trailing-Logik reproduzieren.
- Es ist kein Python-Port enthalten, der der Konvertierungsanforderung entspricht. In diesem Paket werden nur die C#-Implementierung und die mehrsprachige Dokumentation bereitgestellt.
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>
/// Port of the "Universal MACross EA v4" MetaTrader expert advisor.
/// The strategy trades the crossover between configurable fast and slow moving averages
/// with optional session filters, stop-and-reverse behaviour and trailing stop management.
/// </summary>
public class UniversalMaCrossV4Strategy : Strategy
{
public enum MovingAverageMethods
{
Simple,
Exponential,
Smoothed,
LinearWeighted
}
public enum AppliedPrices
{
Close,
Open,
High,
Low,
Median,
Typical,
Weighted
}
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<MovingAverageMethods> _fastMaType;
private readonly StrategyParam<MovingAverageMethods> _slowMaType;
private readonly StrategyParam<AppliedPrices> _fastPriceType;
private readonly StrategyParam<AppliedPrices> _slowPriceType;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _minCrossDistancePoints;
private readonly StrategyParam<bool> _reverseCondition;
private readonly StrategyParam<bool> _confirmedOnEntry;
private readonly StrategyParam<bool> _oneEntryPerBar;
private readonly StrategyParam<bool> _stopAndReverse;
private readonly StrategyParam<bool> _pureSar;
private readonly StrategyParam<bool> _useHourTrade;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _volume;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _fastMa;
private IIndicator _slowMa;
private decimal? _fastPrev;
private decimal? _fastPrevPrev;
private decimal? _slowPrev;
private decimal? _slowPrevPrev;
private DateTimeOffset? _lastEntryBar;
private TradeDirections _lastTrade = TradeDirections.None;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
/// <summary>
/// Fast moving average period.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Slow moving average period.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Method applied to the fast moving average.
/// </summary>
public MovingAverageMethods FastMaType
{
get => _fastMaType.Value;
set => _fastMaType.Value = value;
}
/// <summary>
/// Method applied to the slow moving average.
/// </summary>
public MovingAverageMethods SlowMaType
{
get => _slowMaType.Value;
set => _slowMaType.Value = value;
}
/// <summary>
/// Price source for the fast moving average.
/// </summary>
public AppliedPrices FastPriceType
{
get => _fastPriceType.Value;
set => _fastPriceType.Value = value;
}
/// <summary>
/// Price source for the slow moving average.
/// </summary>
public AppliedPrices SlowPriceType
{
get => _slowPriceType.Value;
set => _slowPriceType.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Minimum distance between moving averages to validate a crossover.
/// </summary>
public decimal MinCrossDistancePoints
{
get => _minCrossDistancePoints.Value;
set => _minCrossDistancePoints.Value = value;
}
/// <summary>
/// Swap bullish and bearish signals when set to <c>true</c>.
/// </summary>
public bool ReverseCondition
{
get => _reverseCondition.Value;
set => _reverseCondition.Value = value;
}
/// <summary>
/// Require the crossover to be confirmed on the previous closed bar.
/// </summary>
public bool ConfirmedOnEntry
{
get => _confirmedOnEntry.Value;
set => _confirmedOnEntry.Value = value;
}
/// <summary>
/// Allow only one new position per candle.
/// </summary>
public bool OneEntryPerBar
{
get => _oneEntryPerBar.Value;
set => _oneEntryPerBar.Value = value;
}
/// <summary>
/// Close and reverse the active position when the opposite signal appears.
/// </summary>
public bool StopAndReverse
{
get => _stopAndReverse.Value;
set => _stopAndReverse.Value = value;
}
/// <summary>
/// Disable stop-loss, take-profit and trailing stop logic.
/// </summary>
public bool PureSar
{
get => _pureSar.Value;
set => _pureSar.Value = value;
}
/// <summary>
/// Enable the hour-based trading session filter.
/// </summary>
public bool UseHourTrade
{
get => _useHourTrade.Value;
set => _useHourTrade.Value = value;
}
/// <summary>
/// Start hour of the trading window (0-23).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// End hour of the trading window (0-23).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Order volume applied to each market order.
/// </summary>
public decimal TradeVolume
{
get => _volume.Value;
set => _volume.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="UniversalMaCrossV4Strategy"/> class.
/// </summary>
public UniversalMaCrossV4Strategy()
{
_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Length of the fast moving average", "Indicators")
.SetOptimize(5, 40, 1);
_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Length of the slow moving average", "Indicators")
.SetOptimize(30, 200, 5);
_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
.SetDisplay("Fast MA Method", "Smoothing method applied to the fast moving average", "Indicators");
_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
.SetDisplay("Slow MA Method", "Smoothing method applied to the slow moving average", "Indicators");
_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
.SetDisplay("Fast MA Price", "Price source injected into the fast moving average", "Indicators");
_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
.SetDisplay("Slow MA Price", "Price source injected into the slow moving average", "Indicators");
_stopLossPoints = Param(nameof(StopLossPoints), 100m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 200m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Take-profit distance in price steps", "Risk");
_trailingStopPoints = Param(nameof(TrailingStopPoints), 40m)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps", "Risk");
_minCrossDistancePoints = Param(nameof(MinCrossDistancePoints), 0m)
.SetNotNegative()
.SetDisplay("Min Cross Distance (points)", "Minimum separation between the moving averages", "Filters");
_reverseCondition = Param(nameof(ReverseCondition), false)
.SetDisplay("Reverse Signals", "Swap bullish and bearish conditions", "General");
_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
.SetDisplay("Confirmed On Entry", "Validate signals on the previous closed bar", "General");
_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
.SetDisplay("One Entry Per Bar", "Allow at most one entry per candle", "General");
_stopAndReverse = Param(nameof(StopAndReverse), true)
.SetDisplay("Stop And Reverse", "Close and reverse when the opposite signal appears", "Risk");
_pureSar = Param(nameof(PureSar), false)
.SetDisplay("Pure SAR", "Disable protective stops and trailing", "Risk");
_useHourTrade = Param(nameof(UseHourTrade), false)
.SetDisplay("Use Hour Filter", "Restrict trading to a specific session", "Session");
_startHour = Param(nameof(StartHour), 10)
.SetDisplay("Start Hour", "Trading window start hour", "Session");
_endHour = Param(nameof(EndHour), 11)
.SetDisplay("End Hour", "Trading window end hour", "Session");
_volume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order volume for each market entry", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary candle subscription used by the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null;
_slowMa = null;
_fastPrev = null;
_fastPrevPrev = null;
_slowPrev = null;
_slowPrevPrev = null;
_lastEntryBar = null;
_lastTrade = TradeDirections.None;
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
Volume = TradeVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageExistingPosition(candle);
if (_fastMa is null || _slowMa is null)
return;
var fastPrice = GetPrice(candle, FastPriceType);
var slowPrice = GetPrice(candle, SlowPriceType);
var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
if (fastResult.IsEmpty) return;
var fastValue = fastResult.GetValue<decimal>();
var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });
if (slowResult.IsEmpty) return;
var slowValue = slowResult.GetValue<decimal>();
var prevFast = _fastPrev;
var prevSlow = _slowPrev;
var prevFastPrev = _fastPrevPrev;
var prevSlowPrev = _slowPrevPrev;
_fastPrevPrev = prevFast;
_slowPrevPrev = prevSlow;
_fastPrev = fastValue;
_slowPrev = slowValue;
var minDistance = GetPriceOffset(MinCrossDistancePoints);
var crossUp = false;
var crossDown = false;
if (ConfirmedOnEntry)
{
// Confirm signals using the previous completed bar (shift 2 -> 1 in MQL terms).
if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
{
var diff = prevFast.Value - prevSlow.Value;
crossUp = prevFastPrev.Value < prevSlowPrev.Value && prevFast.Value > prevSlow.Value && diff >= minDistance;
crossDown = prevFastPrev.Value > prevSlowPrev.Value && prevFast.Value < prevSlow.Value && -diff >= minDistance;
}
}
else
{
// Validate crossovers on the current finished bar.
if (prevFast.HasValue && prevSlow.HasValue)
{
var diff = fastValue - slowValue;
crossUp = prevFast.Value < prevSlow.Value && fastValue > slowValue && diff >= minDistance;
crossDown = prevFast.Value > prevSlow.Value && fastValue < slowValue && -diff >= minDistance;
}
}
bool buySignal;
bool sellSignal;
if (!ReverseCondition)
{
buySignal = crossUp;
sellSignal = crossDown;
}
else
{
buySignal = crossDown;
sellSignal = crossUp;
}
if (!IsWithinTradingHours(candle))
return;
if (StopAndReverse && Position != 0)
{
var reverseToShort = _lastTrade == TradeDirections.Long && sellSignal;
var reverseToLong = _lastTrade == TradeDirections.Short && buySignal;
if (reverseToLong || reverseToShort)
{
ClosePosition();
ResetProtection();
_lastTrade = TradeDirections.None;
}
}
if (Position != 0)
return;
if (OneEntryPerBar && _lastEntryBar == candle.OpenTime)
return;
if (buySignal)
{
BuyMarket(TradeVolume);
SetProtectionLevels(candle.ClosePrice, true);
_lastTrade = TradeDirections.Long;
_lastEntryBar = candle.OpenTime;
}
else if (sellSignal)
{
SellMarket(TradeVolume);
SetProtectionLevels(candle.ClosePrice, false);
_lastTrade = TradeDirections.Short;
_lastEntryBar = candle.OpenTime;
}
}
private void ManageExistingPosition(ICandleMessage candle)
{
if (Position == 0)
{
ResetProtection();
return;
}
UpdateTrailingStop(candle);
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
ClosePosition();
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
ClosePosition();
ResetProtection();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
ClosePosition();
ResetProtection();
return;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
ClosePosition();
ResetProtection();
}
}
}
private void UpdateTrailingStop(ICandleMessage candle)
{
if (PureSar || TrailingStopPoints <= 0m || !_entryPrice.HasValue)
return;
var trailingDistance = GetPriceOffset(TrailingStopPoints);
if (trailingDistance <= 0m)
return;
if (Position > 0)
{
var move = candle.ClosePrice - _entryPrice.Value;
if (move > trailingDistance)
{
var candidate = candle.ClosePrice - trailingDistance;
if (!_stopPrice.HasValue || candidate > _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
else if (Position < 0)
{
var move = _entryPrice.Value - candle.ClosePrice;
if (move > trailingDistance)
{
var candidate = candle.ClosePrice + trailingDistance;
if (!_stopPrice.HasValue || candidate < _stopPrice.Value)
{
_stopPrice = candidate;
}
}
}
}
private bool IsWithinTradingHours(ICandleMessage candle)
{
if (!UseHourTrade)
return true;
var hour = candle.OpenTime.Hour;
var start = StartHour;
var end = EndHour;
if (start <= end)
return hour >= start && hour <= end;
return hour >= start || hour <= end;
}
private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
{
return method switch
{
MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
MovingAverageMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
_ => new SimpleMovingAverage { Length = period }
};
}
private static decimal GetPrice(ICandleMessage candle, AppliedPrices priceType)
{
return priceType switch
{
AppliedPrices.Open => candle.OpenPrice,
AppliedPrices.High => candle.HighPrice,
AppliedPrices.Low => candle.LowPrice,
AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
_ => candle.ClosePrice,
};
}
private void SetProtectionLevels(decimal entryPrice, bool isLong)
{
_entryPrice = entryPrice;
if (PureSar)
{
_stopPrice = null;
_takeProfitPrice = null;
return;
}
var stopDistance = GetPriceOffset(StopLossPoints);
var takeDistance = GetPriceOffset(TakeProfitPoints);
_stopPrice = stopDistance > 0m ? (isLong ? entryPrice - stopDistance : entryPrice + stopDistance) : null;
_takeProfitPrice = takeDistance > 0m ? (isLong ? entryPrice + takeDistance : entryPrice - takeDistance) : null;
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
}
private decimal GetPriceOffset(decimal points)
{
if (points <= 0m)
return 0m;
var step = Security?.PriceStep ?? 0m;
if (step > 0m)
return points * step;
var decimals = Security?.Decimals;
if (decimals.HasValue && decimals.Value > 0)
{
decimal scale = 1m;
for (var i = 0; i < decimals.Value; i++)
scale /= 10m;
return points * scale;
}
return points;
}
private void ClosePosition()
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
}
private enum TradeDirections
{
None,
Long,
Short
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
ExponentialMovingAverage,
SimpleMovingAverage, SmoothedMovingAverage, WeightedMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
# MA method constants
MA_SIMPLE = 0
MA_EXPONENTIAL = 1
MA_SMOOTHED = 2
MA_LINEAR_WEIGHTED = 3
# Applied price constants
PRICE_CLOSE = 0
PRICE_OPEN = 1
PRICE_HIGH = 2
PRICE_LOW = 3
PRICE_MEDIAN = 4
PRICE_TYPICAL = 5
PRICE_WEIGHTED = 6
# Trade direction constants
DIR_NONE = 0
DIR_LONG = 1
DIR_SHORT = 2
class universal_ma_cross_v4_strategy(Strategy):
"""Universal MA Cross EA v4. Trades crossover between configurable fast and slow
moving averages with optional session filters, stop-and-reverse, and trailing stop."""
def __init__(self):
super(universal_ma_cross_v4_strategy, self).__init__()
self._fast_ma_period = self.Param("FastMaPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA Period", "Length of the fast moving average", "Indicators")
self._slow_ma_period = self.Param("SlowMaPeriod", 80) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA Period", "Length of the slow moving average", "Indicators")
self._fast_ma_type = self.Param("FastMaType", MA_EXPONENTIAL) \
.SetDisplay("Fast MA Method", "Smoothing method for the fast MA", "Indicators")
self._slow_ma_type = self.Param("SlowMaType", MA_EXPONENTIAL) \
.SetDisplay("Slow MA Method", "Smoothing method for the slow MA", "Indicators")
self._fast_price_type = self.Param("FastPriceType", PRICE_CLOSE) \
.SetDisplay("Fast MA Price", "Price source for the fast MA", "Indicators")
self._slow_price_type = self.Param("SlowPriceType", PRICE_CLOSE) \
.SetDisplay("Slow MA Price", "Price source for the slow MA", "Indicators")
self._stop_loss_points = self.Param("StopLossPoints", 100.0) \
.SetDisplay("Stop Loss (points)", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 200.0) \
.SetDisplay("Take Profit (points)", "Take-profit distance in price steps", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 40.0) \
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps", "Risk")
self._min_cross_distance_points = self.Param("MinCrossDistancePoints", 0.0) \
.SetDisplay("Min Cross Distance (points)", "Minimum separation between MAs", "Filters")
self._reverse_condition = self.Param("ReverseCondition", False) \
.SetDisplay("Reverse Signals", "Swap bullish and bearish conditions", "General")
self._confirmed_on_entry = self.Param("ConfirmedOnEntry", True) \
.SetDisplay("Confirmed On Entry", "Validate signals on the previous closed bar", "General")
self._one_entry_per_bar = self.Param("OneEntryPerBar", True) \
.SetDisplay("One Entry Per Bar", "Allow at most one entry per candle", "General")
self._stop_and_reverse = self.Param("StopAndReverse", True) \
.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk")
self._pure_sar = self.Param("PureSar", False) \
.SetDisplay("Pure SAR", "Disable protective stops and trailing", "Risk")
self._use_hour_trade = self.Param("UseHourTrade", False) \
.SetDisplay("Use Hour Filter", "Restrict trading to a specific session", "Session")
self._start_hour = self.Param("StartHour", 10) \
.SetDisplay("Start Hour", "Trading window start hour", "Session")
self._end_hour = self.Param("EndHour", 11) \
.SetDisplay("End Hour", "Trading window end hour", "Session")
self._volume_param = self.Param("TradeVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Order volume for each entry", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary candle subscription", "General")
self._fast_ma = None
self._slow_ma = None
self._fast_prev = None
self._fast_prev_prev = None
self._slow_prev = None
self._slow_prev_prev = None
self._last_entry_bar = None
self._last_trade = DIR_NONE
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@property
def FastMaType(self):
return self._fast_ma_type.Value
@property
def SlowMaType(self):
return self._slow_ma_type.Value
@property
def FastPriceType(self):
return self._fast_price_type.Value
@property
def SlowPriceType(self):
return self._slow_price_type.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def MinCrossDistancePoints(self):
return self._min_cross_distance_points.Value
@property
def ReverseCondition(self):
return self._reverse_condition.Value
@property
def ConfirmedOnEntry(self):
return self._confirmed_on_entry.Value
@property
def OneEntryPerBar(self):
return self._one_entry_per_bar.Value
@property
def StopAndReverse(self):
return self._stop_and_reverse.Value
@property
def PureSar(self):
return self._pure_sar.Value
@property
def UseHourTrade(self):
return self._use_hour_trade.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def TradeVolume(self):
return self._volume_param.Value
def OnReseted(self):
super(universal_ma_cross_v4_strategy, self).OnReseted()
self._fast_ma = None
self._slow_ma = None
self._fast_prev = None
self._fast_prev_prev = None
self._slow_prev = None
self._slow_prev_prev = None
self._last_entry_bar = None
self._last_trade = DIR_NONE
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def _create_ma(self, method, period):
if method == MA_SIMPLE:
ma = SimpleMovingAverage()
elif method == MA_SMOOTHED:
ma = SmoothedMovingAverage()
elif method == MA_LINEAR_WEIGHTED:
ma = WeightedMovingAverage()
else:
ma = ExponentialMovingAverage()
ma.Length = period
return ma
def _get_price(self, candle, price_type):
if price_type == PRICE_OPEN:
return float(candle.OpenPrice)
elif price_type == PRICE_HIGH:
return float(candle.HighPrice)
elif price_type == PRICE_LOW:
return float(candle.LowPrice)
elif price_type == PRICE_MEDIAN:
return (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
elif price_type == PRICE_TYPICAL:
return (float(candle.HighPrice) + float(candle.LowPrice) + float(candle.ClosePrice)) / 3.0
elif price_type == PRICE_WEIGHTED:
return (float(candle.HighPrice) + float(candle.LowPrice) + 2.0 * float(candle.ClosePrice)) / 4.0
return float(candle.ClosePrice)
def _get_price_offset(self, points):
pts = float(points)
if pts <= 0:
return 0.0
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is not None and float(step) > 0:
return pts * float(step)
return pts
def OnStarted2(self, time):
super(universal_ma_cross_v4_strategy, self).OnStarted2(time)
self._fast_ma = self._create_ma(self.FastMaType, self.FastMaPeriod)
self._slow_ma = self._create_ma(self.SlowMaType, self.SlowMaPeriod)
self.Volume = float(self.TradeVolume)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _close_position(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
def _set_protection_levels(self, entry_price, is_long):
self._entry_price = entry_price
if self.PureSar:
self._stop_price = None
self._take_profit_price = None
return
stop_dist = self._get_price_offset(self.StopLossPoints)
take_dist = self._get_price_offset(self.TakeProfitPoints)
if stop_dist > 0:
self._stop_price = entry_price - stop_dist if is_long else entry_price + stop_dist
else:
self._stop_price = None
if take_dist > 0:
self._take_profit_price = entry_price + take_dist if is_long else entry_price - take_dist
else:
self._take_profit_price = None
def _update_trailing_stop(self, candle):
if self.PureSar or float(self.TrailingStopPoints) <= 0 or self._entry_price is None:
return
trailing_distance = self._get_price_offset(self.TrailingStopPoints)
if trailing_distance <= 0:
return
close = float(candle.ClosePrice)
if self.Position > 0:
move = close - self._entry_price
if move > trailing_distance:
candidate = close - trailing_distance
if self._stop_price is None or candidate > self._stop_price:
self._stop_price = candidate
elif self.Position < 0:
move = self._entry_price - close
if move > trailing_distance:
candidate = close + trailing_distance
if self._stop_price is None or candidate < self._stop_price:
self._stop_price = candidate
def _manage_existing_position(self, candle):
if self.Position == 0:
self._reset_protection()
return
self._update_trailing_stop(candle)
low = float(candle.LowPrice)
high = float(candle.HighPrice)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self._close_position()
self._reset_protection()
return
if self._take_profit_price is not None and high >= self._take_profit_price:
self._close_position()
self._reset_protection()
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self._close_position()
self._reset_protection()
return
if self._take_profit_price is not None and low <= self._take_profit_price:
self._close_position()
self._reset_protection()
def _is_within_trading_hours(self, candle):
if not self.UseHourTrade:
return True
hour = candle.OpenTime.Hour
start = self.StartHour
end = self.EndHour
if start <= end:
return hour >= start and hour <= end
return hour >= start or hour <= end
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_existing_position(candle)
if self._fast_ma is None or self._slow_ma is None:
return
fast_price = self._get_price(candle, self.FastPriceType)
slow_price = self._get_price(candle, self.SlowPriceType)
time = candle.OpenTime
fast_result = process_float(self._fast_ma, fast_price, time, True)
if fast_result.IsEmpty:
return
fast_value = float(fast_result)
slow_result = process_float(self._slow_ma, slow_price, time, True)
if slow_result.IsEmpty:
return
slow_value = float(slow_result)
prev_fast = self._fast_prev
prev_slow = self._slow_prev
prev_fast_prev = self._fast_prev_prev
prev_slow_prev = self._slow_prev_prev
self._fast_prev_prev = prev_fast
self._slow_prev_prev = prev_slow
self._fast_prev = fast_value
self._slow_prev = slow_value
min_distance = self._get_price_offset(self.MinCrossDistancePoints)
cross_up = False
cross_down = False
if self.ConfirmedOnEntry:
if prev_fast is not None and prev_slow is not None and \
prev_fast_prev is not None and prev_slow_prev is not None:
diff = prev_fast - prev_slow
cross_up = prev_fast_prev < prev_slow_prev and prev_fast > prev_slow and diff >= min_distance
cross_down = prev_fast_prev > prev_slow_prev and prev_fast < prev_slow and -diff >= min_distance
else:
if prev_fast is not None and prev_slow is not None:
diff = fast_value - slow_value
cross_up = prev_fast < prev_slow and fast_value > slow_value and diff >= min_distance
cross_down = prev_fast > prev_slow and fast_value < slow_value and -diff >= min_distance
if not self.ReverseCondition:
buy_signal = cross_up
sell_signal = cross_down
else:
buy_signal = cross_down
sell_signal = cross_up
if not self._is_within_trading_hours(candle):
return
if self.StopAndReverse and self.Position != 0:
reverse_to_short = self._last_trade == DIR_LONG and sell_signal
reverse_to_long = self._last_trade == DIR_SHORT and buy_signal
if reverse_to_long or reverse_to_short:
self._close_position()
self._reset_protection()
self._last_trade = DIR_NONE
if self.Position != 0:
return
if self.OneEntryPerBar and self._last_entry_bar == candle.OpenTime:
return
close = float(candle.ClosePrice)
if buy_signal:
self.BuyMarket()
self._set_protection_levels(close, True)
self._last_trade = DIR_LONG
self._last_entry_bar = candle.OpenTime
elif sell_signal:
self.SellMarket()
self._set_protection_levels(close, False)
self._last_trade = DIR_SHORT
self._last_entry_bar = candle.OpenTime
def CreateClone(self):
return universal_ma_cross_v4_strategy()