English Русский Español 日本語 Português
preview
Ermittlung fairer Wechselkurse anhand von KKP- und IWF-Daten

Ermittlung fairer Wechselkurse anhand von KKP- und IWF-Daten

MetaTrader 5Integration |
14 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Irgendwann wurde mir klar, dass ich mehr Zeit damit verbrachte, nach dem „perfekten“ Prognosemodul zu suchen, als zu verstehen, was die Wechselkurse tatsächlich beeinflusst. Also stellte ich mir eine einfache Frage: Was wäre, wenn ich all diese Charts beiseiteließe und versuchte, den wahren Wert einer Währung zu ermitteln? Nicht der Wert, den der Markt in einem bestimmten Moment unter dem Einfluss von Emotionen und Spekulation gerade anzeigt, sondern derjenige, der sich aus den grundlegenden wirtschaftlichen Gesetzen ergibt?

Diese Frage war der Ausgangspunkt für eine mehrmonatige Arbeit. Ich begann mit dem Studium der Kaufkraftparitätstheorie und entwickelte schließlich ein umfassendes System zur Wechselkursanalyse in Python. Das erwies sich als viel interessanter als jeder technische Indikator.


Definition des Problems

Bei der Analyse des Devisenmarktes liegt der Fokus oft auf der Suche nach technischen Indikatoren und Chartmustern, während fundamentale wirtschaftliche Faktoren außer Acht gelassen werden. Da stellt sich die logische Frage: Lässt sich der faire Wert von Währungen anhand wirtschaftlicher Gesetzmäßigkeiten bestimmen, anstatt anhand kurzfristiger Marktstimmungen und spekulativer Bewegungen?

Diese Studie stellt einen praktischen Ansatz zur Lösung dieses Problems vor, indem ein umfassendes System zur Berechnung fairer Wechselkurse auf der Grundlage der Kaufkraftparitätstheorie entwickelt wird, das mit der Programmiersprache Python umgesetzt wurde.

Theoretische Grundlagen der Kaufkraftparität
Das klassische Beispiel des Big Mac von McDonald’s verdeutlicht den Kern der Theorie: Wenn ein Hamburger in den USA 5 US-Dollar und in Europa 9 Euro kostet, sollte der faire Wechselkurs bei 1,8 US-Dollar pro Euro liegen. Hinter dieser einfachen Darstellung verbirgt sich ein grundlegendes Instrument zur Analyse der Devisenmärkte, mit dem sich langfristige Ungleichgewichte jenseits der kurzfristigen Marktvolatilität erkennen lassen.

Die historischen Wurzeln dieses Konzepts lassen sich bis ins 16. Jahrhundert zurückverfolgen, als spanische Händler Preisunterschiede zwischen dem Mutterland und den Kolonien feststellten. Wenn man in Sevilla für einen Peso einen Laib Brot kaufen konnte, in Mexiko jedoch drei, deutete dies auf ein grundlegendes Ungleichgewicht im relativen Wert der Währungen hin, das durch eine Änderung des Wechselkurses oder einen Preisausgleich korrigiert werden musste.

KKP-Formulierungen
Die absolute Kaufkraftparität (KKP) besagt, dass ein direkter Zusammenhang zwischen den Wechselkursen und dem Preisverhältnis identischer Güter in verschiedenen Ländern besteht. Dieser Ansatz ist zwar theoretisch elegant, stößt in der Praxis jedoch auf zahlreiche verzerrende Faktoren: steuerliche Unterschiede, regulatorische Hindernisse, Transportkosten und Handelsbeschränkungen.

S₁₂ = P₁ / P₂

wobei S₁₂ der Wechselkurs von Währung 1 zu Währung 2 ist und P₁ und P₂ die Preisniveaus in den jeweiligen Ländern darstellen.

Die relative Kaufkraftparität konzentriert sich eher auf die Dynamik von Preisänderungen als auf das absolute Preisniveau. Nach dieser Formel sollte die Währung von Land A gegenüber der Währung von Land B um 5 % abwerten, wenn die Inflation in Land A bei 10 % pro Jahr und in Land B bei 5 % liegt, um die Kaufkraftparität aufrechtzuerhalten.

S₁₂(t) / S₁₂(0) = [P₁(t) / P₁(0)] / [P₂(t) / P₂(0)]

Oder vereinfacht ausgedrückt:

ΔS₁₂ = π₁ - π₂

wobei π₁ und π₂ die Inflationsraten in den Ländern 1 und 2 sind.

Der relative Ansatz wurde aufgrund seiner größeren praktischen Anwendbarkeit, seiner besseren Abbildung der wirtschaftlichen Realität und der Möglichkeit, konkrete quantitative Referenzwerte für Wechselkurse zu erhalten, als methodische Grundlage des Algorithmus gewählt.


Auswertung der verfügbaren Datenquellen

Eine Untersuchung bestehender Anbieter von KKP-Daten ergab erhebliche Einschränkungen hinsichtlich der praktischen Anwendbarkeit. Die OECD veröffentlicht offizielle Kaufkraftparitätskurse mit einer Verzögerung von sechs Monaten bis zu einem Jahr. Die Daten für das Jahr 2023 lagen erst Mitte 2024 vor, was für dynamische Devisenmärkte inakzeptabel ist.

Datenquelle Aktualisierungsfrequenz
Verzögerung
Kosten pro Jahr
Transparenz der Methodik
Praktische Anwendbarkeit
OECD
Jährlich
6–12 Monate
0 USD
Niedrig
Forschung
Penn World Table
2–3 Jahre
1–2 Jahre
0 USD
Durchschnittlich
Wissenschaft
Bloomberg-Terminal
Echtzeit
Keine
24.000 US-DOLLAR
Keine
Professioneller Einsatz
Refinitiv Eikon
Echtzeit
Keine
22.000 US-DOLLAR
Keine
Professioneller Einsatz

