English 日本語
preview
Datenwissenschaft und ML (Teil 46): Aktienmarktprognosen mit N-BEATS in Python

Datenwissenschaft und ML (Teil 46): Aktienmarktprognosen mit N-BEATS in Python

MetaTrader 5Handelssysteme |
108 1
Omega J Msigwa
Omega J Msigwa

Inhalt


Was ist N-BEATS?

N-BEATS (Neural Basis Expansion Analysis for Time Series) ist ein Deep-Learning-Modell, das speziell für die Prognose von Zeitreihen entwickelt wurde. Es bietet einen flexiblen Rahmen für univariate und multivariate Prognoseaufgaben.

Es wurde von Forschern von Element AI (jetzt Teil von ServiceNow) im Jahr 2019 mit dem Artikel „N-BEATS: Neural basis expansion analysis for interpretable time series forecasting“ vorgestellt.

Die Entwickler von Element AI haben dieses Modell entwickelt, um die Dominanz klassischer statistischer Modelle wie ARIMA und ETS in Zeitreihen herauszufordern, ohne dabei die Fähigkeiten klassischer maschineller Lernmodelle zu beeinträchtigen.

Wir alle wissen, dass die Vorhersage von Zeitreihen eine schwierige Aufgabe ist. Deshalb verlassen sich Experten für maschinelles Lernen und Anwender manchmal auf Deep-Learning-Modelle wie RNNs, LSTMs usw., die häufig eingesetzt werden:

  • Überkompliziert für einige einfache Aufgaben.
  • Schwer zu interpretieren.
  • Trotz ihrer Komplexität sind sie nicht durchgängig besser als die statistischen Basiswerte.

Herkömmliche Modelle für Zeitreihenprognosen, wie ARIMA, sind für viele Aufgaben zu einfach.

Daher beschlossen die Autoren/Entwickler, ein Deep-Learning-Modell für Zeitreihenprognosen zu entwickeln, das gut funktioniert, interpretierbar ist und keine domänenspezifischen Anpassungen benötigt.



Hauptziele des N-BEATS-Modells

Die Entwickler hatten klare Ziele und Beweggründe für die Entwicklung dieses maschinellen Lernwerkzeugs; sie wollten die Grenzen sowohl der klassischen als auch der auf Deep Learning basierenden Zeitreihenprognose überwinden.

Im Folgenden werden die Kernziele von N-BEATS ausführlich erläutert:

  1. Einfaches Modell ohne Einbußen bei der Genauigkeit

    Da einfachere/lineare Modelle für Zeitreihenprognosen wie ARIMA nicht in der Lage sind, komplexe Beziehungen zu erfassen, die besser von Modellen auf der Grundlage neuronaler Netze (Deep-Learning-Modelle) erfasst werden, haben sich die Entwickler entschieden, einfache Architekturen neuronaler Netze (MLPs) für Zeitreihenprognosen zu verwenden, da diese besser interpretierbar, schneller und einfacher zu beheben sind.

    Die Verwendung von Deep-Learning-Modellen (RNNs, LSTMs oder Transformers) erhöht die Komplexität des Systems, wodurch das Modell schwieriger abzustimmen ist und nur langsam trainiert werden kann.

  2. Interpretierbarkeit durch Struktur

    Da MLPs und andere auf neuronalen Netzen basierende Modelle keine interpretierbaren Ergebnisse liefern, wollten die Entwickler ein auf neuronalen Netzen basierendes Modell schaffen, das vom Menschen interpretierbare Prognosen liefern kann, indem es die Ausgabe in Trend- und saisonale Komponenten zerlegt, ähnlich wie klassische Zeitreihenmodelle wie ETS.

    Das N-BEATS-Modell ermöglicht eindeutige Zuordnungen, z. B. „Diese Spitze in den Daten ist auf den Trend zurückzuführen“ oder „dieser Rückgang ist saisonal bedingt“, dies wird durch Basiserweiterungsschichten (wie Polynom- oder Fourier-Basis) erreicht.

  3. Wettbewerbsfähige Genauigkeit ohne bereichsspezifische Anpassungen

    Ein weiteres Ziel, das mit diesem Modell erreicht werden soll, ist die Entwicklung eines universell einsetzbaren Modells, das für eine breite Palette von Zeitreihen geeignet ist und nur ein minimales manuelles Feature Engineering erfordert.

    Der Grund dafür ist, dass Modelle wie Prophet vom Nutzer verlangen, dass er Trend- und Saisonmuster angibt.

    N-BEATS lernt diese Muster automatisch, direkt aus den Daten.

  4. Unterstützung der globalen Modellierung über viele Zeitreihen hinweg

    Da viele Modelle für Zeitreihenprognosen nur eine einzige Zeitreihe prognostizieren können (Paneldaten), wurde dieses Modell für die Prognose mehrerer Zeitreihen entwickelt.
    Dies ist sehr praktisch, da wir bei Finanzdaten möglicherweise mehr als ein Merkmal haben, das wir prognostizieren wollen. Zum Beispiel die Vorhersage der Schlusskurse für NASDAQ und S&P 500 zur gleichen Zeit.
  5. Schnelles und skalierbares Training

    N-BEATS wurde entwickelt, um das Modell im Gegensatz zu RNNs oder aufmerksamkeitsbasierten Modellen schnell und einfach zu parallelisieren.

  6. Starke Basisleistung

    N-BEATS zielt darauf ab, die modernsten klassischen Methoden wie ARIMA und ETS in einer fairen, rückgeprüften Bewertung zu schlagen.

  7. Modularer und umfangreicher Aufbau

    Klassische Modelle für Zeitreihenprognosen sind statisch und nicht veränderbar. N-BEATS verfügt über eine leicht zu modifizierende Architektur, die das einfache Hinzufügen von nutzerdefinierten Blöcken, wie z. B. Trendblöcken, Saisonblöcken oder generischen Blöcken, ermöglicht.

Bevor wir dieses Modell einführen, sollten wir uns einen kurzen Moment Zeit nehmen, um zu verstehen, worum es dabei geht.


Wie funktioniert das N-BEATS-Modell (eine kurze mathematische Einführung)

