English 日本語
preview
Neuinterpretation klassischer Strategien in Python: Das Kreuzen von MAs

Neuinterpretation klassischer Strategien in Python: Das Kreuzen von MAs

MetaTrader 5Handelssysteme | 21 August 2024, 16:11
11 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Einführung

Viele der heutigen Handelsstrategien wurden in einer völlig anderen Marktlandschaft entwickelt. Die Bewertung ihrer Relevanz auf den heutigen, von Algorithmen beherrschten Märkten ist von entscheidender Bedeutung. Dieser Artikel befasst sich mit der Strategie des Kreuzens von gleitenden Durchschnitten, um ihre Wirksamkeit im heutigen Finanzumfeld zu bewerten.

Dieser Artikel befasst sich mit den folgenden Punkten:

  • Gibt es quantitative Belege für die weitere Anwendung der Strategie?
  • Welche Vorteile bietet die Strategie im Vergleich zur direkten Preisanalyse?
  • Funktioniert die Strategie inmitten des modernen algorithmischen Handels noch?
  • Gibt es weitere Indikatoren, die die Genauigkeit der Strategie verbessern können?
  • Kann KI effektiv zur Vorhersage von Überkreuzungen gleitender Durchschnitte genutzt werden, bevor sie eintreten?

Die Technik der gleitenden Durchschnittsübergänge wurde über Jahrzehnte hinweg eingehend untersucht. Das grundlegende Konzept, diese Durchschnittswerte zur Erkennung von Trends und Handelssignalen zu verwenden, ist eine tragende Säule der technischen Analyse, auch wenn ihr genauer Ursprung ungewiss bleibt.

Die Kreuzungsstrategie mit gleitendem Durchschnitt umfasst in der Regel zwei gleitende Durchschnitte mit unterschiedlichen Periodenlängen, wobei die entscheidende Bedingung ist, dass eine Periodenlänge länger ist als die andere. Wenn der gleitende Durchschnitt der kürzeren Periode den gleitenden Durchschnitt der längeren Periode übersteigt, signalisiert dies einen potenziellen Aufwärtstrend, und umgekehrt einen Abwärtstrend.

Technische Analysten nutzen diese Strategie seit Jahrzehnten, um Einstiegs- und Ausstiegspunkte zu bestimmen, die Marktstimmung zu beurteilen und für verschiedene andere Anwendungen. Um festzustellen, wie wirksam sie derzeit ist, werden wir die Strategie einem modernen quantitativen Test unterziehen. Unser Ansatz wird im Folgenden erläutert.


Kreuzungen gleitender Durchschnitte

Abb. 1: Ein Beispiel für das Kreuzen gleitender Durchschnitte, die auf das Paar CADJPY angewendet werden.

Übersicht

Wir sind dabei, eine spannende Reise anzutreten, bei der wir unser MetaTrader5-Terminal mit unserer Python-Umgebung verbinden werden. Zunächst werden wir M15-Daten für das EURUSD-Paar vom 1. Januar 2020 bis zum 25. Juni 2024 anfordern. Dieser umfangreiche Datensatz wird uns einen umfassenden Überblick über das aktuelle Marktverhalten geben.

Unser nächster Schritt besteht darin, zwei Ziele festzulegen. Der erste misst unsere Genauigkeit bei der Vorhersage direkter Preisänderungen und dient als Basis. Anhand dieses Benchmarks können wir vergleichen, wie gut wir bei der Vorhersage von Überkreuzungen gleitender Durchschnitte abschneiden. Auf dem Weg dorthin werden wir nach zusätzlichen technischen Indikatoren suchen, um unsere Genauigkeit zu erhöhen. Schließlich werden wir unsere Computermodelle bitten, die Schlüsselvariablen für die Vorhersage von Überkreuzungen gleitender Durchschnitte zu ermitteln. Wenn das Modell den beiden von uns verwendeten gleitenden Durchschnitten keine Priorität einräumt, könnte dies ein Hinweis darauf sein, dass unsere ursprünglichen Annahmen falsch waren.

Bevor wir uns mit den Zahlen befassen, sollten wir die möglichen Ergebnisse betrachten:

  1. Die Überlegenheit der direkten Preisvorhersage: Wenn die direkte Vorhersage von Preisveränderungen eine höhere oder gleiche Genauigkeit im Vergleich zum Kreuzen gleitender Durchschnitte ergibt, deutet dies darauf hin, dass die Vorhersage von Kreuzungen möglicherweise keinen zusätzlichen Vorteil bietet und die Gültigkeit der Strategie in Frage stellt.

  2. Die Überlegenheit der Vorhersage der Kreuzungen: Wenn wir eine bessere Genauigkeit bei der Vorhersage von Überkreuzungen des gleitenden Durchschnitts erreichen, würde uns das motivieren, mehr Daten zu suchen, um unsere Vorhersagen weiter zu verbessern, was den potenziellen Wert der Strategie unterstreicht.

  3. Die Irrelevanz der gleitenden Durchschnitte: Wenn unsere Modelle keinen der beiden gleitenden Durchschnitte als entscheidend für die Vorhersage von Überkreuzungen identifizieren, deutet dies darauf hin, dass andere Variablen bedeutsamer sein könnten, was wiederum bedeutet, dass die angenommene Beziehung zwischen den beiden gleitenden Durchschnitten nicht zutrifft.

  4. Die Relevanz der gleitenden Durchschnitte: Wenn einer oder beide gleitenden Durchschnitte als wichtig für die Vorhersage von Überkreuzungen eingestuft werden, bestätigt dies eine wesentliche Beziehung zwischen ihnen, die es uns ermöglicht, zuverlässige Modelle für fundierte Vorhersagen zu erstellen.

Diese Analyse wird uns helfen, die Stärken und Schwächen der Verwendung von den Kreuzungen gleitender Durchschnitte in unserer Handelsstrategie zu verstehen und uns zu effektiveren Prognosemethoden zu führen.


Das Experiment: Sind die Kreuzungen gleitender Durchschnitte noch verlässlich?

