Dokumentation

BAR6-Format

Das binäre Datenformat von partiqon für effiziente, transparente Speicherung und schnellen Zugriff auf Marktdaten. Designed für Geschwindigkeit, kompakte Speicherung und keinen Vendor-Lock-in.

Überblick

BAR6 ist ein offenes, dokumentiertes Binärformat für historische Marktdaten. Es unterstützt sowohl Bar-Daten (OHLCV) als auch Rohtick-Daten mit Bid/Ask-Informationen. Anders als proprietäre Formate ist BAR6 vollständig dokumentiert und kann von jedem Werkzeug gelesen werden.

Dateistruktur

Jede BAR6-Datei beginnt mit einem 49 Byte großen Binär-Header, gefolgt von Datensätzen fester Größe:

[Header (49 Bytes)] [Datensatz 1] [Datensatz 2] ... [Datensatz N]

Datensätze haben eine feste Größe und stehen in aufsteigender Zeitfolge. Datensatz n liegt damit bei 49 + n × Datensatzgröße. Jedes Werkzeug kann direkt an eine Stelle einer mehrere Gigabyte großen Datei springen, ohne alles davor zu lesen.

Header (49 Bytes)

Little-Endian, ohne Padding. Python-Formatstring: <IIBIIqq16s

OffsetGrößeTypFeldBeschreibung
04uint32magic0x42415236, die ASCII-Bytes „BAR6"
44uint32version2 = aktuell
81uint8data_type0 = Bar-Datensätze, 1 = Tick-Datensätze
94uint32timeframe_minutesz. B. 15 für M15. 0 bei Tick-Dateien, denn Tick-Daten haben keinen Zeitrahmen
134uint32record_countAnzahl der folgenden Datensätze
178int64start_tsZeitstempel des ersten Datensatzes, Unix-Millisekunden UTC
258int64end_tsZeitstempel des letzten Datensatzes, Unix-Millisekunden UTC
3316char[16]symbolMit Nullbytes aufgefüllter Instrumentenname, z. B. „DEU.IDX-EUR"

Bar-Datensatz (56 Bytes)

Vorhanden bei data_type = 0. Python-Formatstring: <q6d

OffsetGrößeTypFeldBeschreibung
08int64timestampERÖFFNUNGSzeit der Kerze, Unix-Millisekunden UTC
88doubleopenEröffnungskurs
168doublehighHöchstkurs der Kerze
248doublelowTiefstkurs der Kerze
328doublecloseSchlusskurs
408doublebidLetzter Bid-Quote der Kerze; zusammen mit Ask der Spread, den ein Fill zahlt
488doubleaskLetzter Ask-Quote der Kerze; zusammen mit Bid der Spread, den ein Fill zahlt

Ein Bar-Datensatz hat kein Volumenfeld. BAR6 speichert stattdessen Bid und Ask, denn ein Backtest muss wissen, was eine Ausführung gekostet hätte, und das lässt sich aus einer Volumenangabe nicht rekonstruieren.

Tick-Datensatz (32 Bytes)

Vorhanden bei data_type = 1. Python-Formatstring: <q3d

OffsetGrößeTypFeldBeschreibung
08int64timestampUnix-Millisekunden UTC
88doublebidBid-Kurs
168doubleaskAsk-Kurs
248doublevolume0, wenn die Quelle keines geliefert hat

Daten konvertieren

Nutze DataConvert, um deine Historien (CSV, MT4 .hst) in das BAR6-Format zu konvertieren. DataConvert läuft lokal auf deinem Rechner; deine Rohdaten verlassen nie deinen PC.

Warum BAR6?

BAR6 programmatisch lesen

BAR6 ist einfach genug, um in jeder Sprache gelesen zu werden. Dieses Python liest beide Dateiarten und wird in unserer Testsuite gegen echte BAR6-Dateien ausgeführt. Es funktioniert also nachweislich und nicht nur der Absicht nach.

import struct HEADER_FORMAT = "<IIBIIqq16s" # 49 Bytes BAR_FORMAT = "<q6d" # 56 Bytes TICK_FORMAT = "<q3d" # 32 Bytes with open("data.bin", "rb") as f: header = f.read(struct.calcsize(HEADER_FORMAT)) (magic, version, data_type, timeframe_minutes, record_count, start_ts, end_ts, symbol) = struct.unpack(HEADER_FORMAT, header) assert magic == 0x42415236, "keine BAR6-Datei" symbol = symbol.rstrip(b"\x00").decode("utf-8") print(symbol, "M" + str(timeframe_minutes), record_count, "Datensätze") record_format = BAR_FORMAT if data_type == 0 else TICK_FORMAT record_size = struct.calcsize(record_format) for _ in range(record_count): record = f.read(record_size) if len(record) < record_size: break # abgeschnittene Datei if data_type == 0: timestamp, o, h, l, c, bid, ask = struct.unpack(record_format, record) else: timestamp, bid, ask, volume = struct.unpack(record_format, record) # Datensatz verarbeiten ...

Das Präfix < ist entscheidend: Es legt Little-Endian fest und schaltet das Padding ab, das Python sonst zwischen Feldern unterschiedlicher Größe einfügt. Ohne das kommen auf manchen Plattformen falsche Datensatzgrößen heraus.

Direkt zu einem Zeitpunkt springen

Da die Datensätze feste Größe haben und zeitlich sortiert sind, ist das Finden eines Zeitstempels eine binäre Suche über die Datei statt eines Durchlaufs: bei 170 Millionen Datensätzen etwa 28 Lesezugriffe von je 8 Byte.

def seek_to(f, header_size, record_size, record_count, target_ms): """Index des ersten Datensatzes ab target_ms.""" lo, hi = 0, record_count while lo < hi: mid = (lo + hi) // 2 f.seek(header_size + mid * record_size) (timestamp,) = struct.unpack("<q", f.read(8)) if timestamp < target_ms: lo = mid + 1 else: hi = mid return lo