Betrachten wir nun die Architektur des N-BEATS-Modells.

Abbildung 01

Oben befinden sich die Zeitreihendaten, die gefiltert und in verschiedenen Stapeln verarbeitet werden, von 1 bis M Stapeln. 

Jeder Stapel besteht aus verschiedenen Blöcken von 1 bis K; aus jedem Block erzeugt das Modell einen Prognosewert oder ein Residuum, das dann an den nächsten Stapel weitergegeben wird.

Abbildung 02

Jeder Block besteht aus vier vollständig verknüpften neuronalen Netzwerkschichten, die entweder einen Backcast oder eine Prognose erstellen.

Der Datenfluss in das Modell

01: In den Magazinen

Zu Beginn des Modells müssen der Rückblickszeitraum und der Prognosezeitraum festgelegt werden.

Der Rückblickszeitraum gibt an, wie weit wir in die Vergangenheit zurückblicken, um die Zukunft vorherzusagen, während der Prognosezeitraum angibt, wie weit wir in die Zukunft blicken wollen.

Nach der Festlegung des Rückblick- und Vorhersagezeitraums beginnen wir mit Stapel 01, der die Daten des Rückblickzeitraums aufnimmt und verarbeitet, um erste Vorhersagen zu treffen. Wenn unser Rückblickszeitraum beispielsweise die Schlusskurse der letzten Stunden für ein bestimmtes Instrument sind, verwendet Stack 01 diese Daten für die Prognose der nächsten 24 Stunden.

Die erhaltene erste Vorhersage und ihre Residuen (d.h. tatsächlicher minus vorhergesagter Wert) werden dann zur weiteren Verfeinerung an den nächsten Stapel (Stapel 02) weitergegeben.

Dieser Vorgang wird für alle nachfolgenden Stapel bis zum Stapel M wiederholt, wobei jeder Stapel die Vorhersagen des vorherigen Stapels verbessert.

Schließlich werden die Prognosen aus allen Stapeln kombiniert, um die Gesamtprognose zu erstellen. Wenn beispielsweise Stack 01 eine Spitze vorhersagt, passt Stack 02 den Trend an, und Stack M verfeinert die langfristigen Muster. Die globale Prognose integriert all diese Erkenntnisse, um eine möglichst genaue Vorhersage zu ermöglichen.

Sie können sich die Stapel als verschiedene Analyseebenen vorstellen. Stack 01 könnte sich auf die Erfassung kurzfristiger Muster konzentrieren, z. B. auf stündliche Schwankungen der Schlusskurse, während Stack 02 auf langfristige Muster ausgerichtet sein könnte, z. B. auf Trends bei den Tagesschlusskursen.

Jeder Stack verarbeitet die Eingabedaten, um einen einzigartigen Beitrag zur Gesamtprognose zu leisten.

02: Innerhalb von Eingabeblöcken

Beginnend mit Block 01, der die Stack-Eingabe entgegennimmt, bei der es sich um die ursprünglichen Lookback-Daten oder die Residuen aus dem vorherigen Stack handeln kann, werden diese Eingaben zur Erstellung einer Prognose und eines Backcasts verwendet. Wenn der Block beispielsweise die letzten 24 Stunden des Stromverbrauchs als Eingaben erhält, erstellt er die Prognose für die nächsten 24 Stunden und den Backcast, um die Eingabedaten anzunähern.

Der Backcast trägt dazu bei, dass die Modelle besser verstehen, wie die Vorhersage zu den Gesamtvorhersagen beiträgt.

Auch hier besteht jeder Stack aus mehreren Blöcken, die nacheinander arbeiten. Nachdem der erste Block den Stack-Input verarbeitet und seine Prognose und Backcasts erstellt hat, nimmt der nächste Block sowohl die Residuen des vorherigen Blocks als auch den ursprünglichen Stack-Input auf. Diese beiden Eingaben tragen dazu bei, dass der aktuelle Block genauere Vorhersagen macht als der vorherige, wobei auch die ursprünglichen Daten berücksichtigt werden, um die Gesamtgenauigkeit zu verbessern.

Diese iterative Verfeinerung innerhalb jedes Blocks eines Stapels sorgt dafür, dass die Vorhersagen über die Blöcke hinweg immer genauer werden. Nachdem alle Blöcke innerhalb eines Stapels die Daten verarbeitet haben, wird der letzte Rest des letzten Blocks (Block K) an den nächsten Stapel weitergegeben.

03: Zerlegen eines Blocks

Innerhalb jedes Blocks werden die Eingabedaten durch einen vierschichtigen, vollständig verknüpften Stack verarbeitet. Dieser Stack transformiert die Blockeingabe und extrahiert Merkmale, die bei der Erstellung des Backcasts und der Prognose helfen.

Die vollverknüpfte Schicht innerhalb jedes Blocks dient der Datenumwandlung und Merkmalsextraktion. Nachdem die Daten die vollverknüpften Schichten durchlaufen haben, werden sie in zwei Teile aufgeteilt (siehe Abbildung 02). Ein Teil für den Backcast und der andere für die Vorhersage.

Die Backcast-Ausgabe zielt auf eine Annäherung an die Eingabedaten ab und trägt dazu bei, die Residuen zu verfeinern, die an den nächsten Block weitergegeben werden, während die Prognose-Ausgabe vorausgesagte Werte für den Prognosezeitraum liefert.


Aufbau des N-BEATS-Modells in Python

Beginnen Sie mit der Installation aller Module, die in der Datei requirements.txt enthalten sind, die Sie in der Tabelle der Anhänge am Ende dieses Artikels finden.

pip install -r requirements.txt

Innerhalb von test.ipynb importieren wir zunächst alle erforderlichen Module.

import MetaTrader5 as mt5
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import warnings

sns.set_style("darkgrid")
warnings.filterwarnings("ignore")

Anschließend wird MetaTrader 5 initialisiert.

if not mt5.initialize():
    print("Metratrader5 initialization failed, Error code =", mt5.last_error())
    mt5.shutdown()

Wir sammeln 1000 Balken des täglichen Zeitrahmens für das Symbol NASDAQ (NAS100).