Die „Penn World Table“ wird in Abständen von mehreren Jahren aktualisiert; die aktuellste verfügbare Version enthält Daten nur bis zum Jahr 2019. Auch wenn eine solche Datenbasis für die akademische Forschung wertvoll ist, sind solche Zeitverzögerungen für den praktischen Handel kritisch.

Die kommerziellen Datenanbieter Bloomberg und Refinitiv bieten aktuellere Informationen, doch die Kosten für den Zugang zum Bloomberg-Terminal beginnen bei 2.000 US-Dollar pro Monat, ohne dass dabei methodische Transparenz oder Datenqualität garantiert werden.

Das Problem der methodischen Undurchsichtigkeit: Ein entscheidender Nachteil bestehender Lösungen ist die mangelnde Transparenz der Berechnungsverfahren. Die Methodik zur Berechnung der OECD-Koeffizienten, die Zusammensetzung der Warenkörbe, die Algorithmen zur Behandlung statistischer Ausreißer sowie die Anpassung an qualitative Unterschiede bei den Waren sind nach wie vor nicht dokumentiert.

Große Datenanbieter arbeiten als geschlossene Systeme: Der Nutzer erhält numerische Ergebnisse, ohne deren Herkunft zu kennen oder die Möglichkeit zu haben, diese zu überprüfen. Dieser Ansatz ist für eine fundierte Finanzanalyse, bei der volle Kontrolle über die Berechnungen erforderlich ist.

Entwicklung eines eigenen Systems: Es wurde beschlossen, ein unabhängiges System zur Berechnung der Kaufkraftparität (KKP) zu entwickeln, das auf öffentlich zugänglichen Daten internationaler Organisationen – des Internationalen Währungsfonds, der Weltbank und nationaler Statistikämter – basiert. Das System muss vollständige Transparenz hinsichtlich der Algorithmen gewährleisten und ermöglichen, die Methodik an spezifische Analyseaufgaben anzupassen.

Zwar ist die Entwicklung wesentlich komplexer als der Kauf einer vorgefertigten Lösung und erfordert die Einarbeitung in verschiedene Datenquellen, API-Formate und statistische Methoden, doch garantiert dieser Ansatz die vollständige Kontrolle über den Berechnungsprozess und ein tiefgreifendes Verständnis der Funktionsweise des Systems.


Architektonische Lösung: Ein System mit mehreren Methoden

Das grundlegende Problem bei jeder KKP-Berechnung besteht darin, dass es mehrere Ansätze zur Schätzung eines fairen Wechselkurses gibt, von denen jeder seine eigenen spezifischen Vor- und Nachteile hat. Die Wahl einer einzigen Methode führt zwangsläufig zu subjektiven Ergebnissen.

Datenquellen Methoden zur Berechnung des KKP Ergebnisse und Signale
IWF-API Preisniveau
Faire Wechselkurse
Weltbank aus dem BIP abgeleitet
Abweichungen vom Markt
Nationale Statistiken inflationsbereinigt
Handelssignale
Ersatzdaten Big-Mac-Proxy
Konfidenzindex
Marktkurse von Währungspaaren Kombinierte Methode
Währungsklassifizierung

Das entwickelte System nutzt mehrere unabhängige Berechnungsmethoden parallel und führt anschließend eine Zusammenführung der Ergebnisse durch. Übereinstimmungen zwischen Schätzungen, die mit unterschiedlichen Methoden ermittelt wurden, stärken die Verlässlichkeit der Schätzung, während erhebliche Abweichungen auf ein hohes Maß an Unsicherheit hindeuten, was wiederum wertvolle Informationen über die Marktlage liefert.

Der erste Ansatz basiert auf internationalen Preisvergleichen. Die Logik ist einfach: Wenn ein Schweizer bei gleichem Lebensstandard eineinhalbmal so viel für seinen Lebensunterhalt ausgibt wie ein Amerikaner, dann ist der Schweizer Franken um 50 % überbewertet.

Die Daten für diese Methode habe ich internationalen Preisvergleichsprogrammen entnommen, die unter der Schirmherrschaft der Weltbank und der OECD durchgeführt wurden. Diese Programme vergleichen die Preise von Standard-Warenkörben in verschiedenen Ländern und berechnen das relative Preisniveau.

def _calculate_price_level_ppp(self) -> Dict:
    ppp_rates = {}
    for country, price_level in self.price_levels_2024.items():
        if country != 'US':
            ppp_factor = price_level / 100.0
            ppp_rates[country] = {
                'ppp_conversion_factor': ppp_factor,
                'price_level_index': price_level,
                'method': 'price_level_adjustment'
            }
    return ppp_rates

Der Vorteil dieser Methode besteht darin, dass sie die tatsächlichen Unterschiede bei den Lebenshaltungskosten widerspiegelt. Der Nachteil ist, dass die Daten nur selten aktualisiert werden, nämlich nur alle paar Jahre.

Der zweite Ansatz ist der, den ich mir selbst ausgedacht habe, und auf den ich immer noch stolz bin. Die Idee kam mir, als ich Daten des IWF studierte: Was wäre, wenn wir das BIP eines Landes in Landeswährung (aus offiziellen Statistiken) mit einer Schätzung desselben BIP in US-Dollar (aus internationalen Datenbanken) vergleichen würden?

Die Logik dahinter ist folgende: Wenn die Bank von Russland angibt, dass das russische BIP 150 Billionen Rubel beträgt, und die Weltbank das russische BIP auf 2 Billionen US-Dollar schätzt, dann beträgt der implizite Wechselkurs 75 Rubel pro US-Dollar. Dies ist der Kurs, zu dem Volkswirtschaften im Verhältnis zueinander „fair“ bewertet werden.

