Finanzen: Aktienkurse per Go-Programm überwachen
Laut US-Vizepräsidentin Kamala Harris ginge der Mehrheit der Amerikaner das Geld aus, wenn nur 400 US-Dollar unerwartete Kosten anfielen. Seit ich das weiß, 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 es, 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.
Das Bild unten zeigt die Ausgabe des Go-Programms Pofo – das Kürzel steht für Portfolio. In sechs Kacheln veranschaulicht jeweils eine Balkengrafik den Kursverlauf der Aktien der Firmen Apple, Netflix, Meta, Amazon, Tesla und Google während der vorherigen 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 acht Requests pro Minute und bis zu 800 am Tag erlaubt, bevor das Rate Limiting greift.
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 das Bild unten die Kurse in der Zeit zwischen dem 16. Juni und dem 31. Juli 2023. In dieser Periode schwankte der Wert dieser Aktie wild zwischen 413,17 und 477,59 US-Dollar. In einer Grafik der Absolutwerte könnte man trotzdem kaum Schwankungen erkennen, denn schließlich macht die Differenz nur rund 15 Prozent des Gesamtwerts aus.
Die wilden Kurssprünge, die typische Chart-Apps zeigen (siehe Bild unten), entsprechen daher nicht den Absolutwerten – sonst kämen sehr statische Blöcke heraus, die niemanden vom Hocker hauen würden. Daher transformieren typische Chartanwendungen die Schwankungen in relative Werte, so dass wenige US-Dollars gleich die gesamte Charthöhe ausmachen.
Deutsche Kursdaten sind teuer
Twelvedata gibt die aktuellen Kursdaten ausgewählter Aktien nur dann heraus, wenn man sich dort mittels E-Mail-Adresse registriert. Im Erfolgsfall erhält man einen API-Key (Abbildung 4), der jedem abgesetzten Request beiliegen muss.
Der kostenlose Test-Account deckt den US-Aktienmarkt ab, eine Kreditkarte ist nur für kostenpflichtige Endpunkte erforderlich.
Möchte man 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 (siehe unten) 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 zu (zum Beispiel 123.45678).
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
}
Dazu baut der Code eine Anfrage an die Webseite 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 (siehe Bild oben) und verpackt das Ganze in die bereits zuvor erwähnte Go-Datenstruktur einer verschachtelten Hashmap.
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 nur der Wert close , also der tägliche Schlusskurs. Das Ganze ließe sich unproblematisch nach der konventionellen Methode anpacken, indem Entwickler entsprechende Go-Datenstrukturen mit identischer Schachteltiefe definieren, die der in Go eingebaute JSON-Unmarshaler aus JSON nach Go importiert.
Stattdessen zieht Listing 2 (siehe unten) das praktische Paket Gjson von Github, das die strenge Typprüfung von Go 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 eine Variable dieses Datentyps 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 [Formulierung], 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].va‐ lues.#.datetime fieselt anschließend alle Zeitstempel der verfügbaren Datenpunkte heraus. Array() aus dem Gjson-Paket macht ein Array daraus. Das Ganze funktioniert für die unter close liegenden Schlusskurse analog.
Die For-Schleife ab Zeile 17 mischt die zwei Arrays wieder zu einem einzigen Array von dateVal-Typen zusammen – fertig ist der Eintrag für das gerade bearbeitete Tickersymbol.
Fliesenleger
Nun muss das Hauptprogramm in Listing 3 (siehe unten) 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.
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])
}
Wie im Bild unten zu sehen ist, erstrecken sich die sechs Kacheln über die gesamte Breite und Höhe des Terminals und teilen sich den Platz brüderlich. 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 zuvor 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 und Google) und 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). Gleich zu Anfang von mkChart() erzeugt Zeile 41 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 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 week‐ day() ab Zeile 70 jeweils den Wochentag des Kursdatums, nimmt den ersten Buchstaben davon 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, aus dem sich der Wochentag ablesen lässt.
Das ist dem ulkigen Zeitstempel-Parser von Go geschuldet. 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 Datum sortiert, liefert also die neuesten Kurse zuerst. Die Balkengrafik stellt sie allerdings zeitlich aufsteigend dar. Aus diesem Grund 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 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.
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.
Das Programm setzt nur eine Internetverbindung 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 lagere ich für einen besseren Bedienungskomfort in eine YAML-Datei aus. Mögen die Kurse steigen!
Der Artikel erschien zuerst im Linux-Magazin, Ausgabe 10/2023(öffnet im neuen Fenster) .
- Anzeige Hier geht es zu Go - Das Praxisbuch bei Amazon Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.



