Aus Linux-Magazin 10/2023

Aktienkurse per Go-Programm überwachen

© thapana onphalai / 123RF.com

Die Kursentwicklung wichtiger Aktien verfolgt Michael Schilli mit einer Anwendung, die aktuelle Kurse mittels API einholt und in einer Terminaloberfläche anzeigt.

Laut der US-Vizepäsidentin Kamala Harris ginge der Mehrheit der Amerikaner das Geld aus, falls nur 400 Dollar unerwartete Kosten anfielen. Seitdem ist es mir ein Anliegen, jeden Tag darüber Bescheid zu wissen, ob ich noch genügend Reserven auf der hohen Kante habe. Dazu gehört, die aktuelle Kursentwicklung der Aktien bekannter Unternehmen zu verfolgen. Zwar gibt es zuhauf Apps mit Portfolio-Einstellungen, die eine Reihe ausgewählter Wertpapiere beobachten. Ich nutze aber lieber in Go geschriebene Kommandozeilenwerkzeuge, die im Terminal laufen.

Abbildung 1 zeigt die Ausgabe des Go-Programms Pofo – das Kürzel steht für Portfolio. In insgesamt sechs Kacheln, drei unten und drei oben, veranschaulicht jeweils eine Balkengrafik den Kursverlauf der Aktien der Firmen Apple, Netflix, Meta, Amazon, Tesla und Google während der letzten sechs Wochen. Die aktuellen und historischen Kursdaten bezieht das Programm kurz nach dem Aufruf in einem Sekundenbruchteil vom Datendealer Twelvedata. Der bietet für Bastler einen kostenlosen Basic Plan an, der bis zu 8 Requests pro Minute und bis zu 800 am Tag erlaubt, bevor das Rate Limiting einschreitet.

Abbildung 1: Ein Screenshot mit der Ausgabe des Go-Programms Pofo.

Abbildung 1: Ein Screenshot mit der Ausgabe des Go-Programms Pofo.

Das Programm holt die Schlusskurse in US-Dollar der sechs überwachten Aktien an der New Yorker Börse für die vergangenen 45 Werktage ein. Das erledigt es in einer einzigen Anfrage an den Server, in einem gewaltigen Rutsch. Für die hypernervöse Netflix-Aktie zeigt Abbildung 2 die Kurse in der Zeitspanne zwischen dem 16. Juni und dem 31. Juli 2023. In dieser Periode schwankte der Wert der Aktie wild zwischen 413,17 und 477,59 US-Dollar. In einer Grafik der Absolutwerte könnte man trotzdem kaum Schwankungen ausmachen, denn schließlich macht die Differenz nur rund 15 Prozent des Gesamtwerts aus.

Abbildung 2: Kursentwicklung der Netflix-Aktie aus JSON-Daten.

Abbildung 2: Kursentwicklung der Netflix-Aktie aus JSON-Daten.

Die wilden Kurssprünge, die typische Chart-Apps zeigen (Abbildung 3), entsprechen daher nicht den Absolutwerten – sonst kämen sehr statische Blöcke heraus, die niemand vom Hocker hauen würden. Daher transformieren typische Chart-Anwendungen die Schwankungen in relative Werte, sodass einige Dollars gleich die gesamte Chart-Höhe ausmachen.

Abbildung 3: Die Aktien-App auf dem iPhone zeigt keine absoluten Werte an.

Abbildung 3: Die Aktien-App auf dem iPhone zeigt keine absoluten Werte an.

Frei nach Registrierung

Twelvedata rückt die aktuellen Kursdaten ausgewählter Aktien nur dann heraus, wenn man sich dort mit seiner E-Mail-Adresse registriert. Im Erfolgsfall erhält man dann einen API-Key (Abbildung 4), der jedem abgesetzten Request beiliegen muss. Der kostenlose Test-Account deckt den gesamten US-Aktienmarkt ab, eine Kreditkarte ist nur für kostenpflichtige Endpunkte erforderlich.

Abbildung 4: Der kostenlose API-Key für Kursdaten auf Twelvedata.com.

Abbildung 4: Der kostenlose API-Key für Kursdaten auf Twelvedata.com.