def _calculate_gdp_implied_ppp(self, economic_data: pd.DataFrame):
    for country, gdp_usd_2023 in self.gdp_usd_estimates_2023.items():
        country_gdp_lcu = gdp_lcu_data[gdp_lcu_data['REF_AREA'] == country]
        if not country_gdp_lcu.empty:
            latest_data = country_gdp_lcu.sort_values('year').iloc[-1]
            gdp_lcu = latest_data['value']
            
            if gdp_lcu > 0:
                implied_rate = gdp_lcu / gdp_usd_2023

Diese Methode hat sich als überraschend genau und aussagekräftig erwiesen. Die BIP-Daten werden vierteljährlich aktualisiert, sodass die Schätzungen auf dem neuesten Stand sind. Und da das BIP ein Gesamtmaß für die gesamte Wirtschaftstätigkeit darstellt, spiegelt es den fundamentalen Wert einer Währung gut wider.

Die dritte Methode wendet die klassische Formel für die relative Kaufkraftparität aus den Lehrbüchern an. Wir nehmen den historischen Wechselkurs ab einem bestimmten Zeitpunkt und bereinigen ihn um die kumulierte Inflationsdifferenz zwischen den Ländern.

Ich wählte 2020 als Basisjahr: aktuell genug, um relevant zu sein, und noch vor den stärksten pandemiebedingten geldpolitischen Verzerrungen, um extreme geldpolitische Verzerrungen möglichst zu begrenzen.

def _calculate_inflation_adjusted_ppp(self, economic_data: pd.DataFrame):
    # Get the basic rate for 2020
    base_rate_2020 = GetBaseRate2020(base_currency, quote_currency)
    
    # Calculating the inflation differential
    inflation_differential = (base_data.inflation_rate - quote_data.inflation_rate) / 100.0
    
    # Adjust the base rate
    adjusted_rate = base_rate_2020 * (1 + inflation_differential)

Die Methode ist theoretisch einwandfrei und leicht zu erklären. Wenn die Inflation in Land A höher war als in Land B, dann sollte die Währung von A proportional zu diesem Unterschied an Wert verlieren.

Das einzige Problem ist die Qualität der Inflationsdaten. Verschiedene Länder berechnen die Inflationsrate auf unterschiedliche Weise, und manche neigen dazu, die Statistiken zu „verschönern“. Insgesamt funktioniert die Methode jedoch gut.

Die vierte Methode ist vom berühmten Big-Mac-Index des „Economist“ inspiriert. Der Gedanke dahinter ist, dass der Big Mac ein standardisiertes Produkt ist, das weltweit nach dem gleichen Verfahren hergestellt wird. Sein Preis sollte die tatsächlichen Unterschiede bei den Ressourcenkosten zwischen den Ländern widerspiegeln.

Das Problem ist, dass das Erfassen der aktuellen Big-Mac-Preise eine eigene Forschungsaufgabe darstellt. Stattdessen nutze ich bekannte Daten über das relative Preisniveau, um zu modellieren, wie viel ein standardisiertes Gut in jedem Land kosten sollte.

def _calculate_big_mac_proxy_ppp(self):
    us_big_mac_price = 5.50
    for country, price_level in self.price_levels_2024.items():
        if country != 'US':
            local_big_mac_price = us_big_mac_price * (price_level / 100.0)
            ppp_rate = local_big_mac_price / us_big_mac_price

Die Methode ist zwar nicht die genaueste, bietet aber eine gute intuitive Überprüfung der Ergebnisse anderer Methoden. Wenn alle anderen Methoden darauf hindeuten, dass die Währung um 20 % überbewertet ist, und die „Big-Mac-Methode“ zu einem ähnlichen Ergebnis kommt, stärkt dies das Vertrauen in die Bewertung.

Die fünfte Methode kombiniert die Ergebnisse aller vorherigen Methoden mittels gewichteter Mittelwertbildung. Ich habe viel Zeit damit verbracht, die Gewichtung zu optimieren und verschiedene Kombinationen anhand historischer Daten zu testen.

Letztendlich habe ich mich für folgende Aufteilung entschieden:

  • Preisniveau (KKP): 30 % (am grundlegendsten)
  • Aus dem BIP abgeleitete Kaufkraftparität: 25 % (am relevantesten)
  • Inflationsbereinigte Kaufkraftparität (KKP): 25 % (theoretisch am stichhaltigsten)
  • Big-Mac-Proxy: 20 % (am intuitivsten)
def _calculate_composite_ppp(self, all_methods: Dict):
    weights = [0.30, 0.25, 0.25, 0.20]
    
    # Dynamic weight normalization
    if valid_methods < 4:
        total_weight = sum(weights[:valid_methods])
        normalized_weights = [w / total_weight for w in weights[:valid_methods]]
    
    composite_rate = sum(rate * weight for rate, weight in zip(rates, normalized_weights))

Das Hauptmerkmal ist die dynamische Normalisierung der Gewichte. Wenn eine Methode kein Ergebnis liefern kann (beispielsweise weil keine Inflationsdaten vorliegen), werden die Gewichte der übrigen Methoden proportional erhöht. Das ist deutlich sinnvoller, als einfach nur „problematische“ Methoden zu streichen.


Programmentwicklung

Nach monatelanger theoretischer Vorbereitung war es an der Zeit, sich ans Keyboard zu setzen. Ich habe mich entschieden, in Python zu programmieren – die Sprache ist schnell genug für Finanzberechnungen, verfügt aber auch über hervorragende Bibliotheken für die Arbeit mit Daten.

Die erste und wichtigste Frage lautet: Woher bekommt man die Daten? Nach Durchsicht zahlreicher Quellen fiel die Wahl auf die API des Internationalen Währungsfonds. Öffentlich, kostenlos, gut dokumentiert und vor allem regelmäßig aktualisiert.