rates = mt5.copy_rates_from_pos("NAS100", mt5.TIMEFRAME_D1, 1, 1000)
rates_df = pd.DataFrame(rates)

Obwohl dieses Modell Techniken des klassischen maschinellen Lernens verwendet, die häufig multivariat sind, verfolgt N-BEATS einen univariaten Ansatz, der demjenigen ähnelt, der in traditionellen Zeitreihenmodellen wie ARIMA und VAR verwendet wird.

Im Folgenden wird beschrieben, wie wir univariate Daten konstruieren.

univariate_df = rates_df[["time", "close"]].copy()
univariate_df["ds"] = pd.to_datetime(univariate_df["time"], unit="s") # convert the time column to datetime
univariate_df["y"] = univariate_df["close"] # closing prices
univariate_df["unique_id"] = "NAS100" # add a unique_id column | very important for univariate models

# Final dataframe
univariate_df = univariate_df[["unique_id", "ds", "y"]].copy()

univariate_df

Ausgabe:

unique_id ds y
0 NAS100 2021-08-30 9.655648
1 NAS100 2021-08-31 9.654988
2 NAS100 2021-09-01 9.655763
3 NAS100 2021-09-02 9.654981
4 NAS100 2021-09-03 9.658335
... ... ... ...
995 NAS100 2025-07-07 10.028180
996 NAS100 2025-07-08 10.031142
997 NAS100 2025-07-09 10.037376
998 NAS100 2025-07-10 10.036098
999 NAS100 2025-07-11 10.033283
univariate_df["unique_id"] = "NAS100" # add a unique_id column | very important for univariate models

Das Modul neuralforecast, das über das Modell N-BEATS verfügt, ist sowohl für univariate als auch für Panel-Prognosen (mehrere Zeitreihen) geeignet. Das Merkmal unique_id teilt dem Modell mit, zu welcher Zeitreihe jede Zeile gehört. Dies ist besonders kritisch, wenn:

  • Sie prognostizieren mehrere Vermögenswerte oder Symbole (z. B. AAPL, TSLA, MSFT, EURUSD).
  • Sie möchten ein einzelnes Modell für viele Zeitreihen im Stapelverfahren trainieren.

Diese Variable ist aufgrund interner Gruppierungs- und Indexierungsmechanismen obligatorisch (auch für eine einzelne Reihe).

Zum Trainieren dieses Modells sind nur wenige Zeilen Code erforderlich.

from neuralforecast import NeuralForecast 
from neuralforecast.models import NBEATS # Neural Basis Expansion Analysis for Time Series

# Define model and horizon
horizon = 30  # forecast 30 days into the future

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=univariate_df)

Ausgabe:

Seed set to 1
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

  | Name         | Type          | Params | Mode 
-------------------------------------------------------
0 | loss         | MAE           | 0      | train
1 | padder_train | ConstantPad1d | 0      | train
2 | scaler       | TemporalNorm  | 0      | train
3 | blocks       | ModuleList    | 2.6 M  | train
-------------------------------------------------------
2.6 M     Trainable params
7.3 K     Non-trainable params
2.6 M     Total params
10.541    Total estimated model params size (MB)
31        Modules in train mode
0         Modules in eval mode

Epoch 99: 100%
 1/1 [00:01<00:00,  0.88it/s, v_num=32, train_loss_step=0.259, train_loss_epoch=0.259]
`Trainer.fit` stopped: `max_steps=100` reached.

Wir können die Vorhersagen und die tatsächlichen Werte auf derselben Achse visualisieren.

forecast = model.predict() # predict future values based on the fitted model

# Merge forecast with original data
plot_df = pd.merge(univariate_df, forecast, on='ds', how='outer') 

plt.figure(figsize=(7,5))
plt.plot(plot_df['ds'], plot_df['y'], label='Actual')
plt.plot(plot_df['ds'], plot_df['NBEATS'], label='Forecast')
plt.axvline(plot_df['ds'].max() - pd.Timedelta(days=horizon), color='gray', linestyle='--')
plt.legend()
plt.title('N-BEATS Forecast')
plt.show()

Ausgabe:

Nachfolgend sehen Sie ein Bild des zusammengeführten Datenrahmens.

unique_id_x ds y unique_id_y NBEATS
0 NAS100 2021-08-31   15599.4   NaN NaN
1 NAS100 2021-09-01 15611.5 NaN NaN
2 NAS100 2021-09-02 15599.3 NaN NaN
3 NAS100 2021-09-03 15651.7 NaN NaN
4 NAS100 2021-09-06 15700.4 NaN NaN
... ... ... ... ... ...
1025 NaN 2025-08-09 NaN NAS100 24235.187500  
1026 NaN 2025-08-10 NaN NAS100 24466.316406
1027 NaN 2025-08-11 NaN NAS100 24454.646484
1028 NaN 2025-08-12 NaN NAS100 24405.820312
1029 NaN 2025-08-13 NaN NAS100 24571.919922

Toll, das Modell hat 30 Tage in die Zukunft vorausgesagt.

Zu Evaluierungszwecken trainieren wir dieses Modell auf einem Datensatz und testen es auf dem anderen, so wie wir immer ein typisches maschinelles Lernmodell evaluieren.


Out-of-sample-Prognose mit dem N-BEATS-Modell

Wir beginnen mit der Aufteilung der Daten in Trainings- und Testdatenrahmen.

split_date = '2024-01-01'  # the split date for training and testing

train_df = univariate_df[univariate_df['ds'] < split_date]
test_df = univariate_df[univariate_df['ds'] >= split_date]

Wir trainieren das Modell mit dem Trainingsdatensatz.

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=train_df)

Da die Vorhersagefunktion die nächste „N“-Anzahl von Tagen entsprechend dem Vorhersagehorizont vorhersagt, müssen wir, um dieses Modell anhand von Prognosen außerhalb der Stichprobe zu bewerten, den Datenrahmen für das vorhergesagte Ergebnis mit dem aktuellen Datenrahmen zusammenführen.

test_forecast = model.predict() # predict future 30 days based on the training data

df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
df_test.dropna(inplace=True) # drop rows with NaN values

df_test

Ausgabe:

