Die Struktur der Datenhaltung für die Optimierung des Handelssystems jenseits von Backtests.
10.11.2023
Rohdaten
Die Rohdaten stammen von Interactive-Brokers. Sie werden über die API eingelesen und liegen als
OHLC-Datensätze vor. Es gibt spezialisierte OHLC-Klassen für Gattungen der Rohdaten
( aktuell: Trades: Handelspreise, Vola: Implizite Volatilitäten). Bearbeitete Daten liegen ebenfalls in
OHLC-Klassen vor, z.B. aggregierte Daten als TrueRange.
Die OHLC-Daten sind mit bidirektionalen Edges untereinander und mit dem Time-Grid verbunden. Der Bezug zum Basiswert wird ebenfalls über die Edges gewährleistet. Es besteht nur eine unidirektionale Verknüpftung der Edges zum Basiswert. Der Basiswert selbst ist dumm, er kennt seine Rohdaten nicht. Auswertungen sind allerdings mit dem Basiswert verknüpft.
Das Time-Grid
Das Time-Grid übernimmt die Rolle von Indizes und Relationen bei klassischen RDMS-Datenbanken. Die
OHLC-Datensätze sind an eine TG::Tag-Node angeheftet. Das Datumsfeld im OHLC-Datensatz ist deshalb
nicht mehr nötig.
OHLC <--- has-ohlc ---> TG::Tag <-- --> TG::Monat <-- --> TG::Jahr | Datenbank-
| Klasse (Type)
:open :contract :w :w :w |
:high (shortcut) (tag) (monat) (jahr) | Attribute
:low |
:close |
:wap
:volume
Die OHLC-Datensätze werden über das TimeGrid ausgewählt:
"20.10.2020".to_tg.nodes( :outE, via: TG::HasOhlc, where: { contract: 'SPX' } )
=> select outE('tg_has_ohlc')[ contract='SPX' ].inV() from #119:1406
[ <vola[#407:73]:{2->}{->1}, close: 0.25, high: 0.26, low: 0.25, open: 0.25, wap: 0.26>,
<trades[#383:73]:{2->}{->1}, close: 3443, high: 3476, low: 3435, open: 3439, trades: 21621 >]
Das Startdatum wird als String oder Date-Objekt definiert, die Methode to_tg ist eine Datenbankabfrage,
die das Datum zum TG::Tag-Node dekodiert (Ergebnis: #119:1406).
Ausgehend von dieser Node werden über die Methode nodes alle verknüpften OHLC-Records selektiert.
Es werden zwei Records gefunden. Die generierte Datenbankabfrage ist simpel, effizient und schnell.
Der Zugriff auf direkt mit dem Time-Grid verbundenen Daten-Nodes ist stets über to_tg.nodes .. via: ..
möglich. Häufig benutzte Abfragen können als Methoden in der Klasse TG::Tag vordefiniert werden.
Für den Zugriff auf OHLC-Objekte ist die Methode ohlc_bar definiert:
class Tag
# selects the first connected node to the time-grid
# which is connected via a TG::HasOhlc Edge
def ohlc_bar contract
edge_name = block_given? ? yield : TG::HasOhlc
n = query.nodes :outE, via: edge_name, where: { contract: contract.shortcut }
n.execute &.select_result &.first
# This is equivalent:
# db.query(" select outE('#{edge_name.database_name}')
# [contract='#{contract.shortcut}']
# .inV() from #{rid} ").allocate_model.first
end
end
Zugriff auf Datenreihen
Informationen über die Struktur der Daten werden über die Analyse von Ausschnitten der Zeitreihen gesammelt.
- Die OHLC-Datensätze sind untereinander verbunden. Sie bilden einen linearen Graphen, der effizient durchmustert werden kann.
- Alle verknüpften Datensätze eines Zeitraums sind über das Time-Grid selbst zugänglich.
Für den Zugriff auf alle Datenpunkte eines Intervalls auf einem linearen Graphen ist die Methode vector zuständig.
spx = Contract.find shortcut: 'SPX'
ohlc = "20.10.2020".to_tg.ohlc_bar( spx ){ TG::HasVola }
=> <vola[#407:73]:{2->}{->1}, close: 0.25, high: 0.26, low: 0.25, open: 0.25...
puts ohlc.vector(4).map{|x| [ x.datum, x.wap ].join(" -> ")}.join("\n")
=> select from ( traverse out('connect_ohlc') from #407:73 while $depth < 4 )
where $depth >= 0
2020-10-20 -> 0.26
2020-10-21 -> 0.26
2020-10-22 -> 0.24
2020-10-23 -> 0.25
Alternativ kann das Time-Grid durchfahren und jede Node auf entsprechende Verbindungen getestet werden.
tg = Date.new( 2020,10,20 ).to_tg
puts tg.vector(4).map{|y| y.ohlc_bar( spx ){ TG::HasVola }}
.map{|x| [ x.datum, x.wap ].join(" -> ")}.join("\n")
2020-10-20 -> 0.26
2020-10-21 -> 0.26
2020-10-22 -> 0.24
2020-10-23 -> 0.25
In dieser Form wird pro durchmusterter Node eine separate Abfrage nach verbundenen OHLC-Records gestellt. Das wird im Produktionsbetrieb durch eine optimierte Abfrage ersetzt, die alles in einem Rutsch erledigt.
Relativer Zugriff auf Einzelwerte
Bei Optionshandelssystemen ist wichtig, wie sich der Basiswert bis zur Fälligkeit entwickelt. Was zwischendurch passiert, ist insbesondere passiven Kurzfristhandel wenig relevant. Statt dessen ist eine zuverlässige Aussage über den Handelspreis zur Fälligkeit gefordert.
Für den Fall einer (fiktiven) Positionseröffnung soll eine Wahrscheinlichkeit ermittelt werden, mit der der Basiswert unterhalb oder oberhalb des gewählten Strikes der Option den Handel zur Fälligkeit beenden wird.
Als erste Vorbereitung müssen dafür Gesetzmäßigkeiten ermittelt werden. Dafür ist
die Preisstruktur in der verbleibenen Restlaufzeit zu untersuchen. Die Preis-Schnipsel sind über die
oben vorgestellte Methode vector zugänglich.
Falls jedoch nur ermittelt werden soll, welcher Marktpreis zum Zeitpunkt X nach der Positionseröffnung
auftrat, kann der OHLC-Datensatz entweder über
# Start: 20.10., Laufzeit: 28 Handelstage = 41 Tage
( Date.new(2020,10,20) + 41 ).to_tg.ohlc_bar( spx ){ TG::HasTrades }.datum
=> select outE('tg_has_trades')[ contract='SPX' ].inV() from #363:1409
Mon, 30 Nov 2020
oder wiederum relativ über den Parameter
start_at der Methode vector angesteuert werden.
ohlc = "20.10.2020".to_tg.ohlc_bar( spx ){ TG::HasTrades }
ohlc.vector(29, start_at: 28).map &:datum
=> select from ( traverse out('connect_ohlc')
=> from #407:73 while $depth < 29 )
=> where $depth >= 28
[Mon, 30 Nov 2020]
Bei wenigen Abfragen ist die erste Methode vorzuziehen. Je mehr und je kürzer die zu analysierenden Zeitscheiben sind, desto mehr spricht für die zweite Methode. Das Datum muss dann nur einmal dekodiert werden.
Mit wenig Aufwand kann nun z.B. die Ableitung des Preises nach der Zeit mit unterschiedlichen Deltas ermittelt werden:
Ausgangspunkt: Jeder Freitag in der gesamten Datenreihe
select_all_fridays = ->(v){ (v).select{|x| x.wday == 5 } }
all_fridays = select_all_fridays[ Trades.start_entry(spx).first.datum .. Trades.last_entry(spx).first.datum ]
Zu jedem Datum die Preisdifferenz für 28 Tage (20 Handelstage) ermitteln
all_fridays.map do | f |
start_node = f.to_tg.ohlc_bar( spx ){ Tg::HasTrades }
next if start_node.nil?
# variante 1
last_node = start_node.vector( 21, start_at: 20 ).first
# variante 2
last_node = (f+28).to_tg.ohlc_bar( spx ){ Tg::HasTrades }
next if last_node.nil?
start_node.close - last_node.close
end
Das Ergebnis:
15.95 -24.78 -25.53 -26.56 23.99 …