def __init__(self):
    self.base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc"
    self.session = requests.Session()
    self.session.headers.update({
        'User-Agent': 'Manual-PPP-Calculator/1.0',
        'Accept': 'application/json'
    })

Ein kleines, aber wichtiges Detail: Ich verwende requests.Session() anstelle der einfachen Aufrufe von requests.get(). Dadurch können TCP-Verbindungen wiederverwendet werden, was die Leistung bei mehreren Anfragen an eine einzige API erheblich verbessert.

Auch der User-Agent ist nicht zufällig. Viele APIs blockieren Anfragen ohne expliziten User-Agent oder mit dem Standardwert von Python. Es ist besser, sich gleich als ernsthafte Anwendung vorzustellen.

Das nächste Problem erwies sich als heimtückischer als erwartet. Währungscodes (USD, EUR, GBP) müssen im IWF-System in irgendeiner Weise mit Ländercodes verknüpft werden. Hier begannen die Überraschungen.

„EUR“ ist kein Ländercode, sondern ein Code für eine Währungsunion. In der IWF-Datenbank wird die Eurozone als „U2“ bezeichnet. Die Schweiz ist CH, nicht SW. Großbritannien heißt GB, nicht UK.

self.currency_country_map = {
    'USD': 'US', 'EUR': 'U2', 'GBP': 'GB', 'JPY': 'JP',
    'AUD': 'AU', 'CAD': 'CA', 'CHF': 'CH', 'NZD': 'NZ',
    'SEK': 'SE', 'NOK': 'NO', 'DKK': 'DK', 'PLN': 'PL'
}

Es mag wie eine Kleinigkeit erscheinen, aber ohne eine ordnungsgemäße Zuordnung funktioniert das gesamte System einfach nicht. Ich habe einen ganzen Tag lang Fehler gesucht, bis mir klar wurde, dass ich nach Daten für nicht existierende Ländercodes gesucht hatte.

Externe APIs haben die unangenehme Angewohnheit, gerade in den ungünstigsten Momenten auszufallen. Die IWF-API bildet da keine Ausnahme. Manchmal ist es mehrere Stunden lang nicht verfügbar, manchmal liefert es unvollständige Daten, und manchmal gibt es sogar ohne ersichtlichen Grund eine Fehlermeldung aus.

Ich habe mich von Anfang an dazu entschlossen, ein System mit Ersatzdatenquellen aufzubauen:

self.fallback_market_rates = {
    'EURUSD': 1.0850, 'GBPUSD': 1.2650, 'USDJPY': 148.50,
    'AUDUSD': 0.6750, 'USDCAD': 1.3550, 'USDCHF': 0.8850,
    'NZDUSD': 0.6150
}

self.price_levels_2024 = {
    'US': 100.0,    # Basic level
    'U2': 88.5,     # Eurozone is 11.5% cheaper than the US
    'GB': 85.2,     # UK
    'JP': 67.4,     # Japan is significantly cheaper
    'AU': 95.8,     # Australia is close to the USA
    'CA': 91.3,     # Canada is moderately cheaper
    'CH': 125.6,    # Switzerland is the most expensive
    'NZ': 89.7      # New Zealand
}

Diese Angaben basieren auf den aktuellsten verfügbaren internationalen Preisvergleichen und den zum Zeitpunkt der Erstellung dieses Artikels aktuellen Marktkursen. Sie sind zwar nicht ganz auf dem neuesten Stand, aber genau genug, damit das System autonom arbeiten kann.

Manche mögen sagen, es sei eine Krücke. Ich halte das für durchdachte Architektur. In der Finanzwelt ist Zuverlässigkeit wichtiger als absolute Genauigkeit. Es ist besser, ein annähernd korrektes Ergebnis zu haben, als gar kein Ergebnis.

Für die BIP-basierte Methode benötigte ich Schätzungen des BIP verschiedener Länder in US-Dollar. Es scheint eine einfache Aufgabe zu sein: Man muss nur die Daten der Weltbank heranziehen, und schon ist man fertig. Aber auch hier gab es einige Tücken.

Die Weltbank veröffentlicht das BIP in aktuellen US-Dollar (zu Marktkursen) und in US-Dollar zu Kaufkraftparität (KKP). Für meine Zwecke benötige ich den Markt-USD, da ich ihn mit dem BIP in Landeswährung aus den IWF-Statistiken vergleiche.

self.gdp_usd_estimates_2023 = {
    'US': 27000,    # USD 27 trillion
    'U2': 17500,    # Eurozone ~USD 17.5 trillion
    'GB': 3300,     # UK ~USD 3.3 trillion
    'JP': 4200,     # Japan ~USD 4.2 trillion
    'AU': 1700,     # Australia ~USD 1.7 trillion
    'CA': 2100,     # Canada ~USD 2.1 trillion
    'CH': 900,      # Switzerland ~USD 0.9 trillion
    'NZ': 250       # New Zealand ~USD 0.25 trillion
}

Die inflationsbereinigte Methode erforderte einen Bezugspunkt – historische Wechselkurse –, anhand dessen die Inflationsbereinigung berechnet werden sollte. Ich habe mich aus mehreren Gründen für das Jahr 2020 entschieden.

Erstens ist es aktuell genug, um relevant zu sein. Zweitens: Das Jahr 2020 lag noch vor der Pandemie und den damit verbundenen extremen geldpolitischen Maßnahmen. Drittens liegen die Daten für 2020 bereits vor und werden von den statistischen Ämtern nicht mehr revidiert.

self.base_rates_2020 = {
    'U2': 0.85,   # EURUSD
    'GB': 0.78,   # GBPUSD  
    'JP': 106.0,  # USDJPY
    'AU': 1.45,   # AUDUSD
    'CA': 1.34,   # USDCAD
    'CH': 0.92,   # USDCHF
    'NZ': 1.52    # NZDUSD
}

