Python im Trading – Datenanalyse, Automatisierung und Forschung
Python hilft, Marktdaten zu ordnen, ein Trading-Journal auszuwerten und Berechnungen ohne manuelles Kopieren von Formeln zu wiederholen. Besonders nützlich ist es, wenn eine Tabelle zu klein oder schwer wartbar wird. Dieser Artikel führt von der Einrichtung einer Umgebung zu einem Skript, das Daten lädt, ihre Qualität prüft und Ergebnisse speichert. Er erklärt auch den Unterschied zwischen Forschung im Notebook und mit einem Handelskonto verbundener Software sowie die dafür erforderlichen Schutzmaßnahmen.

- Python in der Forschung und Python in der Ausführung bedeuten unterschiedliche Verantwortung – ein Notebook zur Statistikberechnung darf folgenlos abstürzen. Code mit Kontozugriff braucht Limits, ein Protokoll, Zeitüberschreitungen, einen Testmodus und einen kontrollierten Stopp
- Die Umgebung muss reproduzierbar sein – Python-Version, isolierte virtuelle Umgebung, festgeschriebene Bibliotheksversionen, Datenquelle, Zeitzone und Datumsbereich. Ohne diese Angaben ist „bei mir funktioniert es“ kein Ergebnis, sondern eine Anekdote
- pandas und numpy ersetzen die Tabellenkalkulation bei der OHLCV-Analyse – Umrechnung von M1 auf H4, gleitender Mittelwert, Drawdown, Korrelationen zwischen Paaren. Das Arbeitsblatt endet bei 1.048.576 Zeilen; Tickdaten beginnen weit darüber
- Schlüssel und Passwörter gehören niemals in den Code – nur in eine Umgebungsvariable oder eine Konfigurationsdatei außerhalb des Repositorys. Ein ins Skript geschriebener Schlüssel landet in der Git-Historie und bleibt dort nach jedem „Löschen“
- Eine API bedeutet keinen unbegrenzten Zugriff – sie hat Authentifizierung, eine Anfragengrenze und Fehlercodes; die Testumgebung liegt manchmal, aber nicht immer, auf einem eigenen Host. Code ohne Behandlung von HTTP 429 und ohne Zeitlimit wird früher oder später im ungünstigsten Moment unterbrochen
- Look-ahead entsteht durch eine einzige Verschiebung im Code – eine zum Kerzenschluss beschlossene Position wirkt erst auf die Rendite der nächsten Kerze. Das Multiplizieren des Signals mit der Rendite derselben Zeile erzeugt ein Ergebnis, das der Markt nie geliefert hat
- Nicht ausgeführter Code ist ungeprüfter Code – kompilieren und „korrekt aussehen“ sind nicht dasselbe wie Ausführung auf Daten. Sechs ausführbare Blöcke wurden offline mit offengelegten Daten und Mocks geprüft. Die Ausgabeblöcke zeigen tatsächliche Testergebnisse; Installationsbefehle beschreiben die Umgebungseinrichtung, während die Orderübermittlung ausdrücklich eine nicht ausführbare Strukturskizze bleibt
1. Wo Python wirklich hilft und wo es nur wie eine Lösung aussieht
Eine Tabellenkalkulation bewältigt ein Trading-Journal so lange, wie Ihre Geduld reicht. Wo diese Grenze liegt, hängt von Rechner, Formelanzahl und Tabellenaufbau ab; eine runde Trade-Anzahl beschreibt sie daher nicht. Eine Grenze ist jedoch unverrückbar und unabhängig von allem, denn sie steht in der Dateiformatspezifikation: Ein Excel-Arbeitsblatt fasst 1.048.576 Zeilen und 16.384 Spalten [16]. Ein Jahr Tickhistorie für ein einziges Währungspaar umfasst ein Vielfaches davon — und kein Trick umgeht diese Grenze.
Python ist dabei keine Magie. Es löst vier konkrete Probleme, die eine Tabelle gar nicht oder nur mit stundenlanger Arbeit löst:
- Datenumfang – begrenzt durch Arbeitsspeicher und Speicherformat, nicht durch die Zeilengrenze eines Arbeitsblatts
- Wiederholbarkeit – dasselbe Skript liefert morgen auf denselben Daten dasselbe Ergebnis. Eine Tabelle mit von Hand gezogenen Formeln garantiert das nicht
- Zeitintervallwechsel ohne Handarbeit – die Aggregation von M1 zu H4 unter Erhalt von OHLCV benötigt einen einzigen
resample-Aufruf statt einer Spalte mit Hilfsformeln - Datenbeschaffung und -bereinigung automatisieren – ein zeitgesteuertes Skript lädt, prüft und speichert Daten ohne Ihre Anwesenheit
Das ist alles. Python verschafft keinen Marktvorteil, verbessert keine Strategiequalität und ändert keine Ausführungsbedingungen beim Broker. Es verkürzt den Weg zwischen Frage und Antwort — und zeigt gerade deshalb schneller, dass eine Idee nicht funktioniert.
Was Python nicht leistet. Für Handel mit extrem niedriger Latenz eignet es sich nicht — dort zählen Mikrosekunden, und eine interpretierte Sprache mit automatischer Speicherbereinigung ist eine schlechte Wahl. Ebenso falsch ist aber „Python ist zu langsam fürs Trading“: Bei einer Strategie, die zur Eröffnung der nächsten H1-Kerze einsteigt, ist die Python-Rechenzeit gegenüber dem Weg der Order durch Netzwerk und Brokerserver vernachlässigbar.
Der Geschwindigkeitsgewinn durch Vektorisierung beruht auf einem Mechanismus, nicht auf Magie: Eine Operation auf einer ganzen Spalte läuft in kompiliertem numpy-Code statt in einer Interpreterschleife. Die Größenordnung des Unterschieds hängt von Daten, Operation und Hardware ab. Wollen Sie eine konkrete Zahl nennen, messen Sie auf Ihrem Rechner mit dem timeit -Modul der Standardbibliothek [4] und dokumentieren Sie ihn zusammen mit den Bibliotheksversionen. Ein fremder Benchmark ohne Umgebungsangaben ist keine Messung.
Analytische Werkzeuge eines Traders im Vergleich:
| Merkmal | Excel | Python | Pine Script | MQL4/MQL5 |
|---|---|---|---|---|
| Datengrenze | 1.048.576 Zeilen pro Arbeitsblatt | Durch Arbeitsspeicher begrenzt | Vom TradingView-Tarif abhängig | Vom Broker bereitgestellte Historie |
| Ausführungsort des Codes | Lokal | Lokal, auf Server oder in der Cloud | Innerhalb der TradingView-Umgebung | Im MetaTrader-Terminal |
| Kontozugriff | Keiner | Nur über Broker-API oder Vermittlungspaket | Keiner — ein Alarm kann eine weitere Kette auslösen | Nativ im Terminal |
| Datenquelle | Alles, was Sie laden | Beliebig, muss aber genannt werden | TradingView-Datenfeed | Broker-Datenfeed |
| Reproduzierbarkeit | Gering (manuelle Formeln) | Hoch bei festgeschriebenen Versionen | Mittel (Pine-Version und Plattformdaten) | Mittel (Terminal-Build und Brokerdaten) |
| Lernaufwand | Gering | Mittel | Gering bis mittel | Mittel bis hoch |
Die Unterscheidung in der letzten Tabellenzeile ist wichtiger, als sie aussieht. Pine Script läuft in der TradingView-Umgebung, MQL im MetaTrader-Terminal und Python dort, wo Sie es starten. Diese Freiheit macht Sie für die Umgebung verantwortlich: Bibliotheksversionen, Systemzeitzone, Verfügbarkeit und die Frage, ob das Skript überhaupt auf das Konto zugreifen darf.
openpyxl liest und schreibt .xlsx-Dateien direkt aus Python; ihre neueste Veröffentlichung stammt aus Juni 2024 (3.1.5) [13] und das ist hier kein Warnsignal: Das .xlsx-Format ist etabliert, die Bibliothek hängt weder von NumPy noch von pandas ab. Die Regel aus Abschnitt 2 schließt sie daher nicht aus, sondern verlangt nur, diesen Stand zu kennen [STAND: 2026-08-22]. Python erweitert die Tabelle dort, wo diese endet.2. Reproduzierbare Umgebung: Versionen, venv, requirements
Wenn ein fremdes Skript „nicht funktioniert“, liegt das sehr häufig nicht am Code. Der Autor nutzte andere Bibliotheksversionen und hielt das nirgends fest. Bei Marktdatenanalysen ist das keine Unannehmlichkeit — es entwertet das Ergebnis, weil es nicht reproduzierbar ist.
Eine Umgebung ist reproduzierbar, wenn Sie sechs Dinge angeben können: Python-Version, Isolationsmethode, festgeschriebene Abhängigkeitsversionen, Datenquelle mit Datumsbereich, Zeitzone und Zufallsstartwert überall dort, wo das Ergebnis vom Zufall abhängt. Fehlt etwas, ist das Ergebnis eine Geschichte, keine Messung.
Eine virtuelle Umgebung ist nicht optional. „Globale“ Bibliotheksinstallation führt dazu, dass das Aktualisieren eines Projekts ein anderes beschädigt. Isolation ist kostenlos, dauert wenige Sekunden und beruht auf dem venv -Modul der Standardbibliothek [4]:
# Windows, PowerShell
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1
# macOS / Linux
python3.12 -m venv .venv
source .venv/bin/activate
# für beide Systeme
python -m pip install --upgrade pip
python -m pip install pandas numpy matplotlib requests python-dotenv
python -m pip freeze > requirements.txt
Die letzte Zeile wird am häufigsten ausgelassen. pip freeze erfasst die exakten Versionen aller tatsächlich installierten Pakete, einschließlich unbekannter Abhängigkeiten. Diese Datei gehört neben den Code ins Repository. Auf einem anderen Rechner stellen Sie die Umgebung dann wieder her mit python -m pip install -r requirements.txt, nicht durch Raten.
Die für die aktuelle Offline-Prüfung verwendete Umgebung der Rechenbeispiele in den Abschnitten 4 und 9: Windows 11, Python 3.12.10, pandas 3.0.5 und NumPy 2.5.3, geprüft am 8. Oktober 2026. Die folgende Liste nennt die zu diesen Berechnungen gehörenden Pakete; sie ist ein begrenztes Inventar, kein Export aller auf dem Rechner installierten Pakete[1][3].
# Abhängigkeitsversionen der lokalen Rechenprüfung
numpy==2.5.3
pandas==3.0.5
python-dateutil==2.9.0.post0
six==1.17.0
tzdata==2026.4
# Interpreter
Python 3.12.10
Beachten Sie, dass pandas weitere Pakete nutzt: Eine Umgebungsbeschreibung endet nicht bei einer Bibliotheksversion. Abhängigkeiten können Unterschiede zwischen Rechnern verursachen. Die obigen Installationsbefehle zeigen ein Verfahren zur Projekteinrichtung; sie protokollieren keine im Rahmen dieser Prüfung ausgeführte Installation. Erzeugen Sie die vollständige Datei requirements.txt in Ihrer eigenen Umgebung mit python -m pip freeze und bewahren Sie sie zusammen mit dem Code auf.
Auch diese Datei beschreibt nicht die Umgebung des gesamten Artikels, sondern nur die der Rechenabschnitte. So muss sie gelesen werden. Das Fragment zur HTTP-Fehlerbehandlung in Abschnitt 7 lief in einer Umgebung, ergänzt um requests 2.34.2; der Test des Sicherheitsfilters in Abschnitt 8 braucht kein Paket außerhalb der Standardbibliothek. Die Versionen von matplotlib und python-dotenv werden nicht genannt, da sie in keinem tatsächlich ausgeführten Beispiel verwendet wurden. Sie „der Ordnung halber“ aufzulisten hieße, eine ungeprüfte Zahl aufzuschreiben. Bauen Sie eine Umgebung für alles auf, nehmen Sie den Installationsbefehl oben als Ausgangspunkt und erzeugen Sie Ihr eigenes pip freeze; nur diese Abhängigkeitsdatei sagt etwas über Ihre Ergebnisse aus.