unique_id ds y NBEATS
3 NAS100 2024-01-02   16554.3   16569.835938  
4 NAS100 2024-01-03 16368.1 16596.839844
5 NAS100 2024-01-04 16287.2 16603.513672
6 NAS100 2024-01-05 16307.1 16729.607422
9 NAS100 2024-01-08 16631.0 16854.746094
10 NAS100 2024-01-09 16672.4 16918.466797
11 NAS100 2024-01-10 16804.7 16958.833984
12 NAS100 2024-01-11 16814.3 17130.972656
13 NAS100 2024-01-12 16808.8 17055.396484
16 NAS100 2024-01-15 16828.7 17272.376953
17 NAS100 2024-01-16 16841.9 17227.498047
18 NAS100 2024-01-17 16727.7 17408.158203
19 NAS100 2024-01-18 16987.0 17499.619141
20 NAS100 2024-01-19 17336.7 17318.767578
23 NAS100 2024-01-22 17329.3 17399.562500
24 NAS100 2024-01-23 17426.1 17289.140625
25 NAS100 2024-01-24 17503.1 17236.478516
26 NAS100 2024-01-25 17469.4 17188.691406
27 NAS100 2024-01-26 17390.1 17315.134766


Wir fahren fort, dieses Ergebnis zu bewerten.

from sklearn.metrics import mean_absolute_percentage_error, r2_score

mape = mean_absolute_percentage_error(df_test['y'], df_test['NBEATS'])
r2_score_ = r2_score(df_test['y'], df_test['NBEATS'])

print(f"mean_absolute_percentage_error (MAPE): {mape} \n R2 Score: {r2_score_}")

Ausgabe:

mean_absolute_percentage_error (MAPE): 0.015779373328172166 
R2 Score: 0.35350182943487285

Nach der MAPE-Metrik sind die Modellvorhersagen prozentual sehr genau; der R2-Wert von 0,35 bedeutet jedoch, dass nur 35 % der Variation der Zielvariablen erklärt werden.

Unten sehen Sie das Diagramm, das die tatsächlichen und die prognostizierten Werte auf einer einzigen Achse enthält.

Wie jedes andere Zeitreihenprognosemodell muss auch N-BEATS regelmäßig mit neuen sequentiellen Daten aktualisiert werden, um relevant und genau zu bleiben. In den vorangegangenen Beispielen haben wir das Modell auf der Grundlage der Vorhersagen bewertet, die 30 Tage im Voraus auf der Grundlage der täglichen Daten erstellt wurden, aber das ist nicht der richtige Weg, da dem Modell viele tägliche Informationen dazwischen entgehen.

Der richtige Weg ist es, das Modell mit neuen Daten zu aktualisieren, sobald neue Daten auftauchen.

Das N-BEATS-Modell bietet eine einfache Möglichkeit, das Modell mit neuen Daten zu aktualisieren, ohne es neu zu trainieren, was viel Zeit spart.

Wenn Sie laufen:

NBEATS.predict(df=new_dataframe)

Das Modell wendet die trainierten Gewichte auf die neuen Daten an, während es eine Inferenz des Modells durchführt, die das Modell mit neuen Informationen aktualisiert und es für die kürzlich erhaltenen Daten aus einem Datenrahmen relevant macht.


Prognose von mehreren Zeitreihen 

Wie bereits in den Kernzielen des N-BEATS-Abschnitts beschrieben.  Dieses Modell wurde entwickelt, um die Prognose von mehreren Zeitreihen bestmöglich zu handhaben.

Dies ist eine beeindruckende Fähigkeit dieses Modells, denn es nutzt die in einer Reihe erlernten Muster, um die Gesamtprognose für beide Zeitreihen zu verbessern.

Im Folgenden erfahren Sie, wie Sie sich diese Fähigkeit zunutze machen können:

Wir beginnen damit, Daten für jedes Symbol von MetaTrader 5 zu sammeln.

rates_nq = mt5.copy_rates_from_pos("NAS100", mt5.TIMEFRAME_D1, 1, 1000)
rates_df_nq = pd.DataFrame(rates_nq)

rates_snp = mt5.copy_rates_from_pos("US500", mt5.TIMEFRAME_D1, 1, 1000)
rates_df_snp = pd.DataFrame(rates_snp)

Wir bereiten jeden einzelnen univariaten Datenrahmen vor.

# NAS100
rates_df_nq["ds"] = pd.to_datetime(rates_df_nq["time"], unit="s")
rates_df_nq["y"] = rates_df_nq["close"]
rates_df_nq["unique_id"] = "NAS100"
df_nq = rates_df_nq[["unique_id", "ds", "y"]]

# US500
rates_df_snp["ds"] = pd.to_datetime(rates_df_snp["time"], unit="s")
rates_df_snp["y"] = rates_df_snp["close"]
rates_df_snp["unique_id"] = "US500"
df_snp = rates_df_snp[["unique_id", "ds", "y"]]

Wir kombinieren beide Datenrahmen und sortieren die Werte nach der Datenspalte und ihrer unique_id.

multivariate_df = pd.concat([df_nq, df_snp], ignore_index=True) # combine both dataframes
multivariate_df = multivariate_df.sort_values(['unique_id', 'ds']).reset_index(drop=True) # sort by unique_id and date

multivariate_df

Ausgabe:

unique_id ds y
0 NAS100 2021-08-31   15599.4  
1 NAS100 2021-09-01 15611.5
2 NAS100 2021-09-02 15599.3
3 NAS100 2021-09-03 15651.7
4 NAS100 2021-09-06 15700.4
... ... ... ...
1995 US500 2025-07-08 6229.9
1996 US500 2025-07-09 6264.9
1997 US500 2025-07-10 6280.3
1998 US500 2025-07-11 6255.8
1999 US500 2025-07-14 6271.9


Wie zuvor haben wir die Daten in Trainings- und Testdatenrahmen aufgeteilt.

split_date = '2024-01-01'  # the split date for training and testing

train_df = multivariate_df[multivariate_df['ds'] < split_date] 
test_df = multivariate_df[multivariate_df['ds'] >= split_date]

Dann trainieren wir das Modell auf die gleiche Weise wie zuvor.

