- In Bereichen wie dem Hochfrequenzhandel (HFT), in denen Latenz direkt Wettbewerbsfähigkeit bedeutet, wird öffentlich nur spärlich verfügbares C++-Optimierungswissen experiment- und implementierungsorientiert aufgearbeitet
- Die Ergebnisse gliedern sich in drei Teile: das Low-Latency Programming Repository, die Optimierung einer marktneutralen Pair-Trading-Strategie und eine C++-Disruptor-Pattern-Bibliothek
- Beim Benchmarking werden Geschwindigkeit, Cache-Nutzung und statistische Signifikanz gemeinsam betrachtet; Cache Warming und Constexpr zeigen große Vorteile bei der Verringerung der Latenz
- Die optimierte Pair-Trading-Strategie verbesserte Ausführungsgeschwindigkeit und Profitabilität, und die Disruptor-Implementierung zeigte bessere Leistung als herkömmliche Queue-Ansätze
- Zu den künftigen Aufgaben gehören die Erweiterung des Repositorys, Tests in einer realen Handelsumgebung sowie die Integration von Disruptor und Handelsalgorithmen mit anschließendem Benchmarking des Gesamtsystems
Ziele der Low-Latency-Optimierung im HFT
- Ziel ist es, latenzsensitiven Code zu optimieren und so die Ausführungsgeschwindigkeit zu erhöhen
- Der Fokus liegt auf Programmierstrategien und Datenstrukturen, wie sie im Hochfrequenzhandel eingesetzt werden
- Die Finanzindustrie, insbesondere Buy-Side-Unternehmen mit Aktivitäten an öffentlichen Märkten, veröffentlicht entsprechendes Wissen aufgrund von Vertraulichkeit und Wettbewerbsvorteilen nur selten
- Um diese Lücke zu verkleinern, wurde ein maßgeschneidertes Low-Latency Programming Repository mit verschiedenen Techniken erstellt und durch statistisches Benchmarking validiert
Drei Ergebnisse
-
Low-Latency Programming Repository
- Es bleibt nicht bei einer Theoriesammlung, sondern dient als praktischer Leitfaden einschließlich statistischem Benchmarking
- Es kuratiert Programmiertechniken, Design Patterns und Best Practices zur Verringerung der Latenz in HFT-Systemen
-
Optimierung einer marktneutralen statistischen Arbitrage-Pair-Trading-Strategie
- Es integriert Techniken zur Latenzreduzierung und Optimierungen auf CPU-Ebene
- Es zeigt Verbesserungen bei Ausführungsgeschwindigkeit und Profitabilität
-
C++-Disruptor-Pattern-Bibliothek
- Sie zeigt Leistungsverbesserungen gegenüber herkömmlichen Queue-Ansätzen
- Sie demonstriert, dass sich solche Datenstrukturen auf das Order Management System (OMS) von HFT-Systemen anwenden lassen
Warum es an öffentlichem Wissen fehlt
- Wissen zur Optimierung von HFT-Systemen stammt überwiegend von Praktikern aus der Branche, doch aufgrund von Vertraulichkeit und Wettbewerbsvorteilen lassen sich aktuelle Forschung und Implementierungsdetails nur schwer offenlegen
- Gerade in Bereichen wie Latenzverbesserung, Code-Effizienz und Cache-Optimierung ist das öffentlich verfügbare Material besonders begrenzt
- Es gibt HFT-Forschung aus ökonomischer und finanzwissenschaftlicher Perspektive sowie Arbeiten zu mathematischen Modellen des algorithmischen Handels, doch nur selten behandeln sie die Details von Code-Optimierung oder Latenzreduktion
- Auch wenn es vergleichsweise viel C++-Literatur gibt, wird diese nur begrenzt direkt in den Kontext von Ultra-Low-Latency-HFT-Systemen eingeordnet
- Online-Blogs und Beiträge liefern oft nur oberflächlich Daten zur durchschnittlichen Latenzzeit; detaillierte Analysen des Cache-Zugriffs oder der Verzögerung bei der Befehlsausführung fehlen häufig
Bewertung und Leistungsverbesserung
- Zu den Bewertungsmetriken gehören Geschwindigkeit, Cache-Nutzung und statistische Signifikanz
- Unter den Techniken des Low-Latency Programming Repository zeigen Cache Warming und Constexpr die größten Vorteile bei der Verringerung der Latenz
- Die Implementierung des Disruptor-Patterns erzielt mithilfe von Ringpuffer, Sequenznummern und speziellen Wait-Strategien eine bessere Latenz und Geschwindigkeit als herkömmliche Queue-Ansätze
- Die marktneutrale Pair-Trading-Strategie verbessert durch Optimierungen auf CPU-Ebene und Techniken zur Latenzreduktion sowohl Ausführungsgeschwindigkeit als auch Profitabilität
Öffentliches Repository und künftige Arbeiten
- Repository, Handelsstrategie und Disruptor-Bibliothek befinden sich unter
https://github.com/0burak/imperial hft - Zu den künftigen Arbeiten gehört die Erweiterung des Repositorys
- Es bleibt die Aufgabe, den optimierten Handelsalgorithmus in einer realen Handelsumgebung zu testen
- Dazu gehört auch die Integration des Disruptor-Patterns in Handelsalgorithmen, um Benchmarking auf Ebene des Gesamtsystems durchzuführen
1 Kommentare
Hacker-News-Kommentare
Dieser Artikel wirkt wie eine ziemlich grundlegende Einführung in das Thema
Nach meiner Erfahrung im Unterrichten von Studierenden wissen selbst sie so etwas meist schon. In Vorlesungen zur Rechnerarchitektur lernt man grundlegende Performance-Faktoren wie Branch Prediction, Cache-Kohärenz und Instruction Cache
Überraschend fand ich, dass der klassische Performance-Killer False Sharing überhaupt nicht behandelt wird, und insgesamt scheint der Fokus vor allem auf Single-Thread-Latenz zu liegen. Ebenso erstaunlich war, dass „kostenlose“ Optimierungshinweise wie fat LTO, PGO,
[[likely]],[[unlikely]]fehlenTiefergehende Performance-Probleme betreffen dann konkrete I/O-APIs, Synchronisationsprimitiven, Interprozesskommunikation und sogar die Nutzung obskurer Compiler-Built-ins
Was Low-Latency-Programmierern am meisten fehlt und am schwersten zu vermitteln ist, ist eine Art Paranoia. Man braucht echte Angst und Wut gegenüber unnötigen Allokationen, Kopien und Performance-Killern. Dieses zwanghafte Bedürfnis, mit callgrind immer wieder Benchmarks laufen zu lassen, nur um einen Objekt-Cache-Miss mitten in einer Hot Loop zu finden, der in einen Aufruf des Allocators führt
Für mich persönlich war ein wichtiger Moment, als mir beim Bau eines Low-Latency-Servers klar wurde, dass es insgesamt schneller sein kann, kleine Objekte in einen zusammenhängenden Puffer zu kopieren und ein einzelnes
writeabzusetzen, statt Vektor-I/O-Operationen zusammenzustellen. Es gibt keine kostenlosen Kopien, und fat pointer sind keine AusnahmeSelbst das derzeit beste Material besteht nur aus ein paar C++-Konferenzvorträgen, und das ist enttäuschend
Wenn man den Drang, anzugeben, beiseitelässt, ist dieses Dokument ein hervorragender Beitrag zu diesem Gebiet und vielleicht die erste maßgebliche Referenz. Vage Aussagen wie, man könne sich ähnliche Informationen aus anderen Vorlesungen zusammensuchen, sind kein Beitrag und helfen niemandem
Zuerst würde ich für rohe Geschwindigkeit die Last mit einem Frontend-FPGA in einfache asset-spezifische Datenströme aufteilen. Der Reibungsverlust bei iterativer Entwicklung, Personal und Lieferkette ist dabei aber so groß, dass ich der Versuchung widerstehen würde, das bis zur tatsächlichen Ausführung durchzuziehen. Die Eingabe wäre etwas wie ein FIX-Stream, die Ausgabe würde entlang eines Low-Latency-Busses in asset-spezifische binäre Event-Streams aufgeteilt, die in asset-spezifische Segmente eines skalierbaren Clusters aus günstigen MCUs gehen
Zweitens würde eine asset-spezifische Ausführungsplattform auf MCU-Basis Annahmen eines General-Purpose-Betriebssystems eliminieren und so auf tatsächlich verfügbarer Hardware einen schnelleren Übergang zu Low-Level-Code ermöglichen, den Menschen noch schreiben können. Drittens: Profit? In so einer Struktur müsste ein Supervisor auf Basis eines General-Purpose-Betriebssystems den Gesamtzustand überwachen und bei Bedarf einzelne Elemente neu programmieren, um Strategien zu stoppen oder zu ändern
Entscheidend ist, wie niedrig die tatsächliche Latenz ausfällt. Ab einem gewissen Punkt ist es vielleicht sinnvoller, statt weiterer Engineering-Arbeit die Kosten dafür zu tragen, die Hardware näher an den Core zu bringen. Das dürfte stark von den Regeln, Rechenzentren und Link-Infrastrukturen abhängen, die an der jeweiligen Börse oder in dem jeweiligen Pool verfügbar sind
Viele profitable Betreiber legen wohl nicht offen, mit welchem Pool sie verbunden sind, und ignorieren womöglich Regulierung oder Vertragsbedingungen, während sie Front Running zum Geschäftsmodell machen. In solchen Fällen ist die relative netzwerkgeografische Latenz zwischen zwei Ausführungspunkten wichtiger als die absolute Latenz zu einem einzelnen Punkt
Tatsächlich hört man von Compiler-Leuten oft als Faustregel, dass solche Hinweise selbst ohne PGO in den meisten Fällen kontraproduktiv sind. Moderne Compiler vertrauen ihren eigenen Analyse-Passes mehr als solchen Hinweisen und ignorieren sie meist
Zur Einordnung: In realem Code habe ich solche Hinweise nur dort gesehen, wo der Compiler sie leicht selbst setzen könnte. Zum Beispiel bei einem Null-Check nach einem
malloc-AufrufDer Punkt, den ich hervorheben möchte, ist dieser
„Die Ausgabe dieses Tests sind die Teststatistik (t-statistic) und der zugehörige p-value. Die t-statistic, auch Score genannt, ist das Ergebnis des Unit-Root-Tests für die Residuen. Eine negativere t-statistic deutet darauf hin, dass die Residuen mit höherer Wahrscheinlichkeit stationär sind. Der p-value liefert ein Maß für die Wahrscheinlichkeit, dass die Nullhypothese des Tests — also die Hypothese, dass keine Kointegration vorliegt — wahr ist. Das Testergebnis ergab einen p-value von etwa 0.0149 und eine t-statistic von -3.7684.“
Dieser Teil wirkt, als sei er mit einem LLM geschrieben worden
Auch das Beispiel ist wirklich seltsam. Man betrachtet die Korrelation von Schlusskursen einmal täglich über fünf Jahre und schreibt dann Code, der den Spread mit 65 Mikrosekunden Latenz berechnet. Das ergibt als reale Aufgabe keinen Sinn. Im inneren Loop würde man die Spread-Statistik tatsächlich nicht berechnen, und 65 Mikrosekunden sind für einen inneren Loop viel zu langsam
Der Punkt mag sein, Optimierungstechniken zu üben, aber als Ziel für Optimierung ist es ziemlich unrepräsentativ
Es wurde eine Implementierung einer Börse in C++ gebaut, die das LMAX-Disruptor-Muster verwendet
https://github.com/sneilan/stock-exchange
Auch eine grundlegende Implementierung von LMAX Disruptor wurde in ein paar C++-Dateien umgesetzt
https://github.com/sneilan/lmax-disruptor-tutorial
Allerdings wird gerade geprüft, das Ganze noch einmal in Rust zu bauen. Man ist bereits bis zu dem Punkt gekommen, an dem ein eigenes WebSocket-Protokoll, ein Authentifizierungssystem, SSL usw. implementiert wurden, hat dabei aber festgestellt, dass Speicherverwaltung und Abhängigkeiten in Rust deutlich einfacher sind. Besonders bei einem Softwareprojekt, das von einer einzelnen Person getragen wird
Speicherzugriffe können sowohl vom Compiler als auch von der CPU umgeordnet werden; um also die im ursprünglichen LMAX-Disruptor-Paper beschriebenen Barrieren zu erhalten, sollte man für Produzenten- und Konsumentenposition
std::atomicverwendenIn der Methode
getwird ein Zeiger auf ein internes Element der Queue zurückgegeben, nachdem die Konsumentenposition erhöht wurde, also nachdem der Slot für den Produzenten freigegeben wurde. Dadurch kann das Element überschrieben werden, während der Nutzer noch darauf zugreiftAußerdem liegen Produzenten- und Konsumentenposition mit hoher Wahrscheinlichkeit in derselben Cache-Line, wodurch False Sharing entsteht
T *item = &this->shared_mem_region->entities[this->shared_mem_region->consumer_position];this->shared_mem_region->consumer_position++;this->shared_mem_region->consumer_position %= this->slots;kann man Folgendes tun
uint64_t mask = slot_count - 1; // binär nur 1enitem = &slots[ pos & mask ];pos ++;Das heißt, Division/Modulo lassen sich durch ein bitweises AND ersetzen, was die Berechnung etwas reduziert. Voraussetzung ist allerdings, dass die Ringpuffergröße eine Zweierpotenz ist
Darüber hinaus kann man Sequenznummern mit dem gesamten Wertebereich wie
uint64_tverwenden. Das Wrapping wird automatisch behandelt. Auch das Subtrahieren zweier Sequenznummern funktioniert unter Berücksichtigung des Wrap-arounds problemlos. Das dumme Problem, einen Slot freilassen zu müssen, um zwischen vollem und leerem Puffer zu unterscheiden, entfällt ebenfallsNatürlich muss man darauf achten, dass das Fenster der „lebenden“ Sequenznummern niemals größer wird als das Fenster des Ringpuffers
Bei der Speicherverwaltung könnte man erwägen, auf
std::shared_ptrumzustellen. Das beseitigt diese Sorge vollständig, ohne die Geschwindigkeit zu verringernFür Sockets gibt es freie Open-Source-Bibliotheken, die bessere Performance liefern als selbst geschriebener Code und zugleich lästige Sonderfälle reduzieren. Zum Beispiel ist das Iterieren über
FD_ISSETlangsamer alsepolloderkqueueDas Abhängigkeitsmanagement ist in C++ definitiv rauer als in anderen Sprachen. Manchmal ist das Finden schwieriger als das Verwalten. Brauchbarer Bibliothekscode liegt verstreut überall herum, manches davon versteckt sich in vergessenen Ecken des Internets. Ihn aufzuspüren ist selbst eine Fähigkeit und kann, wenn man gut darin ist, sehr lohnend sein
In dem System, an das gedacht wird, dürfte ein SPSC-Ringpuffer schwer zu schlagen sein, und falls nötig könnte man Work-Stealing auch mit altmodischen Locks umsetzen
https://martinfowler.com/articles/lmax.html
https://github.com/CppCon/CppCon2017/blob/master/Presentatio... kommt mir in den Sinn
Die Folie, auf der ein Fake-Server Auftragsdaten wiedergibt, ein zweiter Server die Ausführungszeit berechnet und die Paketzeiten des Testservers mit einem Hardware-Switch gemessen werden, ist auf eine angenehm kompromisslose Art hardcore
Ich hätte zwar keine Lust, im Finanzbereich zu arbeiten, aber es wäre interessant, ein performancekritisches System zu betreuen, bei dem es wirtschaftlich vertretbar ist, rackweise Hardware nur fürs Benchmarking zu kaufen
Es wurde eine C++-Logging-Bibliothek gebaut, die dem LMAX Disruptor in vieler Hinsicht ähnelt und offenbar auch in der HFT-Community einigermaßen genutzt wird
Das ursprüngliche Ziel war, in Produktionsumgebungen Post-mortem-Debugging mit extrem detaillierten Logs ohne Performance-Einbußen zu ermöglichen. Es gab Kollegen, die sich weigerten, für die Problemanalyse wichtige Informationen ins Log aufzunehmen, weil sie Performance-Effekte befürchteten; mit dieser Bibliothek war diese Debatte beendet
[1] https://github.com/mattiasflodin/reckless
Ein weiterer Vorteil von Dispatch zur Compile-Zeit ist, dass der Compiler den Code der aufgerufenen Funktion direkt an der Aufrufstelle inline einfügen kann, wenn er statisch bestimmen kann, welche Funktion aufgerufen wird.
Dadurch lässt sich der Overhead des Funktionsaufrufs vollständig eliminieren, und zusätzliche Optimierungen wie Dead-Code-Elimination oder Konstantenfortpflanzung werden möglich.
Ein guter JIT unterstützt polymorphes Inlining. Meine Erfahrung mit C++ liegt zwar schon etwas zurück, aber die Lösung für dieses Problem war PGO. Es wird allerdings nicht breit genutzt. Stattdessen vermeidet man in performancekritischem Code eher dynamischen Dispatch selbst.
Die allgemeinere Lehre ist, dass man in jeder Sprache in heißen Codepfaden unnötige dynamische Verzweigungen vermeiden sollte, sofern man nicht sehr sicher ist, dass Compiler oder JIT hindurchsehen können.
https://youtu.be/i5MAXAxp_Tw
Gibt es einen guten Grund dafür, dass es Hochfrequenzhandel überhaupt geben sollte? Die Leute kritisieren oft, dass Bitcoin Energie verschwendet, aber auch das hier wirkt gesellschaftlich wie ein klarer Nettoverlust und scheint seltsamerweise einfach durchzugehen.
Es ist schwer zu sagen, dass diese Branche enorm gemeinwohlfördernd ist, aber wenn die Spreads enger werden, fließt zumindest weniger Geld an Intermediäre.
HFT ist zwar ein ziemlich konzentrierter Bereich, aber vom Umfang her eher klein. Im Hinblick auf Energieverschwendung liegt es um mehrere Größenordnungen unter Bitcoin.
Der einzige positive Effekt von HFT sind Liquidität und engere Spreads, wobei das auch davon abhängt, wie man HFT definiert. Zum Beispiel hätten Robinhood und kostenloser Handel ohne HFT wahrscheinlich nicht existiert.
Diese Akteure holen sich Anteile, die früher an Broker und Banken gingen. HFT ist kein Geschäft, das „Kleinanleger“ ausnimmt.
Aus meiner Sicht hat es kaum oder gar keine negativen Auswirkungen auf die Gesellschaft. Wer langfristig in den Aktienmarkt investiert, hat kaum einen Grund, sich um HFT zu kümmern.
Jedenfalls gibt es kein natürliches Ereignis, das Hochfrequenzhandel erfordert. Dass sich fundamentale Werte extrem schnell ändern, ist selten, und selbst dann handelt es sich eher um einen gesicherten Übergang als um Volatilität.
HFT beseitigt Inkonsistenzen, etwa wenn drei Währungspaare nicht zueinander passen oder eine „offensichtliche“ Fehlbepreisung vorliegt, und macht die Finanzmärkte dadurch ein kleines bisschen präziser.
Wenn man etwas handeln will, gibt es auf der Gegenseite jemanden. Meistens wird man zu dem Preis, den man möchte, wahrscheinlich mit einem HFT-Teilnehmer handeln. Wenn man einen besseren Preis bekommt, ist das Geld, das man behält.
Auch der Aussage, es werde „einfach durchgewunken“, kann ich schwer zustimmen. HFT wird auch hier ziemlich häufig kritisiert.
Für professionelle Entwickler lohnt es sich, das vollständig anzusehen.
https://github.com/CppCon/CppCon2017/tree/master/Presentatio...
und auch das übergeordnete Verzeichnis.
Ich frage mich etwas. Warum verwendet oder verwendete man in diesem Bereich für die Logik C++ statt C? Welche Vorteile hat C++ in diesem Umfeld gegenüber C? Ich kenne mich mit C/Assembler gut aus, aber mit HFT-Praktiken überhaupt nicht, daher wäre eine einfache Erklärung hilfreich.
Natürlich kann man darüber streiten, ob diese Ausdrucksstärke die enorme Komplexität der Sprache wert ist, aber in der Praxis haben sich die Leute aus empirischen Gründen für C++ entschieden.
Die Struktur und der Ton dieses Artikels riechen stark nach LLM