Beginnen wir damit, dass wir zunächst die benötigten Standard-Python-Bibliotheken importieren.

import pandas as pd
import pandas_ta as ta
import numpy as np
import MetaTrader5 as mt5
from   datetime import datetime
import seaborn as sns
import time

Als Nächstes geben wir unsere Anmeldedaten ein.

account = 123436536
password = "Enter Your Password"
server = "Enter Your Broker"

Wir werden nun versuchen, uns bei unserem Handelskonto anzumelden.

if(mt5.initialize(login=account,password=password,server=server)):
    print("Logged in succesfully")
else:
    print("Failed to login")

Erfolgreich eingeloggt.

Als Nächstes werden wir einige globale Variablen definieren.

timeframe = mt5.TIMEFRAME_M15
deviation = 1000
volume = 0
lot_multiple = 10
symbol = "EURUSD"

Dann holen wir uns Marktdaten zu dem Symbol, das wir handeln wollen.

#Setup trading volume
symbols = mt5.symbols_get()
for index,symbol in enumerate(symbols):
    if symbol.name == "EURUSD":
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        volume = symbol.volume_min * lot_multiple

EURUSD hat ein minimales Volumen: 0.01

Nun werden wir uns daran machen, Trainingsdaten zu holen.

#Specify date range of data to be modelled
date_start = datetime(2020,1,1)
date_end = datetime.now()

Als Nächstes legen wir fest, wie weit in die Zukunft wir prognostizieren wollen.

#Define how far ahead we are looking
look_ahead = 20

Anschließend können wir die Marktdaten von unserem MetaTrader5-Terminal abrufen und die Daten kennzeichnen. Unser Kennzeichnungsschema verwendet eine „1“, um eine Aufwärtsbewegung zu kodieren, und eine „0“ für Abwärtsbewegungen. 

#Fetch market data
market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end))
market_data["time"] = pd.to_datetime(market_data["time"],unit='s')
#Add simple moving average technical indicator
market_data.ta.sma(length=5,append=True)
#Add simple moving average technical indicator
market_data.ta.sma(length=50,append=True)
#Delete missing rows
market_data.dropna(inplace=True)

#Add a column for the target
market_data["target"] = 0
market_data["close_target"] = 0

#Encoding the target
ma_cross_conditions = [
    (market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead)),
    (market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead))
]
#Encoding pattern
ma_cross_choices = [
    #Fast MA above Slow MA
    1,
    #Fast MA below Slow MA
    0
]

price_conditions = [
    (market_data["close"] > market_data["close"].shift(-look_ahead)),
    (market_data["close"] < market_data["close"].shift(-look_ahead))
]

#Encoding pattern
price_choices = [
    #Price fell
    0,
    #Price rose
    1
]

market_data["target"] = np.select(ma_cross_conditions,ma_cross_choices)
market_data["close_target"] = np.select(price_conditions,price_choices)

#The last rows do not have answers
market_data = market_data[:-look_ahead]
market_data


Unser Datenrahmen mit Marktdaten.

Abb. 2: Unser Datenrahmen mit unseren Marktdaten in seiner aktuellen Form.

Wir werden nun die benötigten Bibliotheken für maschinelles Lernen importieren.

#XGBoost
from xgboost import XGBClassifier
#Catboost
from catboost import CatBoostClassifier
#Random forest
from sklearn.ensemble import RandomForestClassifier
#LDA and QDA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis , QuadraticDiscriminantAnalysis
#Logistic regression
from sklearn.linear_model import LogisticRegression
#Neural network
from sklearn.neural_network import MLPClassifier
#Time series split
from sklearn.model_selection import TimeSeriesSplit
#Accuracy metrics
from sklearn.metrics import accuracy_score
#Visualising performance
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve

Vorbereitung auf die Durchführung einer Zeitreihenaufteilung des Datensatzes.

#Time series split
splits = 10
gap = look_ahead
models = ["Logistic Regression","Linear Discriminant Analysis","Quadratic Discriminant Analysis","Random Forest Classifier","XGB Classifier","Cat Boost Classifier","Neural Network Small","Neural Network Large"]

Wir werden die Genauigkeit vieler verschiedener Modelle bewerten und die von jedem Modell erreichte Genauigkeit in einem Datenrahmen speichern. In einem Datenrahmen wird unsere Genauigkeit bei der Vorhersage von gleitenden Durchschnittsübergängen gespeichert, und der zweite Datenrahmen misst unsere Genauigkeit bei der direkten Vorhersage von Preisveränderungen.

error_ma_crossover = pd.DataFrame(index=np.arange(0,splits),columns=models)
error_price = pd.DataFrame(index=np.arange(0,splits),columns=models)

Wir werden nun die Genauigkeit der einzelnen Modelle messen. Zunächst müssen wir jedoch die Eingaben definieren, die unsere Modelle verwenden sollen.

predictors = ["open","high","low","close","tick_volume","spread","SMA_5","SMA_50"]

Um die Genauigkeit eines jeden Modells zu messen, trainieren wir unsere Modelle auf einem Teil des Datensatzes und testen sie dann auf dem Rest des Datensatzes, den sie beim Training nicht gesehen haben. Die Bibliothek TimeSeriesSplit partitioniert unseren Datenrahmen für uns und erleichtert diesen Prozess.

tscv = TimeSeriesSplit(n_splits=splits,gap=gap)
#Training each model to predict changes in the moving average cross over
for i,(train,test) in enumerate(tscv.split(market_data)):
    model = MLPClassifier(solver='lbfgs',alpha=1e-5,hidden_layer_sizes=(20, 10), random_state=1,early_stopping=True)
    model.fit( market_data.loc[train[0]:train[-1],predictors] , market_data.loc[train[0]:train[-1],"target"] ) 
    error_ma_crossover.iloc[i,7] = accuracy_score(market_data.loc[test[0]:test[-1],"target"],model.predict(market_data.loc[test[0]:test[-1],predictors]))