from neuralforecast import NeuralForecast 
from neuralforecast.models import NBEATS # Neural Basis Expansion Analysis for Time Series

# Define model and horizon
horizon = 30  # forecast 30 days into the future

model = NeuralForecast(
    models=[NBEATS(h=horizon, # predictive horizon of the model
                   input_size=90, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                   max_steps=100, # maximum number of training steps (epochs)
                   scaler_type='robust', # scaler type for the time series data
                   )], 
    freq='D' # frequency of the time series data
)

# Fit the model
model.fit(df=train_df)

Wir erstellen Prognosen auf der Grundlage von Daten, die außerhalb der Stichprobe liegen.

test_forecast = model.predict() # predict future 30 days based on the training data

df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
df_test.dropna(inplace=True) # drop rows with NaN values

df_test

Ausgabe:

unique_id ds y NBEATS
6 NAS100 2024-01-02 16554.3 16267.765625
7 US500 2024-01-02 4747.4 4706.230957
8 NAS100 2024-01-03 16368.1 16230.808594
9 US500 2024-01-03 4707.3 4706.517090
10 NAS100 2024-01-04 16287.2 16136.568359
11 US500 2024-01-04 4690.9 4686.380859
12 NAS100 2024-01-05 16307.1 16218.930664
13 US500 2024-01-05 4695.8 4704.896484


Schließlich bewerten wir das Modell für beide Instrumente und stellen die tatsächlichen und die prognostizierten Werte auf der gleichen Achse dar.

from sklearn.metrics import mean_absolute_percentage_error, r2_score

unique_ids = df_test['unique_id'].unique()

for unique_id in unique_ids:
    
    df_unique = df_test[df_test['unique_id'] == unique_id].copy()
    
    mape = mean_absolute_percentage_error(df_unique['y'], df_unique['NBEATS'])
    r2_score_ = r2_score(df_unique['y'], df_unique['NBEATS'])

        
    print(f"Unique ID: {unique_id} - MAPE: {mape}, R2 Score: {r2_score_}")
    
    
    plt.figure(figsize=(7, 4))
    plt.plot(df_unique['ds'], df_unique['y'], label='Actual', color='blue')
    plt.plot(df_unique['ds'], df_unique['NBEATS'], label='Forecast', color='orange')
    plt.title(f'Actual vs Forecast for {unique_id}')
    plt.xlabel('Date')
    plt.ylabel('Value')
    plt.legend()
    plt.show()

Ausgabe:

Unique ID: NAS100 - MAPE: 0.0221775184381915, R2 Score: -0.16976266747298419

Unique ID: US500 - MAPE: 0.007412931117247571, R2 Score: 0.3782229067061038




Handelsentscheidungen treffen mit N-BEATS in MetaTrader 5

Da wir nun in der Lage sind, Vorhersagen aus diesem Modell zu erhalten, können wir es in einen Python-basierten Handelsroboter integrieren. 

In der Datei NBEATS-tradingbot.py implementieren wir zunächst die Funktion zum Training des gesamten Modells:

def train_nbeats_model(forecast_horizon: int=30,
                       start_bar: int=1,
                       number_of_bars: int=1000, 
                       input_size: int=90, 
                       max_steps: int=100, 
                       mt5_timeframe: int=mt5.TIMEFRAME_D1,
                       symbol_01: str="NAS100",
                       symbol_02: str="US500",
                       test_size_percentage: float=0.2,
                       scaler_type: str='robust'):
    
    """    
        Train NBEATS model on NAS100 and US500 data from MetaTrader 5.
        
        Args:
            start_bar: starting bar to be used to in CopyRates from MT5
            number_of_bars: The number of bars to extract from MT5 for training the model
            forecast_horizon: the number of days to predict in the future
            input_size: number of previous days to consider for prediction
            max_steps: maximum number of training steps (epochs)
            mt5_timeframe: timeframe to be used for the data extraction from MT5
            symbol_01: unique identifier for the first symbol (default is NAS100)
            symbol_02: unique identifier for the second symbol (default is US500)
            test_size_percentage: percentage of the data to be used for testing (default is 0.2)
            scaler_type: type of scaler to be used for the time series data (default is 'robust')
        
        Returns:
            NBEATS: the n-beats model object
    """
        
    # Getting data from MetaTrader 5

    rates_nq = mt5.copy_rates_from_pos(symbol_01, mt5_timeframe, start_bar, number_of_bars)
    rates_df_nq = pd.DataFrame(rates_nq)

    rates_snp = mt5.copy_rates_from_pos(symbol_02, mt5_timeframe, start_bar, number_of_bars)
    rates_df_snp = pd.DataFrame(rates_snp)

    if rates_df_nq.empty or rates_df_snp.empty:
        print(f"Failed to retrieve data for {symbol_01} or {symbol_02}.")
        return None
    
    # Getting NAS100 data
    rates_df_nq["ds"] = pd.to_datetime(rates_df_nq["time"], unit="s")
    rates_df_nq["y"] = rates_df_nq["close"]
    rates_df_nq["unique_id"] = symbol_01
    df_nq = rates_df_nq[["unique_id", "ds", "y"]]

    # Getting US500 data
    rates_df_snp["ds"] = pd.to_datetime(rates_df_snp["time"], unit="s")
    rates_df_snp["y"] = rates_df_snp["close"]
    rates_df_snp["unique_id"] = symbol_02
    df_snp = rates_df_snp[["unique_id", "ds", "y"]]

    multivariate_df = pd.concat([df_nq, df_snp], ignore_index=True) # combine both dataframes
    multivariate_df = multivariate_df.sort_values(['unique_id', 'ds']).reset_index(drop=True) # sort by unique_id and date

    # Group by unique_id and split per group
    train_df_list = []
    test_df_list = []

    for _, group in multivariate_df.groupby('unique_id'):
        group = group.sort_values('ds')
        split_idx = int(len(group) * (1 - test_size_percentage))

        train_df_list.append(group.iloc[:split_idx])
        test_df_list.append(group.iloc[split_idx:])

    # Concatenate all series
    train_df = pd.concat(train_df_list).reset_index(drop=True)
    test_df = pd.concat(test_df_list).reset_index(drop=True)

    # Define model and horizon

    model = NeuralForecast(
        models=[NBEATS(h=forecast_horizon, # predictive horizon of the model
                    input_size=input_size, # considered autorregresive inputs (lags), y=[1,2,3,4] input_size=2 -> lags=[1,2].
                    max_steps=max_steps, # maximum number of training steps (epochs)
                    scaler_type=scaler_type, # scaler type for the time series data
                    )], 
        freq='D' # frequency of the time series data
    )

    # fit the model on the training data
    
    model.fit(df=train_df)

    test_forecast = model.predict() # predict future 30 days based on the training data

    df_test = pd.merge(test_df, test_forecast, on=['ds', 'unique_id'], how='outer') # merge the test data with the forecast
    df_test.dropna(inplace=True) # drop rows with NaN values


    unique_ids = df_test['unique_id'].unique()
    for unique_id in unique_ids:
        
        df_unique = df_test[df_test['unique_id'] == unique_id].copy()
        
        mape = mean_absolute_percentage_error(df_unique['y'], df_unique['NBEATS'])   
        print(f"Unique ID: {unique_id} - MAPE: {mape:.2f}")
    
    return model