Die Kurse werden als Durchschnittswerte für das Jahr 2020 herangezogen, um kurzfristige Schwankungen auszugleichen. Dies ist wichtig, da bei der inflationsbereinigten Methode davon ausgegangen wird, dass der Basiskurs zu Beginn „angemessen“ war.


Schwierigkeiten bei der Arbeit mit der IWF-API

Der undankbarste, aber zugleich entscheidende Teil jedes Finanzprojekts ist die Arbeit mit externen Datenquellen. Die IWF-API ist leistungsstark und informativ, hat aber ihre Eigenheiten, deren Handhabung etwas Ausprobieren erforderte.

Als Erstes fiel mir auf, dass die API keine großen Anfragen mag. Wenn Sie versuchen, mehrere Indikatoren für mehrere Länder gleichzeitig abzufragen, gibt der Server eine Fehlermeldung aus oder es kommt zu einer Zeitüberschreitung. Ich musste die Aufteilung von Anfragen in kleine Teile umsetzen.

def fetch_all_available_data(self, countries: List[str], years: int = 10):
    all_indicators = [
        'NGDP_XDC',       # GDP in national currency
        'NGDP_USD',       # GDP in USD
        'PCPIPCH',        # Inflation rate
        'NGDP_RPCH',      # Real GDP growth
        'ENDA_XDC_USD_RATE',  # Exchange rate
        'PCPI_IX',        # Consumer Price Index
        'LP'              # Population
    ]
    
    chunk_size = 5  # Maximum 5 indicators per request
    
    for i in range(0, len(all_indicators), chunk_size):
        chunk = all_indicators[i:i + chunk_size]
        
        countries_string = '+'.join(countries)
        indicators_string = '+'.join(chunk)
        
        url = f"{self.base_url}/CompactData/IFS/A.{countries_string}.{indicators_string}"

Ich habe die Blockgröße empirisch festgelegt. 3 Indikatoren sind zu konservativ, man muss sehr viele Abfragen durchführen. 7–8 Indikatoren – führen oft zu Zeitüberschreitungen. 5 erwies sich als der optimale Kompromiss.

Die IWF-API verhält sich manchmal unvorhersehbar. Die gleichen Anfragen können morgens erfolgreich sein und abends fehlschlagen. Manchmal gibt der Server ohne Vorwarnung nur unvollständige Daten zurück. Manchmal liegen Daten in einem unerwarteten Format vor.

Ich habe ein umfassendes Fehlerbehandlungssystem mit Protokollierung hinzugefügt:

try:
    response = self.session.get(url, params={
        'startPeriod': str(start_year),
        'endPeriod': str(end_year)
    }, timeout=60)
    
    if response.status_code == 200:
        raw_data = response.json()
        df_chunk = self._parse_response_data(raw_data)
        
        if not df_chunk.empty:
            all_data.append(df_chunk)
            logger.info(f"Chunk {i//chunk_size + 1}: {len(df_chunk)} data points loaded")
        else:
            logger.warning(f"Chunk {i//chunk_size + 1}: empty response")
    else:
        logger.error(f"HTTP {response.status_code}: {response.text}")

except requests.exceptions.Timeout:
    logger.warning(f"Timeout for chunk {i//chunk_size + 1}")
except requests.exceptions.RequestException as e:
    logger.error(f"Request failed for chunk {i//chunk_size + 1}: {e}")
except Exception as e:
    logger.error(f"Unexpected error in chunk {i//chunk_size + 1}: {e}")
    continue

Ohne eine derart detaillierte Protokollierung wäre das Debuggen die Hölle. Wenn eine Anfrage fehlschlägt, müssen Sie wissen, in welcher Phase dies geschehen ist und warum.

Das Antwortformat der IWF-API verdient besondere Erwähnung. Sie verwenden SDMX-JSON, ein „Standard“-Format für den Austausch statistischer Daten. In der Praxis erwies sich das als ziemlich mühsam.

def _parse_response_data(self, data: Dict) -> pd.DataFrame:
    records = []
    
    try:
        compact_data = data['CompactData']
        dataset = compact_data['DataSet']
        
        if 'Series' not in dataset:
            return pd.DataFrame()
        
        series_list = dataset['Series']
        # API can return a single series as an object or an array of series as a list
        if not isinstance(series_list, list):
            series_list = [series_list]
        
        for series in series_list:
            # All attributes are marked with the '@' symbol - this needs to be processed
            series_attrs = {k.replace('@', ''): v for k, v in series.items() 
                          if k.startswith('@')}
            
            obs_list = series.get('Obs', [])
            if not isinstance(obs_list, list):
                obs_list = [obs_list]
            
            for obs in obs_list:
                if isinstance(obs, dict):
                    record = series_attrs.copy()
                    record.update({
                        'year': obs.get('@TIME_PERIOD', ''),
                        'value': obs.get('@OBS_VALUE', ''),
                        'status': obs.get('@OBS_STATUS', '')
                    })
                    records.append(record)
        
        df = pd.DataFrame(records)
        
        if 'value' in df.columns:
            df['value'] = pd.to_numeric(df['value'], errors='coerce')
        
        if 'year' in df.columns:
            df['year'] = pd.to_numeric(df['year'], errors='coerce')
        
        return df
        
    except Exception as e:
        logger.error(f"Error parsing SDMX-JSON response: {e}")
        return pd.DataFrame()

Alle Attribute in SDMX-JSON sind mit dem Symbol „@“ gekennzeichnet, was bei der Arbeit mit den Daten zu Unannehmlichkeiten führt. Außerdem kann die API eine Datenreihe als Objekt oder mehrere Reihen als Array zurückgeben – auch dies muss verarbeitet werden.