#Training each model to predict changes in the close price
for i,(train,test) in enumerate(tscv.split(market_data)):
    model = MLPClassifier(solver='lbfgs',alpha=1e-5,hidden_layer_sizes=(20, 10), random_state=1,early_stopping=True)
    model.fit( market_data.loc[train[0]:train[-1],predictors] , market_data.loc[train[0]:train[-1],"close_target"] ) 
    error_price.iloc[i,7] = accuracy_score(market_data.loc[test[0]:test[-1],"close_target"],model.predict(market_data.loc[test[0]:test[-1],predictors]))

Sehen wir uns zunächst den Datenrahmen an, der unsere Genauigkeit bei der direkten Vorhersage von Preisveränderungen misst.

error_price

Fehler bei der Vorhersage des Preises

Abb. 3: Unsere Genauigkeit bei der direkten Vorhersage von Preisveränderungen.

Bevor wir fortfahren, wollen wir die Ergebnisse interpretieren. Die erste Bemerkung, die wir machen können, ist, dass keines der Modelle, die wir haben, bei dieser Aufgabe gut abschneidet. Einige Modelle zeigten eine Genauigkeit von weniger als 50 % bei der direkten Vorhersage der Preise. Diese Leistung ist enttäuschend, denn sie zeigt, dass wir mit diesen Modellen mehr oder weniger gleich gut hätten abschneiden können, wenn wir einfach zufällig geraten hätten. Unsere Modelle sind in der Reihenfolge zunehmender Komplexität angeordnet, mit einfacher logistischer Regression auf der linken Seite und tiefen neuronalen Netzen auf der rechten Seite. Wie wir feststellen können, hat die Erhöhung der Komplexität der Modelle unsere Genauigkeit bei der direkten Preisprognose nicht erhöht. Schauen wir uns nun an, ob es eine Verbesserung gibt, wenn wir stattdessen gleitende Durchschnittsübergänge prognostizieren.

error_ma_crossover

Fehler bei der Vorhersage von Kreuzungen der gleitenden Durchschnitte

Abb. 4: Unsere Genauigkeit bei der Vorhersage von gleitenden Durchschnittswerten.

Wie im obigen Datenrahmen zu sehen ist, hat die lineare Diskriminanzanalyse (LDA) bei dieser Aufgabe außerordentlich gut abgeschnitten. Es war mit großem Abstand das leistungsstärkste Modell, das wir untersucht haben. Wenn man außerdem die verbesserte Modellleistung von LDA mit der schlechten Leistung bei der ersten Aufgabe vergleicht, wird deutlich, dass gleitende Durchschnittsübergänge zuverlässiger zu prognostizieren sind als direkte Preisänderungen. Die Vorteile der Prognose von gleitenden Durchschnittsübergängen sind in einem solchen Fall unbestritten.

Visualisierung der Ergebnisse

Veranschaulichen wir uns die oben erzielten Ergebnisse. 

Visualisierung der Ergebnisse

Abb. 5: Visualisierung der erzielten Ergebnisse.

Die Verbesserung des LDA-Algorithmus ist in den Boxplots deutlich sichtbar, was auf ein signifikantes Lernen durch unser Modell hinweist. Darüber hinaus gab es eine leichte, aber spürbare Verbesserung der Leistung der logistischen Regression. Vor allem bei der Vorhersage von gleitenden Durchschnittsübergängen lieferte LDA durchweg eng geclusterte Werte in Boxplots, was die wünschenswerte Genauigkeit und Konsistenz belegt. Diese Häufung deutet darauf hin, dass die Vorhersagen des Modells stabil waren, wobei die wahrscheinlich stationären Residuen auf eine zuverlässige Beziehung hinweisen, die das Modell gelernt hat.

Lassen Sie uns nun analysieren, welche Fehler unser Modell gemacht hat. Wir wollen herausfinden, ob es bei der Erkennung von Aufwärts- oder Abwärtsbewegungen besser abschneidet, oder ob seine Leistung bei beiden Aufgaben gleich ist.

LDA-Verwirrungsmatrix

Abb. 6: Eine Konfusionsmatrix der Leistung unseres LDA-Modells.

Die obige Konfusionsmatrix zeigt auf der linken Seite die wahre Klassifizierung und auf der unteren Seite die Vorhersage unseres Modells. Aus den Daten geht hervor, dass unser Modell mehr Fehler bei der Vorhersage von Aufwärtsbewegungen machte, da es in 47 % der Fälle eine Aufwärtsbewegung als Abwärtsbewegung falsch einordnete. Auf der anderen Seite konnte unser Modell Abwärtsbewegungen sehr gut vorhersagen. Nur in 25 % der Fälle verwechselte unser Modell eine echte Abwärtsbewegung mit einer Aufwärtsbewegung. Wir können also deutlich sehen, dass unser Modell Abwärtsbewegungen besser vorhersagen kann als Aufwärtsbewegungen. 

Wir können den Lernfortschritt unseres Modells visualisieren, wenn es auf immer größere Mengen von Trainingsdaten trifft. Anhand der nachstehenden Grafik lässt sich beurteilen, ob unser Modell die Trainingsdaten zu gut oder zu schlecht abbildet. Eine Überanpassung liegt vor, wenn das Modell Rauschen aus den Daten lernt und sinnvolle Beziehungen nicht erfasst. Eine unzureichende Anpassung wird hingegen durch eine signifikante Lücke zwischen der Trainingsgenauigkeit (dargestellt durch die blaue Linie) und der Validierungsgenauigkeit (die orangefarbene Linie) in der Grafik angezeigt. In unserem aktuellen Diagramm sehen wir eine deutliche, aber nicht allzu große Lücke zwischen den Trainings- und den Validierungsergebnissen, was darauf hindeutet, dass unser LDA-Modell die Trainingsdaten tatsächlich übererfüllt. Die Skala auf der linken Seite zeigt jedoch, dass diese Überanpassung nicht schwerwiegend ist.

Lernkurve für die lineare Diskriminanzanalyse