Diese Funktion kombiniert alle zuvor besprochenen Trainingsverfahren und liefert das N-BEATS-Modellobjekt für direkte Vorhersagen.

Die Funktion zur Vorhersage der nächsten Werte folgt einem ähnlichen Ansatz wie die Trainingsfunktion.

def predict_next(model, 
                  symbol_unique_id: str, 
                  input_size: int=90):
    
    """
        Predict the next values for a given unique_id using the trained model.
        
        Args:
            model (NBEATS): the trained NBEATS model
            symbol_unique_id (str): unique identifier for the symbol to predict
            input_size (int): number of previous days to consider for prediction
        
        Returns:
            DataFrame: containing the predicted values for the next days
    """
    
    # Getting data from MetaTrader 5

    rates = mt5.copy_rates_from_pos(symbol_unique_id, mt5.TIMEFRAME_D1, 1, input_size * 2)  # Get enough data for prediction
    if rates is None or len(rates) == 0:
        print(f"Failed to retrieve data for {symbol_unique_id}.")
        return pd.DataFrame()
    
    rates_df = pd.DataFrame(rates)
    
    rates_df["ds"] = pd.to_datetime(rates_df["time"], unit="s")
    rates_df = rates_df[["ds", "close"]].rename(columns={"close": "y"})
    rates_df["unique_id"] = symbol_unique_id
    rates_df = rates_df.sort_values(by="ds").reset_index(drop=True)
    
    # Prepare the dataframe for reference & prediction    
    univariate_df = rates_df[["unique_id", "ds", "y"]]
    forecast = model.predict(df=univariate_df)
    
    return forecast

Die Datengröße des Modells ist doppelt so groß wie die Größe der Eingaben, die beim Training verwendet werden – nur um genügend Daten zu erhalten.

Rufen wir die Vorhersagefunktion zweimal für jedes einzelne Symbol auf und beobachten wir die resultierenden Datenrahmen.

trained_model = train_nbeats_model(max_steps=10)

print(predict_next(trained_model, "NAS100").head())
print(predict_next(trained_model, "US500").head())

Ausgabe:

Predicting DataLoader 0: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 45.64it/s]
  unique_id         ds        NBEATS
0    NAS100 2025-07-16  22836.160156
1    NAS100 2025-07-17  22931.242188
2    NAS100 2025-07-18  22984.792969
3    NAS100 2025-07-19  23037.224609
4    NAS100 2025-07-20  23119.804688
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
Predicting DataLoader 0: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 71.43it/s]
  unique_id         ds       NBEATS
0     US500 2025-07-16  6234.584961
1     US500 2025-07-17  6254.846680
2     US500 2025-07-18  6261.153320
3     US500 2025-07-19  6282.960449
4     US500 2025-07-20  6307.293945
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs

Da die resultierenden Datenrahmen für beide Symbole mehrere tägliche Schlusskursvorhersagen für 30 Tage in der Zukunft enthalten (das heutige Datum ist der 16.07.2025), müssen wir einen für den aktuellen Tag (das heutige Datum) vorhergesagten Wert auswählen.

today = dt.datetime.now().date() # today's date

forecast_df = predict_next(trained_model, "NAS100") # Get the predicted values for NAS100, 30 days into the future
today_pred_close_nq = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for NAS100

forecast_df = predict_next(trained_model, "US500") # Get the predicted values for US500, 30 days into the future
today_pred_close_snp = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for US500

print(f"Today's predicted NAS100 values:", today_pred_close_nq)
print(f"Today's predicted US500 values:", today_pred_close_snp)

Ausgabe:

Today's predicted NAS100 values: [22836.16]
Today's predicted US500 values: [6234.585]

Schließlich können wir diese vorhergesagten Werte in einer einfachen Handelsstrategie verwenden.

# Trading modules 

from Trade.Trade import CTrade
from Trade.PositionInfo import CPositionInfo
from Trade.SymbolInfo import CSymbolInfo

SLIPPAGE = 100 # points
MAGIC_NUMBER = 15072025 # unique identifier for the trades
TIMEFRAME = mt5.TIMEFRAME_D1 # timeframe for the trades

# Create trade objects for NAS100 and US500
m_trade_nq = CTrade(magic_number=MAGIC_NUMBER,
                 filling_type_symbol = "NAS100",
                 deviation_points=SLIPPAGE)

m_trade_snp = CTrade(magic_number=MAGIC_NUMBER,
                 filling_type_symbol = "US500",
                 deviation_points=SLIPPAGE)

# Training the NBEATS model INITIALLY
trained_model = train_nbeats_model(max_steps=10,
                                    input_size=90,
                                    forecast_horizon=30,
                                    start_bar=1,
                                    number_of_bars=1000,
                                    mt5_timeframe=TIMEFRAME,
                                    symbol_01="NAS100",
                                    symbol_02="US500"
                                   )

m_symbol_nq = CSymbolInfo("NAS100") # Create symbol info object for NAS100
m_symbol_snp = CSymbolInfo("US500") # Create symbol info object for US500