Ein weiteres Problem: Manchmal gibt die API Daten ohne den Abschnitt „Obs“ zurück, manchmal mit einem leeren „Obs“, manchmal enthält „Obs“ keine Liste, sondern ein einzelnes Objekt. Jeder Fall muss separat bearbeitet werden.

Wenn keine aktuellen Inflationsdaten vorliegen, verwende ich für jedes Land typische Werte:

def _approximate_inflation_adjustment(self) -> Dict:
    logger.info("Using approximate inflation adjustment...")
    
    # Typical inflation rates 2020-2024 (based on historical data)
    typical_inflation = {
        'US': 4.5,   # US: Relatively high inflation due to stimulus
        'U2': 3.8,   # Eurozone: Moderate inflation
        'GB': 4.2,   # UK: Brexit + energy crisis
        'JP': 1.8,   # Japan: Traditionally low inflation
        'AU': 4.1,   # Australia: Commodity inflation
        'CA': 3.9,   # Canada: close to the US
        'CH': 2.1,   # Switzerland: Low inflation
        'NZ': 4.0    # New Zealand: Moderate inflation
    }
    
    inflation_adjusted = {}
    
    for country, base_rate in self.base_rates_2020.items():
        us_inflation = typical_inflation.get('US', 4.5)
        country_inflation = typical_inflation.get(country, 3.5)
        
        inflation_differential = us_inflation - country_inflation
        adjustment_factor = 1 + (inflation_differential / 100)
        adjusted_rate = base_rate * adjustment_factor
        
        inflation_adjusted[country] = {
            'inflation_adjusted_rate': adjusted_rate,
            'base_rate_2020': base_rate,
            'inflation_differential': inflation_differential,
            'method': 'approximate_inflation'
        }
        
        logger.info(f"{country}: Approx inflation diff {inflation_differential:+.2f}pp")
    
    return inflation_adjusted

Ich habe diese Zahlen aus verschiedenen Quellen zusammengetragen und den Durchschnittswert für den Zeitraum 2020–2024 berechnet. Sie sind zwar nicht vollkommen genau, liefern aber eine angemessene Annäherung für Länder, bei denen Daten fehlen.


Faire Wechselkurse in echtes Geld umwandeln

Die Berechnung fairer Wechselkurse ist nur die halbe Miete. Das Wichtigste ist, zu wissen, was man damit anfangen soll. Wie lassen sich akademische Berechnungen in praktische Handelssignale umsetzen?

def calculate_ppp_fair_values(self, currency_pairs: List[str]) -> Dict:
    logger.info("Starting manual PPP fair value calculation...")
    
    # Determine which countries we need
    countries = set()
    for pair in currency_pairs:
        base_currency = pair[:3]
        quote_currency = pair[3:]
        
        base_country = self.currency_country_map.get(base_currency)
        quote_country = self.currency_country_map.get(quote_currency)
        
        if base_country and quote_country:
            countries.add(base_country)
            countries.add(quote_country)
    
    logger.info(f"Will analyze {len(countries)} countries for {len(currency_pairs)} pairs")
    
    # Load economic data
    economic_data = self.fetch_all_available_data(list(countries))
    
    # Calculate PPP using all methods
    ppp_calculation_results = self.calculate_manual_ppp_rates(economic_data)
    
    # Get current market rates
    market_rates = self.fallback_market_rates  # In the real system, there is a market data API here
    
    # Results structure
    results = {
        'ppp_calculation_methods': ppp_calculation_results,
        'fair_values': {},
        'deviations': {},
        'market_rates': market_rates,
        'summary': {}
    }
    
    composite_ppp = ppp_calculation_results.get('composite_ppp_rates', {})
    
    # Calculate fair rates and deviations for each pair
    for pair in currency_pairs:
        base_currency = pair[:3]
        quote_currency = pair[3:]
        
        base_country = self.currency_country_map.get(base_currency)
        quote_country = self.currency_country_map.get(quote_currency)
        
        if not base_country or not quote_country:
            logger.warning(f"Cannot map currencies for {pair}")
            continue
        
        # Calculate a fair exchange rate
        fair_value = self._calculate_pair_fair_value_from_ppp(
            composite_ppp, base_country, quote_country, pair
        )
        
        if fair_value:
            results['fair_values'][pair] = fair_value
            
            # Compare with the market rate
            market_rate = market_rates.get(pair)
            if market_rate and fair_value.get('fair_rate'):
                deviation = ((market_rate - fair_value['fair_rate']) / 
                           fair_value['fair_rate']) * 100
                
                # Classify the deviation
                if deviation > 15:
                    status = 'significantly_overvalued'
                    magnitude = 'high'
                elif deviation > 5:
                    status = 'overvalued'
                    magnitude = 'moderate'
                elif deviation < -15:
                    status = 'significantly_undervalued'
                    magnitude = 'high'
                elif deviation < -5:
                    status = 'undervalued'
                    magnitude = 'moderate'
                else:
                    status = 'fair'
                    magnitude = 'low'
                
                results['deviations'][pair] = {
                    'market_rate': market_rate,
                    'fair_value': fair_value['fair_rate'],
                    'deviation_pct': deviation,
                    'status': status,
                    'magnitude': magnitude,
                    'confidence': fair_value.get('confidence', 0.5),
                    'signal_strength': abs(deviation) * fair_value.get('confidence', 0.5)
                }
                
                logger.info(f"{pair}: Market {market_rate:.4f}, Fair {fair_value['fair_rate']:.4f}, "
                          f"Deviation {deviation:+.1f}% ({status})")
    
    # Generate summary statistics
    results['summary'] = self._generate_summary(results['deviations'])
    
    return results