Abb. 7: Die Lernkurve für unseren LDA-Klassifikator.

Andererseits ist die Unteranpassung durch eine geringe Trainings- und Validierungsgenauigkeit gekennzeichnet. Als Beispiel haben wir die Lernkurve eines unserer leistungsschwachen Modelle, des kleinen neuronalen Netzes, beigefügt. In der nachstehenden Grafik sehen wir eine instabile Beziehung zwischen der Leistung unseres Modells und der Menge der Trainingsdaten, denen es ausgesetzt war. Zunächst verschlechtert sich die Validierungsleistung des Modells mit zunehmender Datenmenge, bis sie einen Wendepunkt erreicht und sich zu verbessern beginnt, wenn sich der Trainingsumfang 10000 Stichproben nähert. Danach stagniert die Verbesserung und es gibt nur noch marginale Verbesserungen, obwohl die Menge der verfügbaren Trainingsdaten weiterhin stark zunimmt.

Lernkurve kleines neuronales Netz

Abb. 8: Die Lernkurve für unser kleines neuronales Netz.

Feature-Eliminierung

Bei den meisten Projekten zum maschinellen Lernen ist es unüblich, dass sich alle Eingaben direkt auf die Zielvariable beziehen. In der Regel ist nur eine Teilmenge der verfügbaren Eingaben für die Vorhersage des Ziels relevant. Die Eliminierung irrelevanter Eingaben bietet mehrere Vorteile, wie zum Beispiel:

  1. Verbesserte Berechnungseffizienz bei der Modellschulung und beim Feature-Engineering.
  2. Verbesserte Modellgenauigkeit, insbesondere wenn die entfernten Merkmale verrauscht waren.

Als Nächstes müssen wir feststellen, ob es eine sinnvolle Beziehung zwischen den gleitenden Durchschnitten gibt. Wir werden Algorithmen zur Eliminierung von Merkmalen einsetzen, um die angenommene Beziehung zu validieren. Wenn es diesen Algorithmen nicht gelingt, die gleitenden Durchschnitte aus der Eingabeliste zu eliminieren, deutet dies darauf hin, dass eine sinnvolle Beziehung besteht. Gelingt es ihnen hingegen, diese Merkmale zu entfernen, deutet dies darauf hin, dass keine signifikante Beziehung zwischen den gleitenden Durchschnitten und dem Kreuzen der gleitenden Durchschnitte besteht.

Wir werden eine Technik zur Auswahl von Merkmalen anwenden, die als Rückwärtsauswahl bekannt ist. Diese Methode beginnt mit der Anpassung eines linearen Modells unter Verwendung aller verfügbaren Eingaben und der anschließenden Messung der Genauigkeit des Modells. Anschließend wird ein Merkmal nach dem anderen entfernt, und die Auswirkungen auf die Modellgenauigkeit werden festgestellt. Das Merkmal, das den geringsten Genauigkeitsverlust verursacht, wird in jedem Schritt eliminiert, bis kein Merkmal mehr übrig ist. In diesem Stadium wählt der Algorithmus automatisch die wichtigsten Merkmale aus, die er ermittelt hat, und empfiehlt sie zur Verwendung.

Ein erheblicher Nachteil der Merkmalseliminierung besteht darin, dass bei verrauschten und unwichtigen Spalten in unserem Datensatz wichtige Spalten uninformativ erscheinen können. Folglich könnte der Algorithmus für die Rückwärtsauswahl versehentlich ein wichtiges Merkmal ausschließen, weil es aufgrund des Rauschens im System uninformativ erscheint.

Schauen wir uns nun an, welche Spalten unser Computer für wichtig hält. Wir beginnen mit dem Import einer Bibliothek namens mlxtend, die Implementierungen des Rückwärtsauswahlalgorithmus enthält.

from mlxtend.feature_selection import SequentialFeatureSelector

Anschließend wenden wir den Algorithmus auf unseren Datensatz an. Achten wir besonders auf 3 der übergebenen Parameter:

  1. „k_features=“ gibt dem Algorithmus vor, wie viele Spalten er auswählen soll. Wir können den Algorithmus anweisen, nur die Spalten auszuwählen, die er für notwendig hält, indem wir ein Intervall von 1 bis zur Gesamtzahl der Spalten im Datensatz angeben.
  2. „forward=“ gibt dem Algorithmus vor, ob er die Vorwärts- oder die Rückwärtsselektion verwenden soll; wir wollen die Rückwärtsselektion verwenden, daher setzen wir diesen Parameter auf „False“.
  3. „n_jobs=“ weist den Algorithmus an, ob er Berechnungen parallel durchführen soll. Wir geben „-1“ an, um dem Algorithmus die Erlaubnis zu erteilen, alle verfügbaren Kerne zu nutzen, was den Zeitaufwand erheblich reduziert. 

backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(),
                                                      k_features=(1,market_data.loc[:,predictors].shape[1]),
                                                      forward=False,
                                                      verbose=2,
                                                      scoring="accuracy",
                                                      cv=5,
						      n_jobs=-1
                                                     ).fit(market_data.loc[:,predictors],market_data.loc[:,"target"])