m_position = CPositionInfo() # Create position info object


def pos_exists(pos_type: int, magic: int, symbol: str) -> bool: 
    
    """ Checks whether a position exists given a magic number, symbol, and the position type

    Returns:
        bool: True if a position is found otherwise False
    """
    
    if mt5.positions_total() < 1: # no positions whatsoever
        return False
    
    positions = mt5.positions_get()
    
    for position in positions:
        if m_position.select_position(position):
            if m_position.magic() == magic and m_position.symbol() == symbol and m_position.position_type()==pos_type:
                return True
            
    return False


def RunStrategyandML(trained_model: NBEATS):

    today = dt.datetime.now().date() # today's date

    forecast_df = predict_next(trained_model, "NAS100") # Get the predicted values for NAS100, 30 days into the future
    today_pred_close_nq = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for NAS100

    forecast_df = predict_next(trained_model, "US500") # Get the predicted values for US500, 30 days into the future
    today_pred_close_snp = forecast_df[forecast_df['ds'].dt.date == today]['NBEATS'].values # extract today's predicted close value for US500

    # convert numpy arrays to float values
    
    today_pred_close_nq = float(today_pred_close_nq[0]) if len(today_pred_close_nq) > 0 else None
    today_pred_close_snp = float(today_pred_close_snp[0]) if len(today_pred_close_snp) > 0 else None

    print(f"Today's predicted NAS100 values:", today_pred_close_nq)
    print(f"Today's predicted US500 values:", today_pred_close_snp)
    
    # Refreshing the rates for NAS100 and US500 symbols
    
    m_symbol_nq.refresh_rates()
    m_symbol_snp.refresh_rates()
    
    ask_price_nq = m_symbol_nq.ask() # get today's close price for NAS100
    ask_price_snp = m_symbol_snp.ask() # get today's close price for US500

    # Trading operations for the NAS100 symol
        
    if not pos_exists(pos_type=mt5.ORDER_TYPE_BUY, magic=MAGIC_NUMBER, symbol="NAS100"):
        if today_pred_close_nq > ask_price_nq: # if predicted close price for NAS100 is greater than the current ask price
            # Open a buy trade 
            m_trade_nq.buy(volume=m_symbol_nq.lots_min(), 
                            symbol="NAS100",
                            price=m_symbol_nq.ask(),
                            sl=0.0,
                            tp=today_pred_close_nq) # set take profit to the predicted close price
    
    print("ask: ", m_symbol_nq.ask(), "bid: ", m_symbol_nq.bid(), "last: ", ask_price_nq)
    print("tp: ", today_pred_close_nq, "lots: ", m_symbol_nq.lots_min())
    print("istp within range: ", (m_symbol_nq.ask() - today_pred_close_nq) > m_symbol_nq.stops_level())
    
    if not pos_exists(pos_type=mt5.ORDER_TYPE_SELL, magic=MAGIC_NUMBER, symbol="NAS100"):
        if today_pred_close_nq < ask_price_nq: # if predicted close price for NAS100 is less than the current bid price
            m_trade_nq.sell(volume=m_symbol_nq.lots_min(), 
                             symbol="NAS100",
                             price=m_symbol_nq.bid(),
                             sl=0.0,
                             tp=today_pred_close_nq) # set take profit to the predicted close price
    
    
    # Buy and sell operations for the US500 symbol
    
        
    if not pos_exists(pos_type=mt5.ORDER_TYPE_BUY, magic=MAGIC_NUMBER, symbol="US500"):
        if today_pred_close_snp > ask_price_snp: # if the predicted price for US500 is greater than the current ask price
            m_trade_snp.buy(volume=m_symbol_snp.lots_min(), 
                            symbol="US500",
                            price=m_symbol_snp.ask(),
                            sl=0.0,
                            tp=today_pred_close_snp)

    if not pos_exists(pos_type=mt5.ORDER_TYPE_SELL, magic=MAGIC_NUMBER, symbol="US500"):
        if today_pred_close_snp < ask_price_snp: # if the predicted price for US500 is less than the current bid price
            m_trade_snp.sell(volume=m_symbol_snp.lots_min(), 
                             symbol="US500",
                             price=m_symbol_snp.bid(),
                             sl=0.0,
                             tp=today_pred_close_snp)


RunStrategyandML(trained_model=trained_model) # Run the strategy and ML model once to initialize

Ausgabe:

Zwei neue Handelsgeschäfte wurden eröffnet.

Schließlich können wir den Trainingsverlauf planen und das Modell so automatisieren, dass es zu Beginn eines jeden Tages Vorhersagen trifft und Handelsgeschäfte eröffnet.

# Schedule the strategy to run every day at 00:00
schedule.every().day.at("00:00").do(RunStrategyandML, trained_model=trained_model)

while True:
    
    schedule.run_pending()
    time.sleep(10)


Schlussfolgerung

N-BEATS ist ein leistungsstarkes Modell für die Zeitreihenanalyse und -prognose. Es übertrifft die klassischen Modelle wie ARIMA, VAR, PROPHET usw. für dieselbe Aufgabe, da es im Kern neuronale Netze nutzt, die sich durch die Erfassung komplexer Muster auszeichnen.

N-BEATS ist eine perfekte Alternative für diejenigen, die Zeitreihenprognosen mit nicht-traditionellen Modellen für Zeitreihenprognosen durchführen wollen.

Ich bin begeistert, dass es Normalisierungstechniken und Auswertungswerkzeuge in seinem Werkzeugkasten hat, wodurch dieses Modell bequem zu nutzen ist.