Das Schwierige daran ist, den fairen Wechselkurs für ein Währungspaar auf der Grundlage der Kaufkraftparitäten der einzelnen Länder korrekt zu berechnen:
def _calculate_pair_fair_value_from_ppp(self, composite_ppp: Dict, 
                                      base_country: str, quote_country: str, pair: str) -> Dict:
    """Calculate the fair exchange rate for a currency pair using composite PPP data"""
    
    base_ppp = composite_ppp.get(base_country, {})
    quote_ppp = composite_ppp.get(quote_country, {})
    
    # Case 1: One of the currencies is USD (PPP base currency)
    if quote_country == 'US':  # XXXUSD type pairs
        if base_ppp:
            fair_rate = base_ppp['composite_ppp_rate']
            confidence = base_ppp['confidence']
        else:
            return {}
    elif base_country == 'US':  # USDXXX type pairs
        if quote_ppp:
            fair_rate = quote_ppp['composite_ppp_rate']
            confidence = quote_ppp['confidence']
        else:
            return {}
    else:
        # Case 2: Cross pairs (without USD)
        if base_ppp and quote_ppp:
            # For cross-pairs: PPP_base / PPP_quote
            fair_rate = base_ppp['composite_ppp_rate'] / quote_ppp['composite_ppp_rate']
            # Confidence - the minimum of two currencies
            confidence = min(base_ppp['confidence'], quote_ppp['confidence'])
        else:
            return {}
    
    return {
        'pair': pair,
        'fair_rate': fair_rate,
        'confidence': confidence,
        'base_country': base_country,
        'quote_country': quote_country,
        'base_ppp_data': base_ppp,
        'quote_ppp_data': quote_ppp
    }

Die Logik lautet wie folgt: Wenn wir KKP-Koeffizienten für den Euro (0,885) und das britische Pfund (0,852) im Verhältnis zum US-Dollar haben, dann beträgt der faire Wechselkurs von EUR/GBP = 0,885 / 0,852 = 1,039.

Ein einfacher Vergleich mit den Marktkursen ergibt eine prozentuale Abweichung, doch für die praktische Anwendung ist eine Einstufung erforderlich:

# Classify the deviation
if deviation > 15:
    status = 'significantly_overvalued'
    signal = 'STRONG_SELL'
elif deviation > 5:
    status = 'overvalued'
    signal = 'SELL'
elif deviation < -15:
    status = 'significantly_undervalued'
    signal = 'STRONG_BUY'
elif deviation < -5:
    status = 'undervalued'
    signal = 'BUY'
else:
    status = 'fair'
    signal = 'HOLD'

Ich habe die Schwellenwerte von 5 % und 15 % empirisch festgelegt und anhand historischer Daten getestet. 5 % ist die Mindestabweichung, die als Handelsgelegenheit betrachtet werden sollte (geringere Abweichungen sind möglicherweise nur Rauschen). 15 % stellen bereits ein erhebliches Ungleichgewicht dar, das Beachtung erfordert.

Eine wichtige Neuerung ist die Berechnung der Signalstärke als Produkt aus dem Abweichungswert und dem Konfidenzindex:

'signal_strength': abs(deviation) * fair_value.get('confidence', 0.5)

Auf diese Weise können wir Handelsmöglichkeiten bewerten. Ein Signal mit einer Abweichung von 20 % und einer Konfidenz von 50 % (Stärke = 10) ist weniger aussagekräftig als ein Signal mit einer Abweichung von 15 % und einem Konfidenzniveau von 80 % (Stärke = 12).

Das häufigste Problem sind fehlende Daten. Die Schweiz verfügt über Daten zum BIP, jedoch nicht zur Inflation. Neuseeland verzeichnet zwar Inflation, verfügt jedoch über keine aktuellen BIP-Daten. Und so weiter.

Ursprünglich hatte ich vor, Länder mit unvollständigen Daten einfach auszuschließen, doch dies erwies sich als unzweckmäßig – fast alle Länder wären davon betroffen gewesen. Stattdessen habe ich eine fehlertolerante Degradationsstrategie implementiert:

# Each method works independently
methods_results = {
    'method_1': self._calculate_price_level_ppp(),
    'method_2': self._calculate_gdp_implied_ppp(economic_data),
    'method_3': self._calculate_inflation_adjusted_ppp(economic_data),
    'method_4': self._calculate_big_mac_proxy_ppp()
}

# The composite method adapts to available data
for country in all_countries:
    available_methods = []
    available_rates = []
    
    for method_name, method_results in methods_results.items():
        if country in method_results:
            available_methods.append(method_name)
            available_rates.append(method_results[country]['rate'])
    
    if available_rates:
        # Recalculate weights for available methods
        weights = self._get_adjusted_weights(available_methods)
        composite_rate = sum(rate * weight for rate, weight in zip(available_rates, weights))



Erzielte Ergebnisse

Nach mehreren Monaten der Entwicklung und Fehlerbehebung lief das System endlich stabil. Nun kommt der interessanteste Teil – die praktische Anwendung.

Die Analyse des Diagramms, das die realen EURUSD-Kurse den KKP-Kursen gegenüberstellt, zeigt, dass Veränderungen des KKP-Wechselkurses sehr oft den tatsächlichen Bewegungen des realen Wechselkurses vorausgehen:

Als ich das System mit den Daten für 2024 durchlaufen ließ, erhielt ich einige interessante Ergebnisse:

  • EURUSD: fairer Kurs 0,8753, Marktkurs 1,0850 → EUR überbewertet 
  • GBPUSD: fairer Kurs 0,8288, Marktkurs 1,2650 → GBP überbewertet 
  • USDJPY: fairer Kurs 136,20, Marktkurs 148,50 → JPY unterbewertet
  • USDCHF: fairer Kurs 1,150, Marktkurs 0,8850 → CHF überbewertet