[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.

[Parallel(n_jobs=-1)]: Done   3 out of   8 | elapsed:    8.0s remaining:   13.3s

[Parallel(n_jobs=-1)]: Done   8 out of   8 | elapsed:    8.0s remaining:    0.0s

[Parallel(n_jobs=-1)]: Done   8 out of   8 | elapsed:    8.0s finished

Sobald der Prozess abgeschlossen ist, können wir mit dem folgenden Befehl eine Liste der Eingaben erhalten, die unser Algorithmus für wichtig hält.

backward_feature_selector.k_feature_names_

('open', 'high', 'close', 'SMA_5', 'SMA_50')

Und wie wir sehen können, hat der Algorithmus für die rückwärts gerichtete Auswahl unsere 2 gleitenden Durchschnitte in seine Liste der wichtigen Merkmale aufgenommen. Das ist eine gute Nachricht für uns, denn es bestätigt, dass unsere Handelsstrategie nicht nur das Ergebnis einer falschen Regression ist.

Technische Merkmale

Nachdem wir nun eine signifikante Beziehung zwischen unseren beiden gleitenden Durchschnitten festgestellt haben, die weitere Verbesserungsbemühungen rechtfertigt, wollen wir untersuchen, ob zusätzliche technische Indikatoren unsere Genauigkeit bei der Vorhersage von Überkreuzungen gleitender Durchschnitte verbessern können. Hier ist das maschinelle Lernen eher eine Kunst als eine Wissenschaft, da es schwierig ist, vorherzusagen, welche Eingaben von Nutzen sein werden. Unser Ansatz wird darin bestehen, mehrere Funktionen hinzuzufügen, die wir für nützlich halten, und ihre tatsächlichen Auswirkungen zu bewerten.

Wir werden Marktdaten aus demselben Markt wie zuvor sammeln, aber dieses Mal werden wir zusätzliche Indikatoren einbeziehen:

  1. Moving Average Convergence Divergence (MACD): Der MACD ist ein sehr aussagekräftiger technischer Indikator zur Trendbestätigung, der uns helfen kann, Veränderungen in den zugrunde liegenden Marktregimen besser zu beobachten.
  2. Der Indikator Awesome Oscillator: Der Awesome-Oszillator ist dafür bekannt, sehr zuverlässige Ausstiegssignale zu liefern, und er kann uns deutlich zeigen, wenn ein Trend seine Dynamik ändert.
  3. Aroon: Der Aroon-Indikator wird verwendet, um den Beginn eines neuen Trends zu erkennen.
  4. Chaikins Commodity Index: Der Chaikins Commodity Index dient als Barometer zur Messung, ob ein Finanztitel überkauft oder überverkauft ist.
  5. Percent Return: Der Indikator für die prozentuale Rendite hilft uns, die Kursentwicklung zu beobachten und festzustellen, ob sie positiv oder negativ ist. 
Fügen wir nun die oben beschriebenen Indikatoren neben unseren ursprünglichen gleitenden Durchschnitten hinzu.

#Fetch market data
market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end))
market_data["time"] = pd.to_datetime(market_data["time"],unit='s')
#Add simple moving average technical indicator
market_data.ta.sma(length=5,append=True)
#Add simple moving average technical indicator
market_data.ta.sma(length=50,append=True)
#Add macd
market_data.ta.macd(append=True)
#Add awesome oscilator
market_data.ta.ao(append=True)
#Add aroon
market_data.ta.aroon(append=True)
#Add chaikins comodity index
market_data.ta.cci(append=True)
#Add percent return
market_data.ta.percent_return(append=True)
#Delete missing rows
market_data.dropna(inplace=True)
#Add the target
market_data["target"] = 0
market_data.loc[market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead),"target"] = 1
market_data.loc[market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead),"target"] = 0
#The last rows do not have answers
market_data = market_data[:-look_ahead]
market_data

Unser neuer Datenrahmen.

Abb. 9: Einige der neuen zusätzlichen Zeilen, die wir zu unserem Datenrahmen hinzugefügt haben.


Nach der Durchführung der Merkmalsauswahl hat unser Algorithmus für die Rückwärtsauswahl die folgenden Variablen als wichtig identifiziert.

backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(),
                                                      k_features=(1,market_data.loc[:,predictors].shape[1]),
                                                      forward=False,
                                                      verbose=2,
                                                      scoring="accuracy",
                                                      cv=5
                                                     ).fit(market_data.iloc[:,1:-1],market_data.loc[:,"target"])
backward_feature_selector.k_feature_names_

('close', 'tick_volume', 'spread', 'SMA_5', 'SMA_50', 'MACDh_12_26_9', 'AO_5_34')

Aufbau unserer Handelsstrategie

Jetzt sind wir bereit, alles, was wir bisher gelernt haben, in eine konsilidierte Handelsstrategie umzusetzen. 

Zunächst passen wir unser Modell an alle uns zur Verfügung stehenden Trainingsdaten an, wobei wir nur die Spalten verwenden, die wir als nützlich erachtet haben.

predictors = ['close','tick_volume','spread','SMA_5','SMA_50','MACDh_12_26_9','AO_5_34']
model = LinearDiscriminantAnalysis()
model.fit(market_data.loc[:,predictors],market_data.loc[:,"target"])

Als Nächstes definieren wir Funktionen zum Abrufen von Marktdaten von unserem MetaTrader5-Terminal.

def get_prices():
    start = datetime(2024,6,1)
    end   = datetime.now()
    data  = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,start,end))
    #Add simple moving average technical indicator
    data.ta.sma(length=5,append=True)
    data.ta.sma(length=50,append=True)
    #Add awesome oscilator
    data.ta.ao(append=True)
    #Add macd
    data.ta.macd(append=True)
    #Delete missing rows
    data.dropna(inplace=True)
    data['time'] = pd.to_datetime(data['time'],unit='s')
    data.set_index('time',inplace=True)
    data = data.loc[:,['close','tick_volume','spread','SMA_5','SMA_50','MACDh_12_26_9','AO_5_34']]
    data = data.iloc[-2:,:]
    return(data)

Anschließend benötigen wir eine weitere Methode, um Vorhersagen aus unserem LDA-Modell zu erhalten.

#Get signals LDA model
def ai_signal(input_data,_model):
    #Get a forecast
    forecast = _model.predict(input_data)
    return forecast[1]

Jetzt können wir unsere Handelsstrategie aufbauen.