Es ist zwar ein gutes Modell, wie jedes andere Modell des maschinellen Lernens auf der Welt, hat aber auch einige Nachteile, die zu beachten sind, darunter:

  1. Sie sind in erster Linie für univariate Prognosen gedacht.
    Wie zuvor gesehen, benötigen sie in erster Linie nur zwei Merkmale im Trainingsdatenrahmen, ds (Zeitstempel) und die Zielvariable gekennzeichnet als y, ähnlich wie bei dem zuvor besprochenen PROPHET-Modell.

    Bei Finanzdaten reichen diese beiden Merkmale nicht aus, um die Marktdynamik zu erfassen.
  2. Sie können bei verrauschten Daten zu stark angepasst werden.
    Wie andere tiefe Netze kann sich auch N-BEATS bei verrauschten Daten überanpassen.
  3. Seine Interpretierbarkeit ist begrenzt.
    Obwohl N-BEATS eine Basisfunktionszerlegung für die Interpretierbarkeit enthält, ist es immer noch ein tiefes neuronales Netz; es ist weniger interpretierbar als andere Modelle für Zeitreihenprognosen wie ARIMA und PROPHET.
  4. In der Industrie ist es weniger verbreitet.
    Wahrscheinlich haben Sie auch noch nie von diesem Modell gehört. 

    Obwohl es in akademischen Benchmarks sehr gut abschneidet, wurde dieses Modell im Vergleich zu anderen Modellen wie ARIMA, XGBoost, LSTM usw. in der Gemeinschaft des maschinellen Lernens noch nicht weit verbreitet. Sie werden online nicht viele Beiträge finden, die dieses Modell beschreiben.

Mit freundlichen Grüßen.


Tabelle der Anhänge

Dateiname Beschreibung und Verwendung
Trade\PositionInfo.py Enthält die Klasse CPositionInfo, ähnlich der in MQL5 verfügbaren Klasse; die Klasse liefert Informationen über alle geöffneten Positionen im MetaTrader 5.
Trade\SymbolInfo.py Enthält die Klasse CSymbolInfo, ähnlich der in MQL5 verfügbaren Klasse; die Klasse liefert alle Informationen über das ausgewählte Symbol aus MetaTrader 5.
Trade\Trade.py Enthält die Klasse CTrade, ähnlich wie die in MQL5 verfügbare; die Klasse bietet Funktionen zum Öffnen und Schließen von Trades in MetaTrader 5.
error_description.py Verfügt über die Funktionen zur Umwandlung von MetaTrader 5-Fehlercodes in menschenlesbare Informationen.
NBEATS-Tradingbot.py Ein Python-Skript, das das N-BEATS-Modell verwendet, um Handelsentscheidungen zu treffen.
test.ipynb Ein Jupyter-Notebook zum Experimentieren mit dem N-BEATS-Modell.
requirements.txt  Enthält alle in diesem Projekt verwendeten Python-Abhängigkeiten. 


Quellen und Referenzen

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

Beigefügte Dateien |
Attachments.zip (125.7 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (1)
nevar
nevar | 21 Juli 2025 in 11:26
Sehr guter Artikel, danke Omega.
Da er die schnelle Fourrier-Transformation für die Zerlegung verwendet, die es dem Modell ermöglicht, sowohl kurzfristige Saisonalität als auch langfristige Trends separat zu erfassen. Ist die Verwendung des Schlusskurses selbst als Eingabe oder Ausgabe für den N-BEATS-Algorithmus geeignet?

MQL5-Handelswerkzeuge (Teil 6): Dynamisches holografisches Dashboard mit Impulsanimationen und Steuerelementen MQL5-Handelswerkzeuge (Teil 6): Dynamisches holografisches Dashboard mit Impulsanimationen und Steuerelementen
In diesem Artikel erstellen wir ein dynamisches holografisches Dashboard in MQL5 zur Überwachung von Symbolen und Zeitrahmen mit RSI, Volatilitätswarnungen und Sortieroptionen. Wir fügen eine pulsierende Animationen, interaktive Schaltflächen und holografische Effekte hinzu, um das Tool visuell ansprechend und reaktionsschnell zu gestalten.
Entwicklung des Price Action Analysis Toolkit (Teil 33): Candle-Range Theory Tool Entwicklung des Price Action Analysis Toolkit (Teil 33): Candle-Range Theory Tool
Verbessern Sie Ihr Marktverständnis mit der Candle-Range Theory Suite für MetaTrader 5, einer vollständig MQL5-nativen Lösung, die rohe Preisbalken in Echtzeit-Volatilitätsinformationen umwandelt. Die leichtgewichtige Bibliothek CRangePattern vergleicht die „True Range“ jeder Kerze mit einer adaptiven ATR und klassifiziert sie in dem Moment, in dem sie schließt. Der CRT-Indikator projiziert diese Klassifizierungen dann als scharfe, farbkodierte Rechtecke und Pfeile auf Ihr Chart, die sich verengende Konsolidierungen, explosive Ausbrüche und Verengungen der gesamten Spanne in dem Moment anzeigen, in dem sie auftreten.
Entwicklung des Price Action Analysis Toolkit (Teil 34): Umwandlung von Marktrohdaten in Prognosemodellen mithilfe einer fortschrittlichen Pipeline der Datenerfassung Entwicklung des Price Action Analysis Toolkit (Teil 34): Umwandlung von Marktrohdaten in Prognosemodellen mithilfe einer fortschrittlichen Pipeline der Datenerfassung
Haben Sie schon einmal einen plötzlichen Marktanstieg verpasst oder wurden Sie von einem solchen überrascht? Der beste Weg, aktuelle Ereignisse zu antizipieren, besteht darin, aus historischen Mustern zu lernen. Mit dem Ziel, ein ML-Modell zu trainieren, zeigt Ihnen dieser Artikel zunächst, wie Sie ein Skript in MetaTrader 5 erstellen, das historische Daten aufnimmt und sie zur Speicherung an Python sendet. Lesen Sie weiter, um die einzelnen Schritte in Aktion zu sehen.
Automatisieren von Handelsstrategien in MQL5 (Teil 24): London Session Breakout System mit Risikomanagement und Trailing Stops Automatisieren von Handelsstrategien in MQL5 (Teil 24): London Session Breakout System mit Risikomanagement und Trailing Stops
In diesem Artikel entwickeln wir ein London Session Breakout System, das Ausbrüche vor der Londoner Handelsspanne identifiziert und schwebende Aufträge mit anpassbaren Handelsarten und Risikoeinstellungen platziert. Wir integrieren Funktionen wie Trailing Stops, Risiko-Ertrags-Verhältnisse, maximale Drawdown-Grenzen und ein Kontrollpanel für die Überwachung und Verwaltung in Echtzeit.