Notebook oder .py-Datei? Beides, aber für unterschiedliche Zwecke. Jupyter ist gut, solange Sie noch nicht wissen, wonach Sie suchen: Zelle ausführen, Ergebnis ansehen, Parameter ändern. Eine Eigenschaft disqualifiziert es jedoch für Ausführungscode: Der Zustand der Variablen hängt von der Ausführungsreihenfolge der Zellen ab, nicht von ihrer Reihenfolge in der Datei. Ein in der Reihenfolge 1–3–2 ausgeführtes Notebook sieht genauso aus wie ein der Reihe nach ausgeführtes. Code mit Kontozugriff muss eine einfache .py -Datei sein, von oben nach unten ausgeführt, mit Tests und Protokoll.
Anaconda und Google Colab sind Abkürzungen mit eigenen Kosten. Anaconda installiert viele Pakete auf einmal und hat eine eigene Abhängigkeitsverwaltung. Das erleichtert den Einstieg, erschwert aber die genaue Umgebungsbeschreibung, wenn Sie conda mit pip mischen. Colab läuft ohne Installation im Browser und eignet sich zum Lernen. Die Umgebung wird jedoch bei jeder Sitzung neu aufgebaut, und Bibliotheksversionen ändern sich ohne Ihr Zutun. Für Material, das in einem Jahr noch wiederholbar sein soll, verlangen beide zusätzliche Disziplin.
Bibliotheken für den Einstieg – vier Einträge, nicht vierzig (fünf Pakete, denn der erste Eintrag ist ein Paar):
pandasundnumpy– Datenstrukturen und Berechnungenmatplotlib– Diagrammerequests– HTTP-Kommunikation mit einer APIpython-dotenv– Konfiguration aus einer Datei außerhalb des Repositorys laden
Technische Indikatoren ergänzen Sie, sobald Sie sie tatsächlich brauchen. Eine Korrektur älterer Anleitungen: TA-Lib veröffentlicht offizielle binäre Wheels mit C-Bibliothek auf PyPI. Für eine unterstützte Kombination aus Betriebssystem, Architektur und Python reicht daher ein einfaches pip install TA-Lib erfordert weder einen inoffiziellen Download noch eine eigene Kompilierung[9]. Prüfen Sie vor der Installation das konkrete Wheel; das ist kein Versprechen, dass es auf jedem Rechner funktioniert. Die am 8. Oktober 2026 geprüften Metadaten nennen TA-Lib 0.8.1 und pandas-ta weiterhin in Version 0.4.71b0, veröffentlicht im September 2025[13]. Letzteres schreibt numba==0.61.2 vor, weshalb die Prüfung allein der pandas-Anforderung nicht reicht. Ein historischer Installationstest unter Python 3.14.3 vom 22. August 2026 scheiterte an dieser Abhängigkeit mit only versions >=3.10,<3.14 are supported. Diese Beobachtung gilt nicht automatisch für die aktuellen Beispiele unter Python 3.12.10. Die vorliegende Prüfung las PyPI-Metadaten; TA-Lib und pandas-ta wurden dabei weder installiert noch ausgeführt. Die nebenstehende Prüfregel greift daher zu kurz: Eine aktuelle Veröffentlichung und eine kompatible pandas-Anforderung belegen nicht, dass sich alle Abhängigkeiten installieren und korrekt ausführen lassen. Prüfen Sie auch festgeschriebene kompilierte Bibliotheken, die Verfügbarkeit von Wheels für Ihren Interpreter und einen Test mit wenigen Daten. Metadaten der beiden letzten Veröffentlichungen beschreiben nicht die gesamte Projektgeschichte; frühere Unterbrechungen beurteilen Sie anhand des Repositorys.
Der Zufallsstartwert. Wo immer das Ergebnis vom Zufall abhängt — Stichprobenteilung, Bootstrapping, Simulation, Modellinitialisierung — setzen und dokumentieren Sie den Startwert. In numpy: rng = np.random.default_rng(20260822) [3], danach nutzen Sie nur dieses Objekt. Ohne es liefern zwei Läufe desselben Skripts zwei unterschiedliche Ergebnisse; unklar bleibt, welches Sie beschreiben.
3. pandas und numpy: OHLCV-Daten, Zeitindex, Zeitzonen
numpy übernimmt Berechnungen auf Zahlenarrays, pandas die Struktur: Ein DataFrame ist ein Array mit benannten Spalten und einem Index, meist einem Zeitindex. Bei Marktdaten ist der Zeitindex wichtiger als die Berechnungen selbst, denn er entscheidet, ob Sie überhaupt das berechnen, was Sie zu berechnen glauben.
Laden – pd.read_csv() bewältigt Dateien mit Millionen Zeilen, erkennt Trennzeichen und verarbeitet Datumsangaben selbst, wenn Sie die Spalte angeben. Standardmäßig kennt es jedoch keine Zeitzone — und das muss zuerst geklärt werden.
Zeitintervall ändern – M1 zu H4 aggregieren und OHLCV erhalten, mit einem einzigen Aufruf: df.resample('4h').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'tick_volume':'sum'}). Achten Sie auf zwei kaum gesetzte Argumente, die die Interpretation bestimmen: label legt fest, ob das Fenster mit Anfang oder Ende beschriftet wird, und closed bestimmt, welcher Intervallrand zum Fenster gehört. Für die meisten Frequenzen liegt die Beschriftung standardmäßig am linken Rand [1], der Kerzenzeitstempel bezeichnet also ihre Eröffnung. In pandas 3.0.5 liefert resample('4h') für acht Stundenkerzen ab Mitternacht die Beschriftungen 00:00 und 04:00. Setzt Ihre Signalkonvention den Kerzenschluss voraus, während die Daten mit der Eröffnung beschriftet sind, verschieben Sie unbemerkt das gesamte Ergebnis um ein Intervall.
Gleitende Statistiken – df['close'].rolling(20).mean() liefert einen gleitenden Mittelwert, .rolling(20).std() die Standardabweichung für Bollinger-Bänder, .ewm(span=20).mean() einen exponentiellen Mittelwert [1]. Alle werden ausschließlich aus Daten berechnet, die relativ zur aktuellen Zeile in der Vergangenheit liegen. Auf dieser Eigenschaft beruht die Korrektheit aller weiteren Schritte.
Filtern – eine zusammengesetzte Bedingung wird einmal geschrieben: df[(df['sma20'] > df['sma50']) & (df['rsi'] < 30)]. Klammern um jede Bedingung sind zwingend, weil & in Python stärker bindet als ein Vergleich. Ohne sie entsteht ein Fehler statt einer stillschweigend falschen Antwort — hier ist das eine gute Nachricht.
Die Zeitzone ist eine Entscheidung, kein Detail. Brokerdaten tragen gewöhnlich den Zeitstempel des Brokerservers, Daten externer Anbieter meist UTC, und das Betriebssystem nutzt möglicherweise Ortszeit mit den zur gewählten Zone gehörenden Zeitumstellungen. Vermischen sich diese drei Uhren, untersucht eine Analyse der „Londoner Sitzung“ etwas anderes als die Londoner Sitzung.
Praktische Regel: Zuerst die Quellzeitzone feststellen, dann in UTC rechnen. Bei Zeitstempeln mit explizitem Offset oder Z verwenden Sie pd.to_datetime(column, utc=True). Serverzeitstempel ohne Offset zuerst einlesen, dann mit tz_localize() die bestätigte Zone zuordnen und dabei mehrdeutige sowie nicht existierende Stunden behandeln. Danach folgt tz_convert("UTC"). Allein utc=True behandelt Werte ohne Zeitzone als UTC und ermittelt nicht die Brokerzeitzone. In Europe/London nur für die Darstellung umrechnen[1]. Ein Index mit expliziter Zeitzone und eine dokumentierte Datenquelle ermöglichen konsistentes Zusammenführen von Zeitreihen.
Fehlende Daten. OHLCV-Historien haben Lücken: Wochenenden, Feiertage, technische Pausen, nicht quotierte Instrumente. pandas bietet df.ffill(), das Lücken mit dem letzten bekannten Wert füllt. Bei Kursdaten ist das meist keine gute Idee: Es erzeugt Kerzen ohne Volatilität und drückt die Standardabweichung. Besser ist, fehlende Zeiträume auszuschließen und ihre Anzahl zu zählen.
df.fillna(method='ffill'), die in einige Jahre alten Anleitungen üblich ist, wurde abgeschafft: Der Parameter method von fillna() wurde in pandas 2.1 als veraltet markiert [2] und in pandas 3.0 entfernt. Das hat praktische Folgen: Die alte Form gibt keine Warnung mehr aus, sondern beendet das Programm mit TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'. Korrekte Formen sind df.ffill() und df.bfill(). Die Signatur in der Dokumentation von pandas 3.0.5 enthält diesen Parameter nicht mehr — DataFrame.fillna(value, *, axis=None, inplace=False, limit=None) [1]. Signatur und Meldungstext wurden durch Ausführung unter pandas 3.0.5 geprüft, nicht durch Lesen der Dokumentation. [STAND: 2026-08-22]Mehrere Instrumente gleichzeitig. Für Währungsportfolioanalysen bietet pandas einen hierarchischen Index (MultiIndex): Ein DataFrame hält Daten mehrerer Paare, nach Paar und Zeit indiziert. So berechnen Sie Korrelationen auf gemeinsamer Zeitachse — sofern zuvor alle Reihen in dieselbe Zeitzone und auf dasselbe Zeitstempelraster gebracht wurden. Andernfalls korrelieren Sie gegeneinander verschobene Daten.
Ergebnisse speichern. df.to_csv() ist menschenlesbar und universell. df.to_parquet() liefert kleinere Dateien und schnelleres Einlesen und erhält dabei Spaltentypen und Indexzeitzone [1] — CSV schreibt alles als Text, der beim erneuten Laden wieder interpretiert werden muss. Für umfangreiche Arbeitsdaten ist Parquet praktischer, braucht jedoch eine zusätzliche Schreibkomponente. Nehmen Sie sie deshalb auf in requirements.txt.
4. Minimalbeispiel: CSV → UTC → Renditen → Lückenprüfung → Speichern
Ihr erstes Skript sollte keine Order senden und sich nicht einmal mit einem Broker verbinden. Es sollte etwas Nützliches, Überprüfbares und völlig Sicheres tun: Datei laden, Zeitangaben klären, Renditen berechnen, Datenfehler klar benennen und das Ergebnis speichern.
Annahmen. Eine CSV-Datei mit den Spalten time,open,high,low,close,tick_volume. Die Spalte time im ISO-8601-Format mit Zeitzonenkennung (Z bedeutet UTC). Das Intervall ist als stündlich deklariert. Die Testdatei enthält bewusst drei Fehler, die bei echten Exporten vorkommen: einen doppelten Zeitstempel, eine fehlende Stunde und eine leere Volumenzelle. Diese sechs Zeilen stehen direkt im Code, damit Sie ohne Dateidownload exakt dasselbe Ergebnis erhalten — ein nicht reproduzierbares Ergebnis ist kein Ergebnis. Umgebung: Python 3.12.10, pandas 3.0.5, numpy 2.5.3, Windows 11, [STAND: 2026-10-08].
"""Minimalbeispiel: CSV -> UTC -> Log-Rendite -> Lückenprüfung -> Speichern."""
import io
from pathlib import Path
import numpy as np
import pandas as pd
# Daten direkt im Code, damit das Ergebnis ohne Dateidownload reproduzierbar ist.
# Sechs Zeilen mit drei bewussten Fehlern: doppelter Zeitstempel 01:00,
# fehlende Stunde 03:00 und leere Volumenzelle um 02:00.
CSV_DATA = """time,open,high,low,close,tick_volume
2026-06-01T00:00:00Z,1.08140,1.08170,1.08131,1.08153,812
2026-06-01T01:00:00Z,1.08153,1.08190,1.08150,1.08176,640
2026-06-01T01:00:00Z,1.08153,1.08195,1.08149,1.08182,655
2026-06-01T02:00:00Z,1.08182,1.08188,1.08130,1.08141,
2026-06-01T04:00:00Z,1.08120,1.08125,1.08040,1.08050,903
2026-06-01T05:00:00Z,1.08050,1.08061,1.07980,1.07993,774
"""
OUTPUT = Path("EURUSD_H1_returns.csv")
INTERVAL = "1h" # deklarierte Frequenz der Eingabedaten
def load_data(source) -> pd.DataFrame:
"""Lädt CSV und setzt einen Index mit Zeitzone UTC.
Sortiert bewusst nicht: Die Zeilenreihenfolge ist selbst ein Hinweis
auf die Exportqualität. Sortiert wird erst nach dem Bericht.
"""
df = pd.read_csv(source)
# utc=True erzwingt eine Zone; sonst kann pandas einen Index ohne Zeitzone liefern
df["time"] = pd.to_datetime(df["time"], utc=True, format="ISO8601")
return df.set_index("time")
def quality_report(df: pd.DataFrame, interval: str) -> dict:
"""Zähler, die vor jeder Berechnung geprüft werden müssen."""
expected = pd.date_range(df.index.min(), df.index.max(), freq=interval, tz="UTC")
return {
"rows": int(len(df)),
"index_duplicates": int(df.index.duplicated().sum()),
"missing_cells": int(df.isna().sum().sum()),
"missing_timestamps": int(len(expected.difference(df.index))),
# VOR dem Sortieren gemessen: Nach sort_index() wäre diese Prüfung immer
# wahr und würde eine Datei mit durcheinandergeratener Chronologie nie erkennen.
"file_chronology_ok": bool(df.index.is_monotonic_increasing),
}
def add_returns(df: pd.DataFrame) -> pd.DataFrame:
"""Log-Rendite = ln(close_t / close_(t-1)). Einheit: natürlicher Logarithmus, einheitenlos."""
df = df[~df.index.duplicated(keep="last")].copy()
df["log_return"] = np.log(df["close"]).diff()
return df
def main() -> None:
df = load_data(io.StringIO(CSV_DATA)) # in der Praxis: load_data(Path("EURUSD_H1.csv"))
print("Qualitätsprüfung:", quality_report(df, INTERVAL))
df = add_returns(df.sort_index())
print("Kerzen nach Deduplizierung:", len(df))
print("Summe der Log-Renditen:", round(float(df["log_return"].sum()), 6))
print("Standardabweichung der Log-Renditen (1h- und 2h-Intervalle):",
round(float(df["log_return"].std()), 6))
df.to_csv(OUTPUT)
print("gespeichert:", OUTPUT)
if __name__ == "__main__":
main()
Ausgabe des Testlaufs mit den direkt im Code enthaltenen Daten und der beschriebenen Umgebung — exakt reproduzierbar:
Qualitätsprüfung: {'rows': 6, 'index_duplicates': 1, 'missing_cells': 1, 'missing_timestamps': 1, 'file_chronology_ok': True}
Kerzen nach Deduplizierung: 5
Summe der Log-Renditen: -0.00148
Standardabweichung der Log-Renditen (1h- und 2h-Intervalle): 0.000467
gespeichert: EURUSD_H1_returns.csv
Interpretation. Das Skript erkannte alle drei eingebauten Fehler, bevor es etwas berechnete — darin liegt sein ganzer Wert. Die Summe der logarithmischen Renditen beträgt −0,00148. Bei fünf nach der Deduplizierung verbliebenen Kerzen — vier Intervallen zwischen aufeinanderfolgenden beobachteten Preisen — entspricht das einem gesamten Kursrückgang von etwa 0,15 Prozent im untersuchten Fenster. Sechs ist die Zeilenanzahl der Datei, nicht die Kerzenanzahl: Eine Zeile war doppelt. Die Standardabweichung von 0,000467 ist eine Größe pro einzelner H1-Kerze, weder jährlich noch täglich; ohne Intervallangabe ist jeder Vergleich sinnlos. Die logarithmische Rendite ist einheitenlos und über die Zeit additiv — deshalb wird sie anders als die einfache Rendite summiert statt multipliziert.
Grenzen. Fünf Kerzen veranschaulichen die Mechanik, bilden aber keine statistische Stichprobe. Aus so wenigen Beobachtungen folgt nichts über den Markt. Das Skript modelliert weder Spread noch Provision oder Slippage, weil es kein Backtest ist: Es berechnet Kursrenditen, kein Kontoergebnis. Die Erkennung fehlender Zeitstempel setzt die tatsächlich stündliche Frequenz voraus. Über ein Wochenende hinweg sind zahlreiche „fehlende“ Zeitstempel völlig normal; die Alarmschwelle richtet sich daher nach dem Handelskalender. Eine weitere Falle liegt in der Kennzahl selbst: Das Skript erkennt die Lücke, füllt sie aber nicht. Die über die fehlende Stunde berechnete Rendite umfasst deshalb zwei Intervalle statt eines. Hier betrifft das eine der vier Renditen; trotzdem wurde die gesamte Größe als „pro H1-Kerze“ bezeichnet. Das ist eine Vereinfachung, keine präzise Messung. Ehrlich sind zwei Wege: Daten auf ein vollständiges Stundenraster umindizieren und die Lücke leer lassen oder die Vermischung der Zeithorizonte ausdrücklich als Messgrenze nennen.
Warum die Reihenfolge vor dem Sortieren geprüft wird. Würde load_data() mit einem sort_index() -Aufruf enden, wäre das Feld file_chronology_ok immer wahr. Die im Bericht angekündigte Prüfung könnte nichts erkennen, und eine Datei mit verwürfelter Reihenfolge bestünde spurlos. Eine Prüfung, die nicht scheitern kann, ist schlimmer als keine: Sie täuscht Validierung vor. Deshalb wird die Reihenfolge auf den frisch geladenen Dateidaten gemessen; sortiert wird erst nach dem Bericht.
Prüfstatus: Dieses Fragment wurde in der angegebenen Umgebung ausgeführt; die Ausgabe oben stammt tatsächlich vom Programm. Andere pandas-Versionen können das Dictionary oder die Rundung anders formatieren.
5. Visualisierung: eine Frage pro Diagramm
Eine einzelne Ergebniskennzahl verbirgt das Wichtigste: wann die Strategie verlor, wie lange und ob das gesamte Ergebnis auf drei Trades beruht. Ein Diagramm ist keine Berichtsdekoration, sondern eine Frage an die Daten, die eine Tabelle nicht beantwortet.
matplotlib ist die grundlegende Python-Diagrammbibliothek: statisch und syntaktisch recht umständlich. plotly liefert interaktive Diagramme: am Punkt Datum und Wert anzeigen, einen Ausschnitt vergrößern, als HTML-Datei exportieren. Bei sehr großen Datensätzen kann es langsamer sein und braucht eine eigene Installation. Für Erkundungen im Notebook ist plotly bequemer, für in Berichte gespeicherte Diagramme matplotlib.
Vier Diagramme sollten Sie zeichnen können, denn jedes beantwortet eine andere Frage:
- Eigenkapitalkurve – kumuliertes Ergebnis über die Zeit. Beantwortet „ob und wann es wuchs“, nicht „wie viel ich verdienen werde“
- Drawdown-Diagramm – Abstand vom laufenden Höchststand zu jedem Zeitpunkt. Zeigt den tiefsten Drawdown und die Erholungsdauer. Geben Sie stets an, ob auf Kontostand oder laufendem Eigenkapital gerechnet wird und ob in Prozent oder Geld
- Sitzungs-Heatmap – Ergebnis nach Stunde und Wochentag. Die Zeitzone muss vorher geklärt sein, sonst beschreibt es Stunden, die nicht existieren
- Renditeverteilung – Histogramm der einzelnen Trade-Ergebnisse. Zeigt schwere Verteilungsränder und Asymmetrie, die eine einzige Risikokennzahl nicht zeigt
Für Kerzendiagramme direkt aus einem DataFrame gibt es mplfinance, eine Schicht über matplotlib. Es gilt eine Einschränkung aus Abschnitt 2: Die letzte Veröffentlichung stammt aus August 2023 und trägt weiterhin eine Vorabversionsnummer (0.12.10b0) [13] [STAND: 2026-10-08]. Das schließt die Bibliothek nicht vorab aus, doch bevor Sie sie aufnehmen in requirements.txt, prüfen Sie anhand eigener Daten die Zusammenarbeit mit Ihren matplotlib- und pandas-Versionen — eine neue Veröffentlichung erledigt das nicht für Sie. Für ein einfaches Dashboard mit Schiebereglern und Auswahllisten gibt es streamlit, ein regelmäßig veröffentlichtes Projekt [13], gestartet mit streamlit run dashboard.py. Beide Bibliotheken sparen Zeit, beide ergänzen Abhängigkeiten in requirements.txt, fügen Sie sie daher bewusst hinzu.
Praktische Regel: Können Sie nicht in einem Satz sagen, welche Frage ein Diagramm beantwortet, brauchen Sie es nicht. Ein Diagramm ohne Frage ist Dekoration; bei Ergebnisanalysen kann diese teuer werden, weil sie Schlüsse suggeriert, die die Daten nicht hergeben.
6. Backtesting in Python: Was zum Code gehört und was zur Methodik
Hier verläuft eine leicht übersehene Grenze. Python ist für die Implementierung des Tests zuständig: Daten laden, Signal berechnen, Ergebnis der richtigen Periode zuweisen, Kosten belasten und Kennzahlen berechnen. Nicht zuständig ist es für die Methodik: Stichprobenteilung, Variantenanzahl, Ausführungsmodell, Robustheitsbewertung. Methodik behandelt ein eigener Artikel — Demokonten, Backtesting und Forward-Testing — und keine bessere Bibliothek kann sie ersetzen.
pandas allein ist kein Backtest. Eine Signalspalte mit einer Renditespalte zu multiplizieren ergibt eine Ergebnisreihe, modelliert aber nichts zwischen Entscheidung und Ausführung: Einstiegszeit und -preis, Spread, Provision, Swap-Punkte, Teilausführungen oder Orderablehnungen. Zur ersten Ideenprüfung kann das vertretbar sein — sofern Sie es Vereinfachung nennen und nicht Ergebnis.
Über eine eigene Schleife hinaus bietet das Python-Ökosystem drei Ansätze fertiger Werkzeuge. Die Tabelle vergleicht ihre Arbeitsweise, nicht Leistungsversprechen. Konkrete Laufzeiten hängen von Daten, Hardware und Version ab; messen Sie sie bei Bedarf selbst und dokumentieren Sie die Umgebung.
| Merkmal | Vollständig ereignisgesteuert | Vektorisiert | Minimal ereignisgesteuert |
|---|---|---|---|
| Beispielbibliothek | backtrader | vectorbt | backtesting.py |
| Berechnungsweise | Kerze für Kerze, mit eigenem Order- und Portfoliomodell | Operationen auf ganzen Arrays zugleich | Kerze für Kerze, vereinfachtes Modell |
| Codeumfang für einfache Strategie | Hoch (Klassen, Vererbung) | Gering, wenn Sie in Spalten denken | Gering |
| Typische Verwendung | Portfoliologik, viele Instrumente, komplexe Orders | Massenprüfung von Varianten und erste Filterung | Lernen und schnelle Prüfung einer Idee |
| Wesentlicher Aufwand | Steile Lernkurve | Schwierigeres Debuggen; unbemerkter Verschiebungsfehler leicht möglich | Grenzen bei komplexen Strategien |
| Vor Verwendung prüfen | Datum der letzten PyPI-Veröffentlichung, Kompatibilität mit Ihren pandas-/NumPy-Versionen sowie Modellierung von Kosten und Ausführungszeitpunkt | ||
backtrader hat mehrjährige Veröffentlichungslücken hinter sich; die letzte Veröffentlichung stammt aus April 2023. Daneben existieren unterschiedlich gepflegte Community-Forks; vectorbt existiert parallel als offenes Projekt und als getrennt entwickelte kommerzielle Version (offene Veröffentlichung vom Juli 2026). Klären Sie zuerst, auf welche sich die gelesene Dokumentation bezieht; backtesting.py hatte eine mehrjährige Veröffentlichungspause, beendete diese aber und lieferte im Juli 2026 eine weitere Version. Die Vergangenheitsform ist hier wichtig. Alle drei Daten stammen aus dem Paketregister [13]. Nichts davon schließt eine Bibliothek vorab aus — ausschlaggebend ist Inkompatibilität mit Ihren verwendeten Versionen. [STAND: 2026-08-22]Die letzte Zeile ist keine Formalität. Jedes Framework nimmt einen Ausführungspreis und -zeitpunkt an; häufig entscheidet diese Annahme das Ergebnis, nicht Ihre Strategielogik. Bevor Sie einer Bibliothek vertrauen, suchen Sie in ihrer Dokumentation drei Antworten: zu welchem Preis sie einsteigt, wie sie Kosten belastet und wie sie eine Kurslücke zwischen Kerzen behandelt.
Die Freiheit des Codes ist zugleich ein Risiko. Ein Plattformtester erzwingt bestimmte Regeln und schützt dadurch vor einigen Fehlern. Eigener Code schützt vor nichts: Er fragt nicht nach getrennten Trainings- und Validierungsdaten, erkennt keinen im Entscheidungszeitpunkt noch nicht vorhandenen Signalpreis und zählt Ihre Varianten nicht. Die gesamte Verantwortung liegt beim Autor. Deshalb behandeln die nächsten Abschnitte genau das, was im Code unbemerkt schiefgehen kann.
7. Broker-APIs: Authentifizierung, Limits, Fehler, Testumgebung
„Der Broker stellt eine API bereit“ wird manchmal als „Mein Skript hat unbegrenzten Kontozugriff“ gelesen. So funktioniert es nicht. Eine API besteht aus Endpunkten mit Authentifizierung, Berechtigungen, Anfragengrenze und definierten Fehlercodes. Manchmal, nicht immer, gibt es getrennte Test- und Produktionsumgebungen; wie sie getrennt sind, muss die Dokumentation klären und darf nicht vorausgesetzt werden. Vor der ersten Codezeile sollten Sie neun Fragen beantworten: Wie funktioniert die Authentifizierung? Welche Rechte hat das Token? Wie viele Anfragen pro Sekunde sind erlaubt? Woran unterscheiden sich Test und Produktion? Welche Datentypen werden zurückgegeben? Welche Fehlercodes gibt es? Bestehen regionale Einschränkungen? Welche Regeln gelten für das Offenhalten der Verbindung? Darf das Token überhaupt Orders platzieren?
Limits sind konkret und veröffentlicht — doch Sie müssen prüfen, welches Limit gemeint ist. Diese Unterscheidung fehlt in vielen Anleitungen und entscheidet den Schleifenaufbau. Bei OANDA v20 stehen zwei Zahlen in zwei Dokumenten und bedeuten Verschiedenes. Die Entwickleranleitung nennt den durchgesetzten Wert: „120 Anfragen pro Sekunde. Weitere Anfragen erhalten HTTP 429“ für die REST-API, daneben zwanzig aktive Streams und zwei neue Verbindungen pro Sekunde — alles pro anfragender IP-Adresse [15]. Das Best-Practices-Dokument dagegen gibt eine Empfehlung: Der Anbieter rät zu höchstens hundert Anfragen pro Sekunde über eine bestehende Verbindung und zwei neuen Verbindungen pro Sekunde [5]. [STAND: 2026-08-22]
Hundert pro Sekunde ist also die empfohlene Grenze; oberhalb von hundertzwanzig antwortet der Server mit Fehler 429. Die Annahme „100/s ist das Maximum“ richtet keinen Schaden an. Wer diese Zahl aber für eine harte Grenze hält und 429 nicht behandelt, scheitert bei der ersten Überschreitung, denn durchgesetztes Limit und Empfehlung liegen auseinander. Das sind Zahlen eines Anbieters, keine Marktregel: Bei anderen Brokern sind sie anders und [FUNKTION ABHÄNGIG VON BROKER/KONTO]. Prüfen Sie sie in der Dokumentation Ihres Anbieters, bevor Sie eine Abfrageschleife schreiben.
Die Testumgebung hat einen anderen Host, nicht einen anderen Modus. Bei OANDA erkennt man die Trennung an der Adresse: api-fxpractice.oanda.com für die Übungsumgebung und api-fxtrade.oanda.com für die Produktion, mit einem entsprechenden Adresspaar für den Datenstrom [15] [STAND: 2026-08-22]. Das ist gut, denn der Fehler lässt sich im Code erkennen: Der Produktionshost kann mit einer zusätzlichen Bedingung abgeschirmt werden. Schlechter ist es bei einer einzigen Adresse mit Unterscheidung nur durch das Token — dann ist Ihre Konfiguration die einzige Barriere.
Vier typische Wege von Python zum Konto — verschiedene Architekturen, keine unterschiedlichen „Fortschrittsstufen“:
| Lösung | Tatsächlicher Verbindungsweg | Bedingungen | Hinweise |
|---|---|---|---|
| OANDA v20 (REST + Streaming) | Skript → Brokerserver, direkt über HTTP | Konto bei diesem Broker; getrennte Hosts für Übung und Produktion | Öffentliche Dokumentation mit ausdrücklich genannten Limits. Kein Branchenstandard, sondern die API eines Anbieters |
| Interactive Brokers (TWS / IB Gateway) | Skript → lokal laufende Vermittlungsanwendung → Brokerserver | Laufende Anwendung TWS oder IB Gateway erforderlich; asynchrones Modell | Das Schließen der Vermittlungsanwendung trennt die Skriptverbindung. Wrapper-Bibliotheken erleichtern die Bedienung, beseitigen aber diese Abhängigkeit nicht. Achten Sie auf den Namen: Das oft zitierte ib_insync hat seit Juli 2023 keine neue Veröffentlichung und beruht auf einer älteren TWS-API-Version; eine aktiv gepflegte Fortsetzung ist ib_async [13] [STAND: 2026-10-08] |
Das offizielle MetaTrader5 -Paket | Skript → installiertes MT5-Terminal auf demselben Rechner → Brokerserver | Das MT5-Terminal muss auf demselben Rechner installiert sein. Der initialize() -Aufruf startet es bei Bedarf und nutzt die im Terminal gespeicherten oder im Aufruf übergebenen Zugangsdaten [18]; die Integration ist seit Build 2085 (2019) verfügbar [7] | Das ist Kommunikation mit dem Terminal, keine eigenständige Broker-API [6]. Die Terminalabhängigkeit ist eine Eigenschaft, kein Defekt. Das offizielle PyPI-Paket lässt sich nur unter Windows installieren. macOS und Linux erfordern Terminal und Python unter Wine oder eine eigene Vermittlungsschicht. [FUNKTION ABHÄNGIG VON VERSION/UMGEBUNG] |
| Externe Cloud-Brücke (z. B. MetaApi) | Skript → externer Dienst → Terminal in der Anbietercloud → Brokerserver | Kontozugriff wird an Dritte übergeben; Preise und Limits tarifabhängig | Fügt ein Glied und eine Vertrauensstelle hinzu. Eine Brücke für Orders braucht das Hauptpasswort, nicht das Investorenpasswort — also Verfügungsrechte über das Konto statt bloßen Lesezugriff. Preise beim Anbieter prüfen [FUNKTION ABHÄNGIG VOM TARIF] |
Außerhalb des Devisenmarkts sollten Sie auch kennen: ccxt – eine Bibliothek für einheitlichen Zugriff auf Kryptobörsen. Sie ist nicht für FX-/CFD-Konten bei Privatkundenbrokern gedacht, wird aber manchmal irrtümlich als Universallösung angesehen.
cTrader. Nutzen Sie cTrader, schreiben Sie Algorithmen in cTrader Algo. Die Umgebung unterstützt nativ sowohl C# als auch Python, einschließlich cBots, Indikatoren und Plugins [8] [STAND: 2026-08-22]. Anders als oben läuft der Code innerhalb der Plattform, nicht als eigener API-Prozess. Prüfen Sie Funktionsumfang und Startverfahren in der aktuellen Anbieterdokumentation, da das Produkt aktiv weiterentwickelt wird.
Schlüssel und Passwörter: niemals im Code
Ein ins Skript geschriebener API-Schlüssel gelangt ins Repository, ins Backup und in die Versionshistorie; bloßes Löschen der Zeile entfernt ihn nicht aus Git. Öffentliche Repositorys werden automatisch nach solchen Zeichenfolgen durchsucht. Erlaubte Orte für Geheimnisse sind eine Umgebungsvariable, eine außerhalb des Repositorys gehaltene und hier ausgeschlossene Konfigurationsdatei: .gitignore oder eine Geheimnisverwaltung, die den Wert über einen dieser Wege liefert.
"""Konfiguration und Geheimnisse: nichts Sensibles in der Codedatei."""
import os
from dataclasses import dataclass
HOSTS = {
"practice": "https://api-fxpractice.oanda.com",
"live": "https://api-fxtrade.oanda.com",
}
class MissingConfiguration(RuntimeError):
"""Ausnahme bei fehlenden Daten, ohne die kein Start erlaubt ist."""
@dataclass(frozen=True)
class Configuration:
token: str
account: str
environment: str
@property
def host(self) -> str:
return HOSTS[self.environment]
def _required(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
# Bewusst kein Standardwert: Ein leeres Token würde die Anfrage
# ohne Autorisierung senden, statt das Programm zu stoppen.
raise MissingConfiguration(f"Fehlende Umgebungsvariable {name}")
return value
def load_config() -> Configuration:
environment = os.environ.get("BROKER_ENV", "practice").strip().lower()
if environment not in HOSTS:
raise MissingConfiguration(f"BROKER_ENV muss einem dieser Werte entsprechen: {sorted(HOSTS)}")
return Configuration(
token=_required("BROKER_API_TOKEN"),
account=_required("BROKER_ACCOUNT_ID"),
environment=environment,
)
Drei Entscheidungen sind bewusst: Standard ist Übung statt Produktion; ein Konfigurationsfehler führt so aufs Demo-, nicht aufs Echtgeldkonto. Eine fehlende Umgebungsvariable stoppt das Programm, statt eine leere Zeichenfolge einzusetzen. Und für das Token gibt es nirgends einen Standardwert, auch keinen auskommentierten — ein auskommentiertes Geheimnis ist offengelegt.
Das zuvor erwähnte python-dotenv ersetzt diesen Code nicht, sondern läuft davor. Der Aufruf load_dotenv() (nach from dotenv import load_dotenv) lädt eine .env -Datei außerhalb des Repositorys in die Prozessumgebungsvariablen. Erst danach liest load_config() sie ein. Die Konfigurationsfunktion weiß deshalb nicht und muss nicht wissen, woher ein Wert stammt: Datei, Systemeinstellungen oder Geheimnisverwaltung. Die Datei .env selbst gehört in .gitignore zusammen mit dem gesamten Konfigurationsverzeichnis.
Datenabruf: Zeitlimit, Wiederholungen, Antwortcodes
Eine Anfrage ohne Zeitlimit kann unbegrenzt hängen und den Prozess blockieren. Unbegrenzte Wiederholungen können das Anfragenlimit erschöpfen und zur Sperre führen. Das folgende Fragment zeigt Mindestschutz für Lesezugriffe sowie eine Unterscheidung, die später das Konto schützt: Wiederholen dürfen Sie nur idempotente Operationen, deren erneute Ausführung den Zustand beim Broker nicht ändert.
import logging
import time
import requests
TIMEOUT_S = 10.0 # Sekunden ohne Aktivität bei Verbindung/Lesen, keine Gesamtlaufzeitgrenze
MAX_ATTEMPTS = 4 # Versuchsanzahl einschließlich des ersten
BACKOFF_S = 1.0 # Basis der exponentiellen Verzögerung in Sekunden
log = logging.getLogger("data")
class ApiError(RuntimeError):
"""Fehler, der sich nicht durch Wiederholung beheben lässt."""
def fetch_candles(session: requests.Session, host: str, token: str,
instrument: str, parameters: dict) -> dict:
"""Wiederholt nur GET: ändert keinen Zustand beim Broker."""
url = f"{host}/v3/instruments/{instrument}/candles"
headers = {"Authorization": f"Bearer {token}"}
for attempt in range(1, MAX_ATTEMPTS + 1):
delay = BACKOFF_S * 2 ** (attempt - 1)
try:
response = session.get(url, headers=headers, params=parameters, timeout=TIMEOUT_S)
except requests.RequestException as error:
if attempt == MAX_ATTEMPTS:
raise ApiError(f"Keine Verbindung nach {MAX_ATTEMPTS} Versuchen") from error
log.warning("Versuch %s: %s", attempt, error)
time.sleep(delay)
continue
if response.status_code == 429:
# Anfragenlimit überschritten. Der Retry-After-Header ist, sofern geliefert,
# verlässlicher als eigenes Raten.
try:
wait = float(response.headers.get("Retry-After", delay))
except (TypeError, ValueError):
# Die Spezifikation erlaubt auch ein HTTP-Datum statt Sekunden;
# sonst würde float() den gesamten Datenabruf abbrechen.
wait = delay
log.warning("Anfragenlimit, warte %.1f s", wait)
time.sleep(wait)
continue
if 500 <= response.status_code < 600:
log.warning("Serverfehler %s, erneuter Versuch", response.status_code)
time.sleep(delay)
continue
if response.status_code != 200:
raise ApiError(f"HTTP {response.status_code}: {response.text[:200]}")
try:
data = response.json()
except ValueError as error:
raise ApiError("Ungültige JSON-Antwort") from error
if not isinstance(data, dict):
raise ApiError("JSON-Antwort muss ein Objekt sein")
if not isinstance(data.get("candles"), list) or not data["candles"]:
raise ApiError("Keine Kerzen in der Antwort: Instrument und Zeitraum prüfen")
return data
raise ApiError(f"Keine gültige Antwort nach {MAX_ATTEMPTS} Versuchen")
Prüfstatus: offline getestet, ohne Brokerverbindung. Die veröffentlichte Funktion fetch_candles() wurde mit einem HTTP-Sitzungs-Mock im Speicher statt eines echten Servers ausgeführt. Neun Szenarien decken 429 mit Retry-After in Sekunden und im selben Szenario anschließend 503 mit Erfolg, eine leere Kerzenliste, 404, einen HTTP-Datumsheader, erschöpfte Versuche, eine Verbindungsausnahme, ungültiges JSON, eine Antwort ohne Objekt und einen ungültigen Typ von candles ab. Die Aufrufe von sleep() wurden durch Aufzeichnung der angeforderten Verzögerung ersetzt. Der Test öffnet keine Sockets und misst keine Netzwerkzeiten. Umgebung: Python 3.12.10, requests 2.34.2, Windows 11, geprüft am 2026-10-08. Der tatsächliche Szenariobericht:
A: 429 Sekunden -> 503 -> 200: Aufrufe=3; Wartezeiten=[3.0, 2.0]; eine Kerze
B: leere Kerzenliste: Aufrufe=1; Wartezeiten=[]; erwarteter Fehler
C: HTTP 404: Aufrufe=1; Wartezeiten=[]; erwarteter Fehler
D: HTTP-Datum, Rückfall: Aufrufe=2; Wartezeiten=[1.0]; eine Kerze
E: Versuchslimit: Aufrufe=4; Wartezeiten=[0.0, 0.0, 0.0, 0.0]; erwarteter Fehler
F: Transportfehler: Aufrufe=4; Wartezeiten=[1.0, 2.0, 4.0]; erwarteter Fehler
G: ungültiges JSON: Aufrufe=1; Wartezeiten=[]; erwarteter Fehler
H: JSON kein Objekt: Aufrufe=1; Wartezeiten=[]; erwarteter Fehler
I: falscher Typ von candles: Aufrufe=1; Wartezeiten=[]; erwarteter Fehler
Was dieses Ergebnis bestätigt und was nicht. Es bestätigt das erwartete Verhalten in neun vorbereiteten Szenarien. A prüft, dass 429 die drei Sekunden des Servers statt einer Sekunde des Codes wählt. D prüft den Rückfall, wenn float() ein HTTP-Datum nicht verarbeiten kann. F prüft, dass timeout=10 übergeben wird, vier Versuche stattfinden und eine Ausnahme die simulierten Transportfehler beendet. Das legt keine Frist für eine echte Verbindung fest: Ein requests-Zeitlimit ist keine Gesamtlaufzeitgrenze, und hier wurde das Warten simuliert. B, C und G–I prüfen die Zurückweisung leerer oder ungültiger Antworten. API-Verfügbarkeit, jedes Datenfeld und Netzwerkverhalten eines konkreten Brokers werden nicht geprüft.
Über den Broker selbst bestätigt es nichts. Endpunktpfad, Parameternamen, Antwortstruktur und die Frage, ob der Anbieter Retry-After überhaupt liefert, sind Eigenschaften einer bestimmten API [FUNKTION ABHÄNGIG VON BROKER/KONTO] und müssen in deren Dokumentation geprüft und auf einem Übungskonto getestet werden. Der Mock belegt Ihren Code, nicht Ihre Annahmen über fremde Server. Darin liegt der gesamte Wert dieses Tests.
Latenz messen, nicht zitieren. Die Anleitungswerte „50–200 ms für diese API, 100–500 ms für jene“ sind wertlos ohne Standort, Verbindung, Tageszeit und Messmethode. Statt sie zu kopieren, trennen Sie vier Größen: Antwortzeit des Datenendpunkts, Zeit vom Ordersenden bis zur Annahmebestätigung, Zeit bis zur Ausführungsbestätigung und Marktdatenalter im Entscheidungszeitpunkt. Nur eine davon wird umgangssprachlich „Latenz“ genannt. Messen Sie jede in Ihrer eigenen Kette, protokollieren Sie sie und nennen Sie Datum, Broker und Versuchsanzahl dazu. Ein ICMP-Ping ist keiner dieser vier Werte.
Abfragen oder Datenstrom. Bei zyklischen REST-Abfragen hängt das Datenalter vom Abstand ab: Alle fünf Sekunden abzufragen kann Entscheidungen auf fünf Sekunden alten Kursen bedeuten; häufigere Abfragen verbrauchen Anfragenkontingent. Ein Stream — je nach API WebSocket oder HTTP-Streaming — kehrt das um: Der Server sendet Änderungen selbst. Das lohnt sich für Reaktionen innerhalb einer Kerze oder wenn Abfragen das Limit verbrauchen. Für Einstieg zur nächsten H1-Eröffnung reichen einfachere Abfragen. Ein Stream fügt dagegen drei Probleme hinzu. Erstens die Lücke nach Wiederverbindung: Nach dreißig Sekunden Abbruch spielt der Socket diese Zeit nicht nach. Der fehlende Abschnitt muss separat historisch abgerufen werden, bevor die Reihe vollständig heißen darf. Zweitens die Erkennung einer toten Verbindung: Ein Socket ohne Daten sieht im Code aus wie ein ruhiger Markt. Deshalb brauchen Sie ein Lebenszeichen (Heartbeat oder Keep-alive) und einen eigenen Stillezähler, der bei Überschreitung die Verbindung für unterbrochen erklärt und sie wiederherstellt. Drittens Reihenfolge und Duplikate: Nach Wiederaufnahme können bereits verarbeitete Nachrichten erneut eintreffen. Wie bei Orders lösen Ereigniskennung und Prüfung auf vorhandene Aufzeichnung das Problem.
Getrennte Limits für Daten und Orders. Eine ernsthafte API trennt Kurs- und Transaktionsstrom. Limits für Kontoänderungen liegen mitunter viel niedriger als Leselimits. Code, der in nervösen Märkten ein Dutzend Stopänderungen pro Sekunde versucht, kann gerade auf dem Transaktionspfad gesperrt werden. Die Position bleibt offen, und Sie haben vorübergehend keinen programmatischen Zugriff darauf. Begrenzen Sie deshalb selbst die Änderungsrate, statt auf Nachsicht des Brokers zu hoffen.
8. Vom Notebook zum Dienst: Wie sich Ausführungscode vom Skript unterscheidet
Hier liegt die eigentliche Grenze dieses Artikels. Ein Notebook zur Journalstatistik darf mittendrin abstürzen: Sie starten es neu. Code mit Kontorechten hinterlässt bei demselben Fehler Zustand — eine offene Position, eine teilweise geänderte Order, einen doppelten Einstieg. Der Unterschied ist nicht Codequalität, sondern dass einer davon unumkehrbare Folgen hat.
Drei Automatisierungsstufen – unterschieden durch die letzte Entscheidungsinstanz, nicht die Technik:
Stufe 1: Benachrichtigungen. Das Skript prüft Bedingungen und sendet eine Nachricht. Es hat keinen Kontozugriff und braucht kein Ordertoken. Entscheidung und Ausführung bleiben beim Menschen. Nur auf dieser Stufe kostet ein Codefehler höchstens Ärger.
Stufe 2: Vorbereitung mit Bestätigung. Das Skript baut die Order mit Instrument, Richtung und Größe und wartet auf ausdrückliche Bestätigung des Bedieners. Erst dann sendet es. So werden Rechenfehler vor der Position erkannt, und der Mensch bleibt in der Entscheidung. Für Privatkunden ohne Absicht zur Softwarepflege ist das meist das Ziel, keine Übergangsstufe — eine redaktionelle Einschätzung, kein Forschungsergebnis.
Stufe 3: Betrieb ohne Bestätigung. Das Skript eröffnet und schließt selbst. Erforderlich sind alle Punkte des Kastens, Tests erst auf Übungskonto, dann mit kleinster Position und Aufsicht im ersten Betriebsabschnitt. Wann genügend Trades vorliegen, ist eine Frage von Stichprobengröße und statistischer Teststärke, keiner runden Zahl; dazu gehört der Artikel über Tests.
Ausführungsschicht mit Sicherheitsfreigaben
Wer Ordercode zeigt, muss auch zeigen, was ihn stoppt. Das folgende Modul ist vollständig ausführbar und enthält keinen Netzwerkaufruf: Es baut eine Order, prüft Limits und stoppt an den Freigaben. Das Senden ist ein getrenntes Fragment — bewusst kein Code.
"""Ausführungsschicht: Validierung, Testmodus und explizite Echtgeldfreigabe."""
import logging
import os
import sys
import uuid
ALLOWED_INSTRUMENTS = {"EUR_USD"}
MAX_UNITS = 1_000 # Basiswährungseinheiten, keine Lots: Konvention je nach API
# Die Echtgeldbestätigung steht nicht in dieser Datei und ist hier nicht ablesbar.
# Das Programm liest sie aus einer Umgebungsvariable und akzeptiert die
# vom Bediener bewusst gewählte Kontonummer, vor dem Start von Hand
# in die Shell getippt. Den Schutzumfang erklärt der Kasten unter der Testausgabe.
CONFIRMATION_VARIABLE = "BROKER_LIVE_CONFIRMATION"
# Ohne Konfiguration entsteht kein log.info-Eintrag: Die Root-Standardstufe
# ist WARNING und ein Ziel fehlt. Die Datei bewahrt Spuren nach Fehlern,
# der Stream zeigt sie live. In größeren Programmen gehört die Konfiguration
# an den Programmeinstieg, nicht in ein Modul.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
handlers=[logging.FileHandler("execution.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout)],
)
log = logging.getLogger("execution")
class SafetyGateError(RuntimeError):
"""Stoppt die Order, bevor sie den Prozess verlässt."""
def new_order_id() -> str:
"""EINMAL je Orderabsicht aufgerufen und vor dem Senden gespeichert.
Derselbe Wert muss bei jedem weiteren Versuch derselben Order
zurückkehren, sonst lässt sich kein Duplikat erkennen.
"""
return str(uuid.uuid4())
def build_order(instrument: str, units: int, order_id: str) -> dict:
if instrument not in ALLOWED_INSTRUMENTS:
raise SafetyGateError(f"Instrument nicht erlaubt: {instrument}")
# bool ist eine int-Unterklasse, also ist isinstance(True, int) wahr. Ohne ausdrückliche
# Ablehnung würde ein Boolescher Wert bestehen und im Payload landen.
if isinstance(units, bool) or not isinstance(units, int) or units == 0:
raise SafetyGateError("Größe muss eine von null verschiedene Ganzzahl sein")
if abs(units) > MAX_UNITS:
raise SafetyGateError(
f"Größe {abs(units)} übersteigt Limit {MAX_UNITS}")
return {
"order": {
"type": "MARKET",
"instrument": instrument,
"units": str(units),
# Die Kennung kommt von außen und bleibt zwischen Versuchen gleich.
# Hier erzeugt hätte jeder neue Versuch einen anderen Wert.
# Feldname und Einschränkungen gehören zur konkreten API.
"clientExtensions": {"id": order_id},
}
}
def _operator_confirmation() -> str:
"""Liest externen Wert; der Code kennt und speichert ihn nirgends."""
return os.environ.get(CONFIRMATION_VARIABLE, "").strip()
def submit_order(session, cfg, instrument: str, units: int, order_id: str,
test_mode: bool = True) -> dict | None:
"""Sendet standardmäßig nichts. Der Produktionshost erfordert zwei Entscheidungen."""
order = build_order(instrument, units, order_id)
if test_mode:
log.info("TESTMODUS [%s] Order erstellt, nichts gesendet: %s",
cfg.environment, order)
return None
if cfg.environment == "live" and _operator_confirmation() != cfg.account:
raise SafetyGateError(
"Produktionshost erfordert ausdrückliche Bedienerbestätigung")
# Freigaben bestanden: Hier endet das Modul. Es sendet nichts,
# weil ehrliches Ausführen nicht möglich ist. Die Struktur folgt als Skizze.
raise NotImplementedError(
"Senden hier nicht implementiert: siehe Strukturskizze")
Die Orderkennung entsteht einmal, nicht bei jedem Versuch. Damit ein Duplikat nach Verbindungsabbruch erkennbar ist, muss der Wert beim Entschluss zur Orderaufgabe berechnet, vor dem Senden dauerhaft gespeichert und für jeden weiteren Versuch derselben Order wiederverwendet werden. Wäre uuid.uuid4() im Orderaufbau enthalten, hätte jede Wiederholung einen anderen Wert, und die zweite Anfrage wäre nicht als Wiederholung der ersten erkennbar. Deshalb nimmt build_order() die Kennung als Argument entgegen, statt sie selbst zu erzeugen.
Was diese Kennung nicht löst. Das Feld clientExtensions im Payload oben ist eine Anbieterlösung, kein Branchenstandard und keine dokumentierte Idempotenzgarantie. OANDA beschreibt es als optionale benutzerdefinierte Orderdaten und warnt gesondert: „tradeClientExtensions bei mit MT4 verbundenen Konten nicht setzen, ändern oder löschen“ [14] (geprüft am 2026-08-22). Eine wiederholte Market-Order mit gleicher Kennung wird nicht zugesichert als Duplikat abgelehnt; sie wird sofort ausgeführt, und das Erkennungsfenster ist minimal. Die Duplikaterkennung beruht deshalb auf zwei eigenen Maßnahmen: dauerhaftem Speichern der Orderabsicht vor dem Senden und Abgleich mit den nach einem Fehler vom Broker abgerufenen Orders und Transaktionen. Feldname und Einschränkungen in der eigenen API-Dokumentation prüfen — anderswo heißt es anders oder fehlt.
Ordersenden: eine Strukturskizze
Hier folgt keine kopierbare Funktion, sondern eine Strukturskizze: Schrittreihenfolge und Operationen, in eigenen Code übertragbar, aber nicht zum Einfügen und Starten. Diese Form ist bewusst gewählt. Die Redaktion führte diesen Zweig nicht aus und beabsichtigt das auch nicht: Ordersenden braucht ein Brokerkonto; Transaktionscode auf fremdem Konto ist kein Test, sondern fremdes Risiko. Eine vollständige Funktion wäre ungeprüfter Code und würde vor allem Vertrauen in Ungeprüftes lehren. Dieselbe Entscheidung fiel für die MQL-Skizzen in Art. 11.5 und die Pine-Script-Skizze in Art. 11.2.
STRUKTURSKIZZE - Order senden und fehlende Antwort behandeln
Orientierung für die HTTP-Schicht. Kein vollständiger, ausführbarer Code.
Deklaration: StanNieznany - Antwort blieb aus; ob die Order existiert, ist unbekannt.
Eigener Typ, keine Unterklasse eines Netzwerkfehlers: Einen Netzwerkfehler
darf man wiederholen, diesen Zustand niemals.
Eingabe: Order (erstellt und validiert), order_id, Konfiguration, HTTP-Sitzung
Ausgabe: Brokerantwort oder StanNieznany - niemals stillschweigender Erfolg
1. DAUERHAFTER SPEICHER - vor allem anderen
außerhalb des Prozessspeichers schreiben: order_id, UTC-Zeitstempel,
instrument, units, environment, status := INTENT
Schreiben auf Festplatte erzwingen, bevor es weitergeht
sonst geht die Orderabsicht bei Prozessneustart verloren,
und danach weiß das Programm nicht, was es bereits versucht hat
2. PROTOKOLL - ebenfalls vor dem Senden
protokollieren: order_id + environment + Orderinhalt
das Token kommt niemals ins Protokoll
Kontonummer bis auf die letzten vier Zeichen maskieren
das Protokoll liest später ein Mensch; den Speicher aus Schritt 1 liest das Programm
3. EINE ANFRAGE, KEINE WIEDERHOLUNG
POST an den Orderpfad laut eigener API-Dokumentation
Autorisierungsheader mit Token aus der Konfiguration, niemals aus dem Code
Zeitlimit zwingend - sonst kann der Aufruf unbegrenzt hängen
4. ANTWORT BLIEB AUS (Zeitüberschreitung oder Transportfehler)
NICHT wiederholen - POST ist nicht idempotent
status im Speicher := UNKNOWN
StanNieznany auslösen
ZUSTANDSABGLEICH: Orders und Transaktionen der letzten Minuten beim Broker
abfragen, darin order_id suchen und erst nach Klärung der Orderexistenz
eine neue Entscheidung treffen
(dieses Verfahren fehlt in der Skizze - siehe Absatz darunter)
5. ANTWORT EINGETROFFEN
protokollieren: HTTP-Code + gekürzter Antwortinhalt
Code außerhalb (200, 201) -> status := REJECTED -> Fehler auslösen
Inhalt kein JSON -> status := UNKNOWN -> StanNieznany auslösen
sonst -> status := ACCEPTED -> Antwort zurückgeben
Was diese Skizze nicht enthalten kann und auch nicht enthält:
keine fertige Funktion zum Einfügen und Ausführen auf einem Konto,
auch keinen Namen des Schutzlevel-Feldes; dieser stammt aus der
Dokumentation einer konkreten API, nicht aus einem fremden Beispiel.
Was die Skizze nicht leistet – und das ist genauso wichtig: Sie ergänzt weder Stop Loss noch Take Profit. Das ist keine Nachlässigkeit; APIs drücken Schutzwerte unterschiedlich aus — eigene Payload-Felder, Abstand oder absoluter Preis, eigene Gültigkeit. Der entsprechende Abschnitt gehört aus der Brokerdokumentation übernommen, nicht aus fremden Beispielen. Die praktische Folge betrifft Schritt 4: Nach einem StanNieznany kann eine ungeschützte offene Position bestehen, ohne dass ihre Existenz sicher ist. Bei sendeberechtigtem Code gehört das Schutzniveau in dieselbe Anfrage wie der Einstieg; sonst entsteht ein Zeitfenster mit offener Position ohne Stop.
Es fehlen nicht nur Schutzniveaus. Von der Liste „Forschungsnotebook ≠ Ausführungsprogramm“ haben weder das ausführbare Modul noch die Skizze bisher Überwachung, Not-Aus oder Bereitstellungsverfahren. Neustartfester Zustand steht in Schritt 1 nur als Anforderung, nicht als Implementierung. Der Abschnitt über Protokolle, Zustand und Tests bespricht diese Elemente, zeigt sie aber nicht als Code. Die Folgerung bleibt: Lernmaterial zur Architektur, kein Ausführungsprogramm.
Die Sicherheitsfreigaben wurden offline getestet – ohne Netzwerk, Broker oder Geheimnisse, mit simulierten Konfigurationsdaten. Hier steht der gesamte Test, nicht nur seine Ausgabe. Sonst müssten Sie uns glauben — genau davor warnt dieser Artikel.
"""Sperrentest ohne Netzwerk: Mock-Konfiguration, kein Geheimnis, kein POST."""
from dataclasses import dataclass
@dataclass(frozen=True)
class MockConfiguration:
environment: str = "practice"
host: str = "https://api-fxpractice.oanda.com"
token: str = "dummy-token-for-offline-test"
account: str = "000-000-0000000-000"
def test_gates() -> None:
cfg = MockConfiguration()
order_id = new_order_id()
print("Konfiguration:", cfg.environment, cfg.host, "token_length=", len(cfg.token))
# session=None ist bewusst: Würde dennoch ein POST versucht,
# scheiterte der Test mit AttributeError statt unbemerkt zu bestehen.
result = submit_order(None, cfg, "EUR_USD", 100, order_id)
assert result is None
print("1. Testmodus: nichts gesendet - OK")
for description, instrument, units in [
("Instrument nicht erlaubt", "GBP_JPY", 100),
("Größe über Limit", "EUR_USD", 5000),
("Größe null", "EUR_USD", 0),
("Boolescher Wert statt Zahl", "EUR_USD", True),
]:
try:
build_order(instrument, units, order_id)
except SafetyGateError as error:
print(f"2. Sperre [{description}]:", error)
else:
raise AssertionError(f"Erwartete Sperre nicht ausgelöst: {description}")
cfg_live = MockConfiguration(environment="live", host="https://api-fxtrade.oanda.com")
try:
submit_order(None, cfg_live, "EUR_USD", 100, order_id, test_mode=False)
except SafetyGateError as error:
print("3. Sperre [Echtgeld ohne Bestätigung]:", error)
else:
raise AssertionError("Echtgeldmodus ohne Bestätigung passiert")
print("alle Sperren bestanden")
if __name__ == "__main__":
test_gates()
Ausgabe unter Python 3.12.10 wie in Abschnitt 2, ohne Netzwerk und externe Pakete: Nach Entfernen des Sendens braucht das Modul kein requests. Die zweite Zeile ist ein Protokolleintrag: Der Inhalt geht zugleich in die Datei execution.log und auf die Standardausgabe. Zeitstempel und Orderkennung ändern sich bei jedem Lauf; alles andere bleibt zeichengenau reproduzierbar.
Konfiguration: practice https://api-fxpractice.oanda.com token_length= 28
1. Testmodus: nichts gesendet - OK
2. Sperre [Instrument nicht erlaubt]: Instrument nicht erlaubt: GBP_JPY
2. Sperre [Größe über Limit]: Größe 5000 übersteigt Limit 1000
2. Sperre [Größe null]: Größe muss eine von null verschiedene Ganzzahl sein
2. Sperre [Boolescher Wert statt Zahl]: Größe muss eine von null verschiedene Ganzzahl sein
3. Sperre [Echtgeld ohne Bestätigung]: Produktionshost erfordert ausdrückliche Bedienerbestätigung
alle Sperren bestanden
Was die Ausgabe bestätigt und was nicht. Fünf Negativfälle lösten die erwarteten Sperren aus: nicht erlaubtes Instrument, überhöhte Größe, null, Boolescher Wert und Echtgeldmodus ohne Bestätigung. Assertions prüfen auch den Testmodus; zusätzlich wurden Grenzen von ±1000, Überschreitungen und ungültige Typen geprüft. Das erste Aufrufargument ist None, und das Senden ist nicht implementiert; dieser Test kann also keine Order platzieren. Er prüft weder Broker-API noch Protokollrobustheit oder die Maskierung jedes sensiblen Feldes. Das Dummy-Token wird nicht ausgegeben, nur seine Länge. Prüfen Sie im eigenen Projekt Protokollschreiben und -wiederherstellung, Datenmaskierung und Festplattenfehler gesondert.
Über das Verhalten an einer echten API bestätigt der Test nichts und kann das auch nicht: Der Sendezweig existiert in diesem Modul nicht. Die Strukturskizze ersetzte ihn; damit entfiel die einzige Sendestelle. Das ist der Unterschied zwischen „nicht ausgeführt“ und „nichts Ausführbares“: Ersteres hinterlässt fertig kopierbaren Code, Letzteres eine selbst zu implementierende Struktur samt Pflicht, die eigene Brokerdokumentation zu lesen.
Der Test muss auch eine fehlende Sicherheitssperre erkennen. Deshalb prüft er das Testmodusergebnis per Assertion und löst nach jedem Negativfall AssertionError aus, wenn die erwartete Ausnahme fehlte. Die Abschlussmeldung erscheint erst nach bestandenen Prüfungen. Es bleibt ein kleiner Beispieltest ohne Brokerantwortformate, sämtliche Datentypen oder Zustandswiederherstellung. Übertragen Sie die Fälle bei größeren Projekten nach pytest, ergänzen Sie Positivfälle und starten Sie sie bei jeder Validatoränderung.
BROKER_LIVE_CONFIRMATION gelesen und mit der Kontonummer verglichen. Der Bediener muss die gewünschte Kontonummer bewusst in die Shell tippen. Das schützt vor drei echten Fehlern: Produktion „nur ausprobieren“, Kontoverwechslung und versehentlichem Abschalten des Testmodus. Mehr leistet es nicht. Code desselben Prozesses kann die Variable selbst setzen und die Sperre umgehen. Eine aus der eigenen Umgebung gelesene Bestätigung schützt nicht vor einem Programm. Echte Rechtebeschränkungen liegen außerhalb Ihres Codes: ein Token ohne Orderrecht, sofern der Broker das trennt, und kontoseitige Positionslimits. [FUNKTION ABHÄNGIG VON BROKER/KONTO].Einheiten sind keine Lots. Der Kommentar bei MAX_UNITS ist keine Dekoration. APIs beschreiben Positionsgröße in Basiswährungseinheiten, Lots oder Kontrakten. Ein Fehler erzeugt weder Syntaxfehler noch Ausnahme, sondern eine um zwei oder drei Größenordnungen zu große Exposition bei korrekt laufendem Code. Prüfen Sie die Konvention in der Dokumentation und bestätigen Sie sie mit einem minimalen Trade auf dem Übungskonto. Zwei Validatorgrenzen sollten Sie kennen: Eine in pandas oder NumPy berechnete Größe hat den Typ numpy.int64, keine Unterklasse des eingebauten int. Der Validator lehnt den ganzzahligen Wert mit einer Ganzzahlmeldung ab: sicher, aber verwirrend. Führen Sie aus dem DataFrame gelesene Zahlen deshalb durch int(...), bevor sie gelangen zu build_order(). Die zweite Grenze: order_id wird gar nicht geprüft, und eine Typannotation erzwingt nichts. Da Duplikaterkennung auf dieser Kennung beruht, prüfen und erzwingen Sie erlaubte Länge und Format laut API-Dokumentation genauso wie das Größenlimit.
Protokolle, Zustand und Tests
Protokoll statt print(). Das Modul logging liefert Schweregrade, Zeitstempel und Dateiausgabe, aber nichts aktiviert sich selbst. Ohne Konfiguration ist die Standardstufe WARNING und es fehlt ein Ausgabeziel; dadurch werden log.info(...) unbemerkt verworfen: Der Code wirkt protokollierend, zeichnet aber nichts auf. Deshalb ist das Journal nach Fehlern oft leer. Konfigurieren Sie einmal am Programmeinstieg, im gestarteten Skript, nicht im importierten Modul. Der Aufruf logging.basicConfig(...) steht oben nur deshalb im Ausführungsmodul, weil dieses zugleich das ganze Programm ist. Der Codekommentar sagt ausdrücklich, dass dies in größeren Projekten falsch ist: Beim Import ausgeführte Konfiguration übernimmt das Protokoll des gesamten Prozesses. Ein späteres logging.basicConfig(...) am echten Programmeinstieg bleibt dann wirkungslos, sobald der Root-Logger Ausgabeziele hat (außer mit force=True) — ohne Fehlermeldung [4]. Ihre Einstellungen verschwinden, Einträge gehen an das vom importierten Modul gewählte Ziel: derselbe stille Defekt von der anderen Seite. Protokollieren Sie Entscheidung samt Gründen, Anfrageinhalt und Serverantwort. Kürzen Sie Letztere, damit eine HTML-Fehlerseite das Journal nicht flutet; zur Diagnose erhöhen Sie die Grenze. Token niemals protokollieren. Ohne Brokerantwort lässt sich später nicht feststellen, ob Logik, Netzwerk oder Server schuld war.
execution.log, im vollständigen Programm auch die Brokerantwort: Kontonummer, Orderkennung, Größe und Preise. Das Token fehlt zu Recht — zehn Testläufe erzeugten elf Einträge ohne ein einziges Tokenvorkommen. Der Rest reicht jedoch aus, um die Datei wie eine geheime Konfiguration zu behandeln: Tragen Sie execution.log ein in .gitignore neben .env. Und .gitignore ist keine Zugriffskontrolle: Es beschränkt keine Dateirechte, löscht nichts nach einer Frist und schwärzt keine Inhalte. Deshalb fordert die Sendeskizze die Maskierung der Kontonummer bis auf die letzten vier Zeichen sowie eine selbst definierte Positivliste der Protokollfelder statt sämtlicher Serverdaten. Vor unbeaufsichtigtem Betrieb sind zwei weitere Dinge zu klären. Erstens der Pfad: execution.log ist relativ angegeben; die Datei entsteht im Arbeitsverzeichnis des Prozesses, nicht neben dem Skript. Bei Start über die Windows-Aufgabenplanung oder cron — genau so werden solche Skripte unten gestartet — kann dieses ganz woanders liegen. Das für die Fehlerbehebung entscheidende Journal liegt dann nicht am erwarteten Ort. Verwenden Sie einen absoluten Pfad aus Skriptstandort oder Konfiguration. Zweitens die Größe: Nach der obigen Empfehlung wächst die Datei unbegrenzt, weil das Beispiel keine Rotation hat. logging.handlers.RotatingFileHandler gehört zur selben Standardbibliothek und ersetzt nach Ergänzung von import logging.handlers— denn import logging allein macht das Untermodul nicht verfügbar — den FileHandler ohne sonstige Konfigurationsänderungen.Zustand muss einen Neustart überleben. Irgendwann wird der Prozess beendet: Systemupdate, Stromausfall oder Ihr eigenes Ctrl+C. Liegen Orderinformationen nur im Prozessspeicher, kennt das Programm nach Neustart seine früheren Handlungen nicht. Minimum ist die dauerhafte Speicherung der Orderkennungen vor dem Senden in Datei oder Datenbank und der Abgleich beim Start mit dem API-Kontostand. Genau das verhindert den doppelten Einstieg nach Verbindungsabbruch.
Tests ohne Netzwerk. Signallogik und Limitprüfung lassen sich auf vorbereiteten Daten offline testen, wie oben die Sicherheitssperren. Ein Test mit erforderlicher API ist kein Unit-Test und kann nicht bei jeder Änderung laufen. Beginnen Sie offline: Einstiegsbedingung, Positionsberechnung, leere Daten und Datenlücken.
Kontrolliertes Beenden. Ein Ausführungsprogramm sollte auf ein Beendigungssignal reagieren: laufenden Vorgang abschließen, Zustand speichern und beenden, statt mitten im Ordersenden unterbrochen zu werden. Eine separate Bedingung sollte neue Einstiege bei Überschreiten des Tagesverlustlimits stoppen. Grenze und Berechnung erklärt der Kursteil zum Risikomanagement; im Code ist das eine normale Prüfung vor jedem Einstieg, keine Bibliotheksfunktion.
cron unter Unix starten periodisch und beenden nach einem Lauf; ein Systemdienst (zum Beispiel eine systemd -Unit) hält den Prozess dauerhaft am Leben und kann ihn nach Beendigung neu starten. Ein Prozessmanager tut das auf Anwendungsebene. Die Wahl zwischen kurzem Lauf und Dauerprozess bestimmt Zustandsspeicherung und Verantwortung für eine einzige Instanz. Python-Code aus dem Zeitplaner muss damit rechnen, dass der vorige Lauf noch nicht beendet ist: Zustand vor Aktionen prüfen statt bei null anzufangen.Siehe auch: Algorithmischer Handel – Systemarchitektur und die Rolle der Automatisierung im gesamten Entscheidungsprozess.
9. Look-ahead und Datenleckage im Python-Code
Look-ahead ist kein Methodikfehler. Es ist ein Fehler in einer Codezeile — deshalb gehört er hierher statt in den Methodikartikel. Einem Zeitpunkt wird ein Ergebnis zugewiesen, das damals noch nicht existierende Informationen nutzt.
Die häufigste pandas-Form ist trivial: Signalspalte mal Renditespalte derselben Zeile. Die Rendite in Zeile t entstand zwischen vorherigem Kerzenschluss und Schluss von Kerze t. Ein Signal aus dem Schluss von Kerze t ist erst bekannt, nachdem diese Rendite bereits eingetreten ist. Durch Multiplikation schreiben Sie sich eine Bewegung gut, die Sie bei der Entscheidung nicht kennen konnten.
Annahmen des Beispiels: acht synthetische Stundenbeobachtungen, UTC-Index, Beispielwerte, keine Transaktionskosten — gezeigt wird die Verschiebungsmechanik, nicht die Regel bewertet. Umgebung wie Abschnitt 2: Python 3.12.10, pandas 3.0.5.
"""Look-ahead in einer Zeile: gleiche Regel, zwei Ergebnisse."""
import pandas as pd
index = pd.date_range("2026-06-01", periods=8, freq="1h", tz="UTC")
df = pd.DataFrame(
{"close": [1.0800, 1.0810, 1.0825, 1.0815, 1.0830, 1.0845, 1.0840, 1.0855]},
index=index,
)
df["sma"] = df["close"].rolling(3).mean()
df["return"] = df["close"].pct_change()
# Regel: long, wenn der Schlusskurs über dem Drei-Kerzen-Mittel liegt.
df["position"] = (df["close"] > df["sma"]).astype(int)
# FALSCH: Position aus Zeile t mal Rendite derselben Zeile.
# Rendite t entstand zwischen Schluss t-1 und Schluss t; die Entscheidung
# verwendet also einen beim Einstieg noch nicht existierenden Preis.
df["result_with_lookahead"] = df["position"] * df["return"]
# KORREKT: Position vom Schluss t wirkt erst auf Rendite t+1.
df["result_without_lookahead"] = df["position"].shift(1) * df["return"]
print(df[["result_with_lookahead", "result_without_lookahead"]].sum().round(6))
Ausgabe des Laufs:
result_with_lookahead 0.005082
result_without_lookahead 0.001384
dtype: float64
Interpretation. Dieselbe Regel, dieselben Daten, ein einziger shift(1) -Aufruf Unterschied: Die Summe einfacher Periodenrenditen fällt von 0,005082 auf 0,001384, also unter 30 Prozent des Ausgangswerts. Bei acht Beobachtungen ist das Illustration, keine Messung. Auf mehrjährigen Daten kann derselbe Fehler Durchschnittliches spektakulär und Spektakuläres unmöglich machen. Beide Werte sind einheitenlos und ohne Spread oder Provision; nach Kosten läge die korrekte Variante noch niedriger.
Eine Einschränkung zur Rechnung selbst. pct_change() liefert einfache Renditen, die wie in Abschnitt 4 multiplikativ verknüpft, nicht addiert werden; summierbar sind nur logarithmische Renditen. Hier ist Summieren eine bewusste Näherung: Bei Tausendstelwerten beträgt die Abweichung zur korrekten Verknüpfung weniger als 0,00001, über zwei Größenordnungen weniger als der gezeigte Unterschied. Für Kontoergebnisse statt Verschiebungsmechanik multiplizieren Sie die Faktoren (1 + r) oder nutzen logarithmische Renditen und summieren erst dann.
Vorsicht vor doppelter Verschiebung. Verschieben Sie an einer Stelle: Position oder Signaleingaben, nie beides. Wer erst aus verschobenen Kursen das Signal berechnet und dann die Position verschiebt, verzögert um zwei Perioden und unterschätzt unbemerkt das Ergebnis. Weniger gefährlich als Look-ahead, aber ebenso falsch.
Weitere Look-ahead-Formen im Python-Code – stets dieselbe Frage: War der Wert im Entscheidungszeitpunkt bekannt?
- Aktuelles Kerzenhoch oder -tief für einen Einstieg nutzen, obwohl es erst nach Schluss bekannt ist
- Skalierung oder Normalisierung vor der Trainings-/Validierungsteilung auf allen Daten; Parameter enthalten Zukunftswissen
- Indikatoren, die mit späteren Kerzen glätten oder Wendepunkte markieren; historische und live sichtbare Werte unterscheiden sich
- Revidierte Makrodaten statt ursprünglich veröffentlichter Werte
- Orderausführung zum Schlusskurs der signalauslösenden Kerze
Datenleckage bei der Datensatzaufbereitung. Der zweite Punkt verdient Klartext: In Python geschieht er reflexhaft. Ein Scaler auf dem gesamten DataFrame vor der Periodenteilung ist nur eine harmlos wirkende Zeile, überträgt aber Wissen über die künftige Verteilung in die Validierung. Transformationen ausschließlich auf früheren Daten bestimmen und auf spätere anwenden, niemals umgekehrt.
Multiples Testen. Je mehr Varianten auf denselben Daten, desto eher ist die beste Zufall. Unter einem expliziten Nullmodell mit 10.000 Tests und je 0,05 Falsch-positiv-Wahrscheinlichkeit sind 0,05 × 10.000 = 500 „signifikante“ Ergebnisse zu erwarten. Das folgt aus dem Summieren von Wahrscheinlichkeiten und verlangt keine Unabhängigkeit: Abhängigkeit ändert unter diesen Annahmen die Verteilung der Anzahl, nicht ihren Mittelwert. Deshalb muss die Versuchsanzahl neben dem Ergebnis stehen.
Eine formale Korrektur für die Versuchsanzahl existiert: die von Bailey und López de Prado 2014 vorgeschlagene Deflated Sharpe Ratio [12]. Prüfen Sie vor Verwendung einer fertigen Implementierung Status und Lizenz der Bibliothek; einige Projekte änderten ihr Vertriebsmodell. Die Formel steht in der Originalpublikation und lässt sich unabhängig implementieren.
Was dieser Abschnitt nicht behandelt. Überanpassung, Stichprobenauswahl, Walk-forward, Überlebensbias und Kostenrobustheit sind Testmethodik. Dazu gehört der Artikel über Backtesting und Forward-Testing. Hier geht es nur um Fehler und Prüfungen im Code.
Code prüfen, bevor Sie dem Ergebnis vertrauen:
- Nutzt das Ergebnis für Zeile t nur vor der Entscheidung verfügbare Daten?
- Genau einmal verschoben — Signal oder Daten, nicht beides?
- Sind Ausführungszeit und -preis ausdrücklich festgehalten?
- Sind Kosten enthalten oder bewusst und dokumentiert ausgelassen?
- Sind Trainings- und Validierungsdaten chronologisch getrennt?
- Transformationen ausschließlich auf dem früheren Teil bestimmt?
- Variantenanzahl zusammen mit dem Ergebnis dokumentiert?
- Zeitindex mit expliziter Zone, steigend und ohne Duplikate?
10. Wie viel Code brauchen Sie wirklich?
Sie müssen kein Entwickler werden. Sie brauchen genug Wissen, um weder eigenem Code noch fremden Repository-Skripten auf den Leim zu gehen.
| Stufe | Fähigkeiten | Nutzen | Ungefährer Zeitaufwand |
|---|---|---|---|
| 0 – kein Code | Keine | Sie analysieren nur, was die Plattform auf ihre Weise zulässt | — |
| 1 – Code lesen | Fremde Skripte verstehen, Parameter ändern, isoliert ausführen | Repository-Skripte auf Look-ahead und Kosten prüfen | Einige Wochen |
| 2 – eigene Analyse | pandas, numpy, matplotlib; eigene Analyseskripte | Journal- und Marktdatenanalyse ohne Tabellengrenzen | Einige Monate |
| 3 – Integration | APIs, Fehlerbehandlung, Protokollierung, Planung, Tests | Benachrichtigungen und Ordervorbereitung mit Bestätigung | Sechs Monate und mehr |
| 4 – Ausführungscode | Automatisierte Tests, Zustandsverwaltung, Bereitstellung, Überwachung | Programm ohne Bestätigung jeder Entscheidung | Jahre praktischer Erfahrung |
Die Zeitangaben sind grob und hängen von Vorwissen und tatsächlichen Wochenstunden ab — Größenordnung, kein Versprechen.
Stufen 1 und 2 verschaffen keinen Marktvorteil. Sie erlauben aber, fremde Lösungen zu beurteilen und keine unverifizierbaren Systeme mehr zu kaufen. Das ist echter Nutzen, nur anderer als von Fertigwerkzeugen versprochen. Stufe 4 braucht, wer ein Ausführungsprogramm pflegen will, nicht wer besser handeln will. Hilpischs Lehrbuch gibt einen Überblick zu Python im Finanzbereich [11], mit derselben Einschränkung wie jedes Codebuch: Vor Verwendung Beispiele mit aktuellen Bibliotheksversionen prüfen, da Schnittstellen seit Veröffentlichung verändert wurden.
Sinnvolle Lernreihenfolge – jeder Schritt baut auf dem vorigen auf und endet mit etwas Funktionierendem:
- Sprachgrundlagen – Variablen, Listen, Funktionen, Bedingungen. Genug zum Lesen fremder Skripte ohne Raten
- pandas auf eigenen Daten – eigenen Journalexport laden, Statistiken nach Wochentag und Instrument berechnen. Wer Lehrbücher bevorzugt, findet diese Stufe vollständig bei pandas-Autor McKinney [10]
- Ein Diagramm – aus den vorigen Berechnungen Eigenkapital- und Drawdown-Kurve zeichnen
- Datenqualität prüfen – Abschnitt 4 auf eigener Datei wiederholen: Zeitzone, Duplikate, Lücken
- Reine Leseintegration – API-Daten vom Übungskonto mit Token ohne Transaktionsrechte abrufen, sofern der Anbieter das unterscheidet
Die Reihenfolge ist bewusst: Schritt fünf erst, wenn Sie Daten prüfen können. Sonst automatisieren Sie Fehler statt Arbeit.
Beginnen Sie mit einer Aufgabe: eigene Daten laden, etwas auf drei Zeilen von Hand Nachprüfbares berechnen und vergleichen. Stimmt Python mit dem Zählen an den Fingern überein, steht das Fundament. Sonst haben Sie den ersten Fehler gefunden — besser als ein sofort funktionierendes, unverstandenes Skript.
Python repariert keine schlechte Strategie. Bestenfalls zeigt es schneller, dass sie schlecht war.
FAQ – Python im Trading
Ist Python schnell genug für Trading-Automatisierung?
Wie lange dauert es, Python für den Handel sinnvoll zu lernen?
Kann ich Python mit einem MT4- oder MT5-Konto verbinden?
MetaTrader5 -Paket via pip, jedoch nur für Windows. Unter macOS und Linux fehlt eine offizielle Installation: Terminal und Python müssen unter Wine oder über eine gesonderte Vermittlungsschicht laufen. [FUNKTION ABHÄNGIG VON VERSION/UMGEBUNG]. Das Paket kommuniziert mit einem MetaTrader-5-Terminal auf demselben Rechner, nicht direkt mit dem Brokerserver. Das Terminal muss installiert, aber nicht vorab gestartet sein. Laut Anbieterdokumentation startet der initialize() -Aufruf es bei Bedarf selbst. Anmeldeparameter sind optional; fehlen sie, nutzt das Paket im Terminal gespeicherte Daten. [18]. Das ist keine Kleinigkeit: Ein zeitgesteuertes Skript kann Terminal und Anmeldung ohne Ihr Zutun starten. Das ist praktisch, heißt aber, dass „das Terminal war geschlossen“ kein Schutz ist. Die Integration kam 2019 mit Build 2085 [7]. Dies ist keine brokerunabhängige REST-API. Eine externe Cloud-Brücke ist eine Alternative mit zusätzlichem Glied und Kontozugriff für Dritte. Der einfachste, sicherste Einstieg bleibt einseitig: Terminaldaten exportieren, in Python analysieren, Mensch entscheidet und führt aus.Wo API-Schlüssel speichern, und was tun bei einem Leck?
.gitignore. Nie im Code, Kommentar oder Notebook — Notebookzellen speichern auch Ausgaben, ein ausgegebenes Token bleibt in der Datei. Gelangt ein Schlüssel ins Repository, hilft Zeilenlöschen nicht: Er bleibt in der Historie. Wirksam ist nur Widerruf beim Broker und Neuerzeugung, danach Bereinigung der Historie. Unabhängig davon sind kontoseitige Größenlimits eine zweite, vom Code unabhängige Schutzschicht.Wurde der Code dieses Artikels geprüft?
127.0.0.1 und auch keine zu einem Broker. Geprüft wurden die dokumentierten Szenarien der Antwortbehandlung, darunter das Ausschöpfen der Versuchsgrenze und eine Verbindungsausnahme. Der Artikel enthält jedoch keine einzige Zeile, die eine Order senden würde: Der Sendezweig wurde durch eine Strukturskizze ersetzt, weil Transaktionscode nicht auf fremden Konten getestet wird und eine fertige Funktion ohne Test nur Vertrauen in ungeprüften Code lehren würde. Codeausführung bestätigt keine Annahmen über fremde Server: Prüfen Sie Endpunktpfade, Antwortstrukturen und Limits in der Dokumentation Ihres Brokers und auf einem Übungskonto.Kann ich einer Strategie aus einem öffentlichen Repository vertrauen?
Quellen und Literatur
- pandas 3.0.5 – Dokumentation – pandas.pydata.org/docs – Datenstrukturen, Zeitindex und Zeitzonen,
resample, Umgang mit fehlenden Werten; Version geprüft am 2026-08-22 - pandas – Versionshinweise für 2.2.0 und 3.0.0 – pandas.pydata.org/docs/whatsnew – Veraltung des Parameters
methodinfillna()und Änderungen des Standardverhaltens - NumPy – Dokumentation und Versionshinweise – numpy.org/doc – Vektorisierung, Zufallszahlengeneratoren und Startwerte
- Python-Dokumentation – docs.python.org/3 – die Module
venv,loggingundtimeitsowie Ausnahmebehandlung - OANDA v20 – bewährte Verfahren – developer.oanda.com/rest-live-v20/best-practices – die empfohlene Anfragerate auf bestehender Verbindung (100/s) und neue Verbindungen (2/s); geprüft am 2026-08-22
- MQL5 – Python-Integration – mql5.com/en/docs/python_metatrader5 – das Paket
MetaTrader5und seine Kommunikation mit dem Terminal - MetaTrader 5 – Versionshinweise für Build 2085 – metatrader5.com/en/releasenotes/terminal/2085 – Einführung der Python-Integration des Terminals
- cTrader Algo – Dokumentation – help.ctrader.com/ctrader-algo – native C#-/Python-Unterstützung für cBots, Indikatoren und Plugins; Stand 2026-08-22
- TA-Lib (Python-Anbindung) – pypi.org/project/TA-Lib – Binärpakete auf PyPI, Installation ohne Kompilierung der C-Bibliothek
- McKinney, W. – Python for Data Analysis, O'Reilly, 3. Auflage, 2022 – pandas, numpy, Zeitreihen
- Hilpisch, Y. – Python for Finance, O'Reilly, 2. Auflage, 2018 – Python im Finanzbereich; Code mit aktuellen Bibliotheken prüfen
- Bailey, D. H., López de Prado, M. – The Deflated Sharpe Ratio, Journal of Portfolio Management, 2014 – Sharpe-Korrektur für die getestete Variantenanzahl
- PyPI – Paketseiten und JSON-Schnittstelle des Registers – pypi.org – Daten und Nummern der letzten Veröffentlichungen genannter Bibliotheken (
mplfinance,streamlit,openpyxl,pandas-ta,backtrader,vectorbt,backtesting.py,ib_insync,ib_async); geprüft am 2026-10-08 - OANDA v20 – Order- und Transaktions-API – developer.oanda.com/rest-live-v20/transaction-df –
clientExtensionsals optionale benutzerdefinierte Orderdaten; Warnung vor Verwendung bei MT4-Konten, keine dokumentierte Idempotenzgarantie für Market-Orders; geprüft am 2026-08-22 - OANDA v20 – Entwickleranleitung – developer.oanda.com/rest-live-v20/development-guide – die durchgesetzte REST-Grenze von 120 Anfragen pro Sekunde mit HTTP 429 sowie 20 aktive Streams und 2 neue Verbindungen pro Sekunde, jeweils je anfragender IP; geprüft am 2026-08-22
- Microsoft – Excel-Spezifikationen und -Grenzen – support.microsoft.com – Arbeitsblatt: 1.048.576 Zeilen und 16.384 Spalten; geprüft am 2026-08-22
- Schweizerische Nationalbank – Mitteilung vom 15. Januar 2015 – snb.ch – Aufhebung des Mindestkurses von 1,20 Franken pro Euro und Zinssenkung auf −0,75 Prozent
- MQL5 –
MetaTrader5.initialize()– mql5.com/en/docs/python_metatrader5/mt5initialize_py – bedarfsweises Starten des Terminals beim Verbindungsaufbau und optionale Parameterpath,login,passwordundservermit im Terminal gespeicherten Daten; geprüft am 2026-08-22