#Now we define the main body of our Python Moving Average Crossover Trading Bot
if __name__ == '__main__':
    #We'll use an infinite loop to keep the program running
    while True:
        #Fetching model prediction
        signal = ai_signal(get_prices(),model)
        
        #Decoding model prediction into an action
        if signal == 1:
            direction = 'buy'
        elif signal == 0:
            direction = 'sell'
        
        print(f'AI Forecast: {direction}')
        
        #Opening A Buy Trade
        #But first we need to ensure there are no opposite trades open on the same symbol
        if direction == 'buy':
            #Close any sell positions
            for pos in mt5.positions_get():
                if pos.type == 1:
                    #This is an open sell order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_totoal():
                #We have no open positions
                mt5.Buy(symbol,volume)
        
        #Opening A Sell Trade
        elif direction == 'sell':
            #Close any buy positions
            for pos in mt5.positions_get():
                if pos.type == 0:
                    #This is an open buy order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_get():
                #We have no open positions
                mt5.sell(symbol,volume)
        
        print('time: ', datetime.now())
        print('-------\n')
        time.sleep(60)

AI Forecast: sell

time:  2024-06-25 14:35:37.954923

-------


Unsere Handelsstrategie in Aktion

Abb. 10: Unsere Handelsstrategie in Aktion.


Implementierung in MQL5

Lassen Sie uns nun die MQL5-API nutzen, um unseren eigenen Klassifikator von Grund auf zu entwickeln. Die Erstellung eines nutzerdefinierten Klassifizierers in MQL5 bietet zahlreiche Vorteile. Als Autor bin ich der festen Überzeugung, dass native MQL5-Lösungen eine unvergleichliche Flexibilität bieten.

Wenn wir unser Modell in das ONNX-Format exportieren würden, bräuchten wir für jeden Markt, den wir handeln wollen, ein eigenes Modell. Außerdem würde der Handel über verschiedene Zeitrahmen hinweg mehrere ONNX-Modelle für jeden Markt erfordern. Indem wir unseren Klassifikator direkt in MQL5 aufbauen, erhalten wir die Möglichkeit, jeden Markt ohne diese Einschränkungen zu handeln.

Legen wir also ein neues Projekt an.

MQL5 EA

Abb. 11: Erstellung eines EA zur Umsetzung unserer Strategie.


Unsere erste Aufgabe besteht darin, einige globale Variablen zu definieren, die wir in unserem Programm verwenden werden.

//Global variables
int ma_5,ma_50;
double bid, ask;
double min_volume;
double ma_50_reading[],ma_5_reading[];
int size;
double current_prediction;
int state = -1;
matrix ohlc;
vector target;
double b_nort = 0;
double b_one = 0;
double b_two = 0;
long min_distance,atr_stop;

Wir werden auch Eingaben haben, die der Endnutzer einstellen kann.

//Inputs
int input lot_multiple = 20;
int input positions = 2;
double input sl_width = 0.4;

Schließlich werden wir die Handelsbibliothek importieren, die uns bei der Verwaltung unserer Positionen hilft.

//Libraries
#include <Trade\Trade.mqh>
CTrade Trade;

Im nächsten Schritt müssen wir Hilfsfunktionen definieren, die uns dabei helfen, Daten zu holen, die Trainingsdaten zu beschriften, unser Modell zu trainieren und Vorhersagen von unserem Modell zu erhalten. Wir beginnen mit der Definition einer Funktion, die Trainingsdaten abruft und das Ziel für unseren Klassifikator kennzeichnet. 

//+----------------------------------------------------------------------+
//|This function is responsible for getting our training data ready      |
//+----------------------------------------------------------------------+
void get_training_data(void)
  {
//How much data are we going to use?
   size = 100;
//Copy price data
   ohlc.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,1,size);
//Get indicator data
   ma_50 = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE);
   ma_5 = iMA(_Symbol,PERIOD_CURRENT,5,0,MODE_EMA,PRICE_CLOSE);
   CopyBuffer(ma_50,0,0,size,ma_50_reading);
   CopyBuffer(ma_5,0,0,size,ma_5_reading);
   ArraySetAsSeries(ma_50_reading,true);
   ArraySetAsSeries(ma_5_reading,true);
//Label the target
   target = vector::Zeros(size);
   for(int i = 0; i < size; i++)
     {
      if(ma_5_reading[i] > ma_50_reading[i])
        {
         target[i] = 1;
        }

      else
         if(ma_5_reading[i] < ma_50_reading[i])
           {
            target[i] = 0;
           }
     }

//Feedback
   Print("Done getting training data.");
  }

Unser Modell hat drei Koeffizienten, die es für seine Vorhersagen verwendet. Diese Koeffizienten müssen optimiert werden. Wir verwenden eine anfängerfreundliche Aktualisierungsgleichung, um diese Koeffizienten anzupassen. Indem wir den Fehler in den Vorhersagen unseres Modells messen, werden wir die Koeffizienten iterativ ändern, um den Fehler zu minimieren und die Genauigkeit unseres Systems zu verbessern. Bevor wir jedoch mit der Optimierung des Modells beginnen können, müssen wir zunächst definieren, wie unser Modell Vorhersagen trifft. 

//+----------------------------------------------------------------------+
//|This function is responsible for making predictions using our model   |
//+----------------------------------------------------------------------+
double model_predict(double input_one,double input_two)
  {
//We simply return the probability that the shorter moving average will rise above the slower moving average
   double prediction = 1 / (1 + MathExp(-(b_nort + (b_one * input_one) + (b_two * input_two))));
   return prediction;
  }

Da unser Modell nun Vorhersagen machen kann, können wir den Fehler in seinen Vorhersagen messen und den Optimierungsprozess starten. Zu Beginn werden alle drei Koeffizienten auf 0 gesetzt. Anschließend werden wir die Koeffizienten iterativ in kleinen Schritten anpassen, um den Gesamtfehler in unserem System zu minimieren. 

//+----------------------------------------------------------------------+
//|This function is responsible for  training our model                  |
//+----------------------------------------------------------------------+
bool train_model(void)
  {
//Update the coefficients
   double learning_rate = 0.3;
   for(int i = 0; i < size; i++)
     {
      //Get a prediction from the model
      current_prediction = model_predict(ma_5_reading[i],ma_50_reading[i]);
      //Update each coefficient
      b_nort = b_nort + learning_rate * (target[i] - current_prediction) * current_prediction * (1 - current_prediction) * 1;
      b_one = b_one + learning_rate * (target[i] - current_prediction) * current_prediction * (1-current_prediction) * ma_5_reading[i];
      b_two = b_two + learning_rate * (target[i] - current_prediction) * current_prediction * (1-current_prediction) * ma_50_reading[i];
      Print(current_prediction);
     }

//Show updated coefficient values
   Print("Updated coefficient values");
   Print(b_nort);
   Print(b_one);
   Print(b_two);
   return(true);
  }

Nachdem das Modell erfolgreich trainiert wurde, wäre es von Vorteil, eine Funktion zu haben, die die Vorhersagen unseres Modells abruft. Diese Vorhersagen dienen uns als Handelssignale. Es sei daran erinnert, dass eine Vorhersage von 1 ein Kaufsignal ist, was bedeutet, dass unser Modell erwartet, dass der kürzere gleitende Durchschnitt über den gleitenden Durchschnitt der längeren Periode steigt. Umgekehrt ist eine Vorhersage von 0 ein Verkaufssignal, was bedeutet, dass unser Modell erwartet, dass der kürzere gleitende Durchschnitt unter den längeren gleitenden Durchschnitt fallen wird.

//Get the model's current forecast
void current_forecast()
  {
//Get indicator data
   ma_50 = iMA(_Symbol,PERIOD_CURRENT,50,0,MODE_EMA,PRICE_CLOSE);
   ma_5 = iMA(_Symbol,PERIOD_CURRENT,5,0,MODE_EMA,PRICE_CLOSE);
   CopyBuffer(ma_50,0,0,1,ma_50_reading);
   CopyBuffer(ma_5,0,0,1,ma_5_reading);
//Get model forecast
   model_predict(ma_5_reading[0],ma_50_reading[0]);
   interpret_forecast();
  }

Wir möchten, dass unser Expert Advisor auf der Grundlage der Vorhersagen des Modells handelt. Daher werden wir eine Funktion schreiben, die die Vorhersage des Modells interpretiert und die entsprechenden Maßnahmen ergreift: kaufen, wenn das Modell 1 vorhersagt, und verkaufen, wenn das Modell 0 vorhersagt. 

//+----------------------------------------------------------------------+
//|This function is responsible for taking action on our model's forecast|
//+----------------------------------------------------------------------+
void interpret_forecast(void)
  {
   if(current_prediction > 0.5)
     {
      state = 1;
      Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,0,0,"Volatitlity Doctor AI");
     }

   if(current_prediction < 0.5)
     {
      state = 0;
      Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume * lot_multiple,bid,0,0,"Volatitlity Doctor AI");
     }
  }

Da unsere Anwendung nun aus Daten lernen, Vorhersagen treffen und auf der Grundlage dieser Vorhersagen handeln kann, müssen wir zusätzliche Funktionen erstellen, um offene Positionen zu verwalten. Insbesondere möchten wir, dass unser Programm jeder Position ein Trailing-Stop und ein Take-Profit hinzufügt, um unser Risiko zu steuern. Wir wollen keine offenen Positionen ohne ein definiertes Risikolimit haben. Die meisten Handelsstrategien empfehlen eine feste Stop-Loss-Größe von 100 Pips, aber wir wollen sicherstellen, dass unsere Stop-Loss- und Take-Profit-Levels dynamisch auf der Grundlage der aktuellen Marktvolatilität platziert werden. Daher werden wir die Average True Range (ATR) verwenden, um zu berechnen, wie weit oder eng unsere Stopps sein sollten. Wir werden ein Vielfaches der ATR verwenden, um diese Niveaus zu bestimmen.

//+----------------------------------------------------------------------+
//|This function is responsible for calculating our SL & TP values       |
//+----------------------------------------------------------------------+
void CheckAtrStop()
  {

//First we iterate over the total number of open positions
   for(int i = PositionsTotal() -1; i >= 0; i--)
     {

      //Then we fetch the name of the symbol of the open position
      string symbol = PositionGetSymbol(i);

      //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading
      if(_Symbol == symbol)
        {
         //Now we get information about the position
         ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
         double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
         long type = PositionGetInteger(POSITION_TYPE); //Position Type
         double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value

         //If the position is a buy
         if(type == POSITION_TYPE_BUY)
           {

            //The new stop loss value is just the ask price minus the ATR stop we calculated above
            double atr_stop_loss = NormalizeDouble(ask - ((min_distance * sl_width)/2),_Digits);
            //The new take profit is just the ask price plus the ATR stop we calculated above
            double atr_take_profit = NormalizeDouble(ask + (min_distance * sl_width),_Digits);

            //If our current stop loss is less than our calculated ATR stop loss
            //Or if our current stop loss is 0 then we will modify the stop loss and take profit
            if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0))
              {
               Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
              }
           }

         //If the position is a sell
         else
            if(type == POSITION_TYPE_SELL)
              {
               //The new stop loss value is just the ask price minus the ATR stop we calculated above
               double atr_stop_loss = NormalizeDouble(bid + ((min_distance * sl_width)/2),_Digits);
               //The new take profit is just the ask price plus the ATR stop we calculated above
               double atr_take_profit = NormalizeDouble(bid - (min_distance * sl_width),_Digits);

               //If our current stop loss is greater than our calculated ATR stop loss
               //Or if our current stop loss is 0 then we will modify the stop loss and take profit
               if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0))
                 {
                  Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                 }
              }
        }
     }
  }

Dann brauchen wir eine Funktion, die wir immer dann aufrufen, wenn wir neue Stop-Loss- und Take-Profit-Werte berechnen wollen.

//+------------------------------------------------------------------+
//|This function is responsible for updating our SL&TP values        |
//+------------------------------------------------------------------+
void ManageTrade()
  {
   CheckAtrStop();
  }

