Organisation der Daten

Die Struktur der Datenhaltung für die Optimierung des Handelssystems jenseits von Backtests.

Notizen zum SPX30DTE 10. Nov. 2023

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
nothing
Abbildung 1: Visualisierung der Datenstruktur: jeweils zwei OHLC-Records sind mit dem Time-Grid verbunden.

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 …