Das interessanteste Ergebnis betraf den JPY. Alle vier Methoden ergaben übereinstimmend, dass der Kurs von 148+ JPY deutlich unterbewertet ist. Der Konfidenzindex lag bei 0,85 und gehörte damit zu den höchsten.

Ich habe kein eigenes Handelssystem auf Basis der Kaufkraftparität entwickelt. Stattdessen habe ich Berechnungen als zusätzlichen Filter in die bestehenden Algorithmen integriert.

Die Logik ist einfach: Wenn ein technisches System ein Signal zum Kauf einer Währung gibt, die gemessen an der Kaufkraftparität deutlich überbewertet ist, wird die Positionsgröße reduziert oder das Signal ignoriert. Umgekehrt werden die Signale in Richtung des KKP-Ungleichgewichts verstärkt.


Weitere Pläne für die Systementwicklung

Die aktuelle Version des Systems ist nur der Anfang

Ich plane mehrere Entwicklungsrichtungen. Maschinelles Lernen wird dazu beitragen, die Gewichtung der Methoden auf der Grundlage der bisherigen Genauigkeit und der aktuellen Bedingungen dynamisch anzupassen. Zu den alternativen Datenquellen zählen OECD-Daten, Lebenshaltungskostenindizes, reale Big-Mac-Preise, Satellitendaten und Stimmungsindikatoren aus sozialen Medien. Die Ausweitung auf Kryptowährungen, Rohstoffe, Aktien und Anleihen wird neue Möglichkeiten eröffnen. Der automatisierte Handel mit dynamischer Absicherung und die Integration von Broker-APIs werden das System vollständig unabhängig machen. Die Echtzeitüberwachung über ein Web-Dashboard rundet das Bild ab.

Was habe ich über KKP und Devisenmärkte gelernt?

Ein paar Monate Arbeit an diesem Projekt haben mir mehr Wissen über die Devisenmärkte vermittelt als jahrelange technische Analyse. Die Kaufkraftparität funktioniert, aber nicht so, wie ich es erwartet hatte.

Dies ist ein Kompass für die langfristige Ausrichtung, keine Zauberformel, um schnell reich zu werden. Abweichungen werden über Monate und Jahre hinweg korrigiert, nicht über Tage und Wochen. Die Datenqualität ist entscheidend – ich habe mehr Zeit mit der Suche nach und der Überprüfung von Daten verbracht als mit der Implementierung der Algorithmen.

Einfachheit schlägt Komplexität – fünf einfache Methoden funktionieren besser als jedes neuronale Netz. Der Handel mit KKP erfordert Geduld, die den meisten Händlern fehlt. Die Fundamentalanalyse stellt im Zeitalter des algorithmischen Handels eine Verbindung zur wirtschaftlichen Realität her. Ein Programm ist lediglich ein Werkzeug zur Lösung eines realen Problems.


Ergebnisse und Schlussfolgerungen

Das Projekt begann aus reiner Neugier: Ist es möglich, faire Wechselkurse selbstständig zu berechnen? Es endete mit der Entwicklung eines vollständigen Analysesystems, das dabei hilft, fundiertere Handelsentscheidungen zu treffen.

Der Weg von der Idee bis zum funktionierenden Code dauerte mehrere Monate, erforderte Hunderte Stunden des Studiums wirtschaftswissenschaftlicher Theorie, Dutzende Algorithmus-Iterationen und unzählige Stunden der Fehlerbehebung. Aber das Ergebnis war es wert.

Mir ist vor allem klar geworden, dass man das Rad nicht neu erfinden oder nach Komplexität streben muss. Manchmal ist der beste Algorithmus ein gut implementierter Klassiker. Die Kaufkraftparität hat vor 500 Jahren funktioniert, sie funktioniert heute und wird auch in Zukunft funktionieren. Wir müssen sie nur richtig anwenden.

Der vollständige Quellcode des Systems ist als Open-Source-Projekt verfügbar. Ich freue mich über Beiträge aus der Community in Form von neuen Datenquellen, alternativen Methoden zur Kaufkraftparitätsberechnung, Verbesserungen bei der Fehlerbehandlung, der Integration von Broker-APIs sowie Backtesting anhand historischer Daten. Wenn Sie sich für Devisenhandel oder quantitative Analyse interessieren, lohnt es sich, dieses System auszuprobieren – es könnte Ihre Sicht auf die Märkte grundlegend verändern.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/18455

Beigefügte Dateien |
PPP_Currency.py (44.66 KB)
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Bewertung der Qualität des Forex-Spread-Tradings anhand saisonaler Faktoren in MetaTrader 5 Bewertung der Qualität des Forex-Spread-Tradings anhand saisonaler Faktoren in MetaTrader 5
Der Artikel untersucht die Qualität eines saisonalen Handelsansatzes auf Tagesbasis, sowohl für einzelne Instrumente als auch für Spreads. Besonderes Augenmerk wird auf die Erkennung wiederkehrender monatlicher Zyklen und deren Anwendungsmöglichkeiten im Handel im laufenden Jahr gelegt.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Algorithmus der Delfin-Echoortung (DEA) Algorithmus der Delfin-Echoortung (DEA)
In diesem Artikel befassen wir uns näher mit dem DEA-Algorithmus, einem metaheuristischen Optimierungsverfahren, das von der einzigartigen Fähigkeit der Delfine inspiriert ist, Beute mithilfe der Echoortung aufzuspüren. Von den mathematischen Grundlagen bis zur praktischen Umsetzung in MQL5, von der Analyse bis zum Vergleich mit klassischen Algorithmen werden wir eingehend untersuchen, warum diese relativ neue Methode einen Platz im Werkzeugkasten von Forschern verdient, die sich mit Optimierungsproblemen befassen.