Nachdem wir nun unsere Hilfsfunktionen definiert haben, können wir sie in unseren Event-Handlern aufrufen. Wenn unser Programm zum ersten Mal geladen wird, wollen wir den Trainingsprozess einleiten. Daher werden wir unsere Hilfsfunktion, die für die Ausbildung unseres Experten verantwortlich ist, innerhalb des OnInit-Ereignishandlers aufrufen.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //Define important global variables
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   //Train the model
   get_training_data();
   if(train_model())
     {
      interpret_forecast();
     }
   return(INIT_SUCCEEDED);
  }

Nachdem wir das Modell trainiert haben, können wir mit dem eigentlichen Handel beginnen.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//Get updates bid and ask prices
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);

   if(PositionsTotal() == 0)
     {
      current_forecast();
     }

   if(PositionsTotal() > 0)
     {
      ManageTrade();
     }
  }

Ausgabe des Modells

Abb. 12: Ein Beispiel für die Ausgabe unseres Expert Advisors.

Unser EA in Aktion

Abb. 13: Unser Expert Advisor in Aktion.


Schlussfolgerung

In diesem Artikel haben wir gezeigt, dass es für unser Modell rechnerisch einfacher ist, Überkreuzungen gleitender Durchschnitte vorherzusagen, als Preisänderungen direkt zu prognostizieren.

Wie bei allen meinen Artikeln ziehe ich es vor, technische Erklärungen am Ende zu geben, während ich das Prinzip zuerst demonstriere. Für diese Beobachtung gibt es mehrere mögliche Gründe. Ein möglicher Grund dafür ist, dass sich die gleitenden Durchschnitte je nach gewähltem Zeitraum nicht so häufig kreuzen, wenn die Kurse ihre Richtung sprunghaft ändern. Mit anderen Worten: In den letzten zwei Stunden kann der Kurs erst gestiegen und dann gefallen sein oder zweimal die Richtung gewechselt haben. Während desselben Zeitraums haben sich die gleitenden Durchschnitte jedoch möglicherweise überhaupt nicht gekreuzt. Daher sind Überkreuzungen von gleitenden Durchschnitten möglicherweise leichter zu prognostizieren, da sie ihre Richtung nicht so schnell ändern wie der Kurs selbst. Dies ist nur eine mögliche Erklärung. Denken Sie selbst nach, ziehen Sie Ihre eigenen Schlüsse und teilen Sie sie in den Kommentaren mit.

Dabei handelt es sich um eine Technik, bei der ein lineares Modell iterativ trainiert wird, wobei bei jedem Schritt ein Merkmal auf der Grundlage seiner Auswirkungen auf die Modellgenauigkeit entfernt wird. Dieser Ansatz hilft dabei, die informativsten Merkmale zu identifizieren und beizubehalten, obwohl er anfällig dafür ist, wichtige Merkmale zu eliminieren, die aufgrund von Rauschen uninformativ erscheinen können.

Nachdem wir eine signifikante Beziehung zwischen zwei gleitenden Durchschnitten bestätigt hatten, untersuchten wir die Integration zusätzlicher technischer Indikatoren: MACD, Awesome Oscillator, Aroon, Chaikins Commodity Index und Percent Return. Diese Indikatoren zielen darauf ab, unsere Fähigkeit zur genauen Vorhersage von Überkreuzungen gleitender Durchschnitte zu verbessern. Die Auswahl dieser Indikatoren ist jedoch eine Kunst für sich, da ihre Auswirkungen auf die Modellleistung nicht vorhersehbar sind.

Insgesamt verbindet unser Ansatz empirische Validierung mit strategischer Merkmalsauswahl, um quantitativ zu beweisen, dass gleitende Durchschnitte in der Tat vorhergesagt werden können, und darüber hinaus wäre jeder Versuch, diese Handelsstrategie zu verbessern, definitiv keine Zeitverschwendung.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/15160

Datenwissenschaft und maschinelles Lernen (Teil 25): Forex-Zeitreihenvorhersage mit einem rekurrenten neuronalen Netzwerk (RNN) Datenwissenschaft und maschinelles Lernen (Teil 25): Forex-Zeitreihenvorhersage mit einem rekurrenten neuronalen Netzwerk (RNN)
Rekurrente neuronale Netze (RNNs) zeichnen sich dadurch aus, dass sie Informationen aus der Vergangenheit nutzen, um zukünftige Ereignisse vorherzusagen. Ihre bemerkenswerten Vorhersagefähigkeiten wurden in verschiedenen Bereichen mit großem Erfolg eingesetzt. In diesem Artikel werden wir RNN-Modelle zur Vorhersage von Trends auf dem Devisenmarkt einsetzen und ihr Potenzial zur Verbesserung der Vorhersagegenauigkeit beim Devisenhandel aufzeigen.
Algorithmen zur Optimierung mit Populationen: Der Boids-Algorithmus Algorithmen zur Optimierung mit Populationen: Der Boids-Algorithmus
Der Artikel befasst sich mit dem Boids Algorithmus, der auf einzigartigen Beispielen für das Verhalten von Tierschwärmen basiert. Der Boids-Algorithmus wiederum dient als Grundlage für die Schaffung einer ganzen Klasse von Algorithmen, die unter dem Namen „Schwarmintelligenz“ zusammengefasst werden.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Aufbau des Kerzenmodells Trend-Constraint (Teil 5): Nachrichtensystem (Teil III) Aufbau des Kerzenmodells Trend-Constraint (Teil 5): Nachrichtensystem (Teil III)
Dieser Teil der Artikelserie ist der Integration von WhatsApp mit MetaTrader 5 für Benachrichtigungen gewidmet. Zum besseren Verständnis haben wir ein Flussdiagramm beigefügt und werden die Bedeutung von Sicherheitsmaßnahmen bei der Integration erörtern. Der Hauptzweck von Indikatoren besteht darin, die Analyse durch Automatisierung zu vereinfachen, und sie sollten Benachrichtigungsmethoden enthalten, um Nutzer zu alarmieren, wenn bestimmte Bedingungen erfüllt sind. Erfahren Sie mehr in diesem Artikel.