Möchte man allerdings Kurswerte des deutschen Aktienmarkts abfragen, etwa den Stand der Volkswagen-Aktie am Xetra-Markt (Symbol: VOW3:XETR), fällt eine nicht unerhebliche Gebühr an. Seit Yahoo vor mehr als fünf Jahren seine Finanz-API eingestampft hat, steht es schlecht um kostenlose Zugriffe auf deutsche Kurswerte. Das betrifft nicht nur Twelvedata, sondern auch alle von mir untersuchten APIs anderer Anbieter.

Listing 1 zeigt den Go-Code, der die historischen Kurse der in »symbols« hinterlegten, kommaseparierten Aktienkürzel vom Kurshändler Twelvedata abholt. Die Funktion »fetchQ()« ab Zeile 7 gibt im Erfolgsfall eine Hashmap zurück, die unter dem jeweiligen Tickersymbol (zum Beispiel aapl für Apple) einen Array-Slice von »dateVal«-Strukturen führt. Sie ordnen jeweils ein Datum (zum Beispiel 10-01-2023) einem Kurs (zum Beispiel 123.45678) zu.

Dazu baut der Code eine Anfrage an die Website von Twelvedata zusammen, der an die Basis-URL die Parameter »symbol« (die gewünschten Tickersymbole) und »interval« (Zeitabstände, im Beispiel ein Tag) sowie den vorher eingeholten API-Key anhängt. Zeile 19 holt mit »Get()« die Antwort aus dem Netz. Die in Zeile 27 aufgerufene Funktion »parse()« schnappt sich den zurückkommenden JSON-Salat (Abbildung 5) und verpackt das Ganze in die bereits erwähnte Go-Datenstruktur einer verschachtelten Hashmap.

Abbildung 5: Der JSON-Salat nach einer Kursabfrage an Twelvedata.

Abbildung 5: Der JSON-Salat nach einer Kursabfrage an Twelvedata.

Listing 1

quote.go

package main
import (
  "io/ioutil"
  "net/http"
  "net/url"
)
func fetchQ(symbols string) (map[string][]dateVal, error) {
  u := url.URL{
    Scheme: "https",
    Host:   "api.twelvedata.com",
    Path:   "time_series",
  }
  q := u.Query()
  q.Set("symbol", symbols)
  q.Set("interval", "1day")
  q.Set("apikey", "a1723ab98fa90ac307a0c5bf332d451c")
  u.RawQuery = q.Encode()
  res := map[string][]dateVal{}
  resp, err := http.Get(u.String())
  if err != nil {
    return res, err
  }
  body, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    return res, err
  }
  return parse(string(body)), nil
}

Wühlen in JSON

Bei dieser Struktur handelt es sich schlicht um ein Dictionary mit den Börsenticker-Symbolen, denen jeweils unter »values« ein Array zugewiesen wird. Dessen einzelne Elemente weisen jeweils einem Datum den Kurs der Aktie zum betreffenden Zeitpunkt zu. Uns interessiert hier nur der Wert »close«, also der tägliche Schlusskurs. Das Ganze ließe sich nun unproblematisch nach der konventionellen Methode anpacken, indem der Entwickler entsprechende Go-Datenstrukturen mit identischer Schachteltiefe definiert, die dann der in Go eingebaute JSON-Unmarshaler aus JSON nach Go importiert.

Stattdessen zieht Listing 2 das praktische Paket Gjson von Github, das die strenge Typprüfung von Go elegant ausschaltet und mit JQuery-artigen Anfragen die Nuggets aus dem JSON-Erdreich gräbt. Die resultierende Datenstruktur, eine Hashmap mit Einträgen aus Tickersymbolen, zeigt wiederum auf Array-Slices mit Tupeln aus Zeitstempeln und Kurswerten. Zeile 9 definiert sie unter dem Namen »qMap«. Die Funktion »parse()« ab Zeile 10 gibt als Ergebnis eine Variable dieses Datentyps an den Aufrufer zurück.

Listing 2

parse.go

package main
import (
  "github.com/tidwall/gjson"
)
type dateVal struct {
  date  string
  price string
}
type qMap map[string][]dateVal
func parse(data string) qMap {
  all := gjson.Get(data, "@this").Map()
  res := qMap{}
  for tick, _ := range all {
    dates := gjson.Get(string(data), tick+".values.#.datetime").Array()
    closes := gjson.Get(string(data), tick+".values.#.close").Array()
    series := []dateVal{}
    for i, date := range dates {
      series = append(series, dateVal{date: date.String(), price: closes[i].String()})
    }
    res[tick] = series
  }
  return res
}

Die Funktion »Get()« in Zeile 11 klappert mit dem Query »@this« die oberste Ebene der Daten ab, die Tickersymbole. »Map()« aus dem Gjson-Paket macht gleich eine Go-Map daraus. Daraufhin kann die For-Schleife in Zeile 13 über alle Tickersymbole iterieren. Der Query »[symbol].values.#.datetime« fieselt anschließend alle Zeitstempel der verfügbaren Datenpunkte heraus. »Array()« aus dem Gjson-Paket macht ein Array daraus. Für die unter »close« liegenden Schlusskurse funktioniert das Ganze analog. Die For-Schleife ab Zeile 17 mischt die beiden Arrays wieder zu einem Array von »dateVal«-Typen zusammen – fertig ist der Eintrag für das gerade bearbeitete Tickersymbol.

Fliesenleger

Nun muss das Hauptprogramm in Listing 3 die Einzelteile aus Listing 1 und Listing 2 zusammenleimen und die GUI aufsetzen. Als Grafikpaket für das Terminal hält wie schon in früheren Snapshots das unverwüstliche TermUI her, aus dem diesmal das Widget »BarChart« sowie der Kachelarrangierer »grid« zum Zug kommen. Wie in Abbildung 1 zu sehen, erstrecken sich die insgesamt sechs Kacheln über die gesamte Breite und Höhe des Terminals und teilen sich den Platz brüderlich.

Listing 3

pofo.go

package main
import (
  ui "github.com/gizak/termui/v3"
  "github.com/gizak/termui/v3/widgets"
  "strconv"
  "time"
)
func main() {
  s := "aapl,nflx,meta,amzn,tsla,goog"
  res, err := fetchQ(s)
  if err != nil {
    panic(err)
  }
  if err := ui.Init(); err != nil {
    panic(err)
  }
  defer ui.Close()
  charts := []*widgets.BarChart{}
  for s, _ := range res {
    charts = append(charts, mkChart(s, res))
  }
  grid := ui.NewGrid()
  termWidth, termHeight := ui.TerminalDimensions()
  grid.SetRect(0, 0, termWidth, termHeight)
  grid.Set(
    ui.NewRow(1.0/2,
      ui.NewCol(1.0/3, charts[0]),
      ui.NewCol(1.0/3, charts[1]),
      ui.NewCol(1.0/3, charts[2]),
    ),
    ui.NewRow(1.0/2,
      ui.NewCol(1.0/3, charts[3]),
      ui.NewCol(1.0/3, charts[4]),
      ui.NewCol(1.0/3, charts[5]),
    ),
  )
  ui.Render(grid)
  <-ui.PollEvents()
}
func mkChart(symbol string, res qMap) *widgets.BarChart {
  bc := widgets.NewBarChart()
  bc.Data = []float64{}
  bc.Labels = []string{}
  bc.BarWidth = 1
  bc.BarGap = 0
  vals := res[symbol]
  var min float64
  for i := len(vals) - 1; i >= 0; i-- {
    price, err := strconv.ParseFloat(vals[i].price, 64)
    if err != nil {
      panic(err)
    }
    bc.Data = append(bc.Data, price)
    if min == 0 || price < min {
      min = price
    }
    bc.Labels = append(bc.Labels, weekday(vals[i].date))
  }
 for i, _ := range bc.Data {
    bc.Data[i] -= min
  }
  bc.NumFormatter = func(f float64) string {
    return ""
  }
  bc.Title = symbol
  bc.BarWidth = 1
  bc.BarColors = []ui.Color{ui.ColorRed, ui.ColorGreen}
  return bc
}
func weekday(date string) string {
  dt, _ := time.Parse("2006-01-02", date)
  return string(dt.Weekday().String()[0])
}

Statt nun zu jedem einzelnen Widget dessen x/y-Koordinaten auszurechnen, vertraut Listing 3 auf den von TermUI bereitgestellten Fliesenleger »grid«. Dessen »Set()«-Funktion in Zeile 25 nimmt zwei Reihen von jeweils drei »BarChart«-Widgets entgegen, die alle nacheinander im vorher erzeugten Array »charts« liegen. Der Aufruf der TermUI-Funktion »Render()« in Zeile 37 nimmt dann als einzigen Parameter das »grid«-Widget entgegen und rechnet selbstständig aus, wo die einzelnen Kacheln zu liegen kommen.

FANG und mehr

Die vier zu überwachenden FANG-Aktien (Facebook aka Meta, Apple, Netflix, Google) sowie die Tickersymbole von Tesla und Amazon definiert Listing 3 in der Zeile 9 als kommaseparierte Zeichenkette. Der Aufruf von »fetchQ()« aus Listing 1 holt die Daten vom Provider ein und gibt die Map »res« zurück. Daraufhin iteriert eine For-Schleife (Listing 3, ab Zeile 19) über die Tickersymbole und ruft für jedes die Funktion »mkChart()« auf (ab Zeile 40).

Zeile 41 gleich zu Anfang von »mkChart()« erzeugt ein neues Widget mit einer Balkengrafik. Die Funktion gibt es am Ende mit Daten gefüllt und fertig zur Anzeige an den Aufrufer zurück. Die Balkengrafik-Funktion aus dem TermUI-Paket stellt die Zeitreihe der Zahlenwerte aus dem Feld »Data« dar und schreibt unter die Balken jeweils die in »Labels« definierten Strings. Wegen der dicht gedrängten Balken in den Stock-Charts dürfen die Werte auf der x-Achse allerdings nur einen Buchstaben lang sein. Daher ermittelt »weekday()« ab Zeile 70 jeweils den Wochentag des Kursdatums, nimmt davon den ersten Buchstaben und platziert ihn zur Anzeige in das Array im Feld »Labels«.

Die vom Anbieter verwendeten Zeitstempel haben das Format 2023-07-31. Die Funktion »Parse()« aus dem Go-Standardpaket time liest sie mit dem Template »2006-01-02« ein, um daraus ein »time«-Objekt zu machen, dem sich dann der Wochentag entnehmen lässt. Erfahrene Snapshot-Leser wissen, dass das dem ulkigen Zeitstempel-Parser von Go geschuldet ist. Er erwartet die Lage von Tag, Monat und Jahr im Template nicht in Form traditioneller Platzhalter, sondern nutzt die Werte eines willkürlich festgelegten Datums (2. Januar 2006, 15:04:05 – Hinweis zur Hausaufgabe: 1, 2, 3, 4, 5, 6).

Üblicherweise zeichnet das »BarChart«-Widget auch noch den y-Wert jedes Balkens in die Grafik, aber das wäre im Gedränge nicht mehr lesbar. Deshalb definiert Zeile 62 den verwendeten »NumFormatter« als eine Funktion, die nur eine leere Zeichenkette zurückgibt.

Nächste Fahrt rückwärts!

Die JSON-Antwort von Twelvedata enthält die Tageskurse absteigend nach dem Datum sortiert, liefert also die neuesten Kurse zuerst. Die Balkengrafik stellt sie allerdings zeitlich aufsteigend dar. Deshalb läuft die For-Schleife ab Zeile 48 rückwärts, um die Werte an das »BarChart«-Array »Data« anzuhängen.

Zur Relativierung der Kurse, die wie eingangs erwähnt für eine bessere Erkennbarkeit der Kursfluktuationen sorgt, ermittelt die erste For-Schleife den Minimalwert aller Tageswerte in »min« und zieht ihn in Zeile 60 von allen darzustellenden Werten ab. Auf diese Weise stellt die Balkengrafik die Kurse mit y-Werten zwischen dem Minimal- und dem Maximalkurs dar.

Listing 4

Binary erzeugen

$ go mod init pofo
$ go mod tidy
$ go build pofo.go quote.go parse.go

Zum Übersetzen und Binden der Go-Sourcen dient der Dreisprung aus Listing 4, der ein Binary namens »pofo« erzeugt. Ohne Parameter aufgerufen, schaltet es das Terminal in den Grafikmodus, und nach einem Sekundenbruchteil stehen die Balkendiagramme als Kacheln in der Anzeige.

Ausblick

Das kleine Programm setzt lediglich eine Internet-Verbindung sowie ein gültiges API-Token für Twelvedata voraus. Letzteres sollte in Produktionsumgebungen in einer externen Konfigurationsdatei statt im Listing stehen. Die zu überwachenden Tickersymbole lagern Sie für einen besseren Bedienkomfort tunlichst in eine YAML-Datei aus. Mögen die Kurse steigen!

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben