Meinem Artikel zu Investment Portfolio kannst du entnehmen, dass ich einen kleinen Teil meiner Reserven in Edelmetalle wie Gold oder Palladium investiere. Da ich keine Lust habe, mich um die Lagerung und Sicherheit des Physikalischen Assets zu kümmern, habe ich nach einem reinen digitalen Produkt gesucht. Die Gesamtsumme ist im Moment einfach noch zu klein für Schliessfach o.Ä.
Während meiner Suche nach dem richtigen Produkt, bin ich über das Angebot von Bitpanda Metals gestossen. Der Vorteil des Produkts besteht darin, dass man Gold, Solber, Palladium und Platinum ab bereits einem Euro kaufen kann. Ausserdem sind die ersten 20g jedes Edelmetalls gebührenfrei. Danch kostet das Halten jedes Assets von 0.65% und 1.3% des Gesamtvolumens pro Jahr. Für kleinere Beträge, wie in meinem Fall, also perfekt; für höhere Beträge aber für meinen Geschmack zu teuer.
Um den Erfolg meines Portfolios tracken und beobachten zu können, nutze ich die freie Software Portfolio Performance. In eben dieser können Kurse für Wertpapiere und andere Assets per API, Tabelle einer Website, o.Ä. importiert werden. Dazu ist es aber wichtig, dass die Historischen Daten herangezogen werden, um die gesamte Performace des Assets ermitteln zu können. Zwar können die Kurse auch aus Buchungen erstellt werden, allerdigs hat dies keinen Effekt mehr, wenn der Kauf ausgesetzt wird. Und jeden Monat die Kurse manuell einzutragen hatte ich schlicht und einfach keine Lust. Ich bin ja Software Entwickler – ergo einfach zu faul!
Für die spezifischen Bitpanda Metals existiert jedoch keine Tabelle, die diese Daten vorhalten würde. Einzig einen Ticker in der offiziellen API habe ich gefunden, der zeigt jedoch immer nur den aktuellen Kurs an. Daraus ist die Idee entstanden, einen kleinen Service bei AWS zu bauen, der diesen Ticker periodisch abfragt und die aktuellen Kurse in eine Datenbank persistiert. Somit erhalte ich eine Historie der einzelnen Assets, startend ab dem Zeitpunkt der ersten Abfrage. Der Payload des Tickers am Beispiel von Bitpanda Gold (XAU) sieht dabei folgendermassen aus:
{
"XAU": {
"EUR":"50.68",
"USD":"57.19",
"CHF":"52.92",
"GBP":"43.14",
"TRY":"789.39"
},
{...}
}
Da ich in Portfolio Performace Schweizer Franken als Hauptwährung angegeben hatte, ist für mich auch nur der CHF
Datensatz relevant. Die anderen Informationen sind dabei nur Speicherverschwendung.
Für diesen einfachen Fall habe ich mich dazu entschieden, nur die AWS Managed Services zu nutzen. Wenn kein Server eingerichtet werden muss können erstens Kosten gesenkt werden, zweitens ist mir der Aufwand zu gross, ein Linux System für diesen Zweck abzusichern und zu warten.
Da nur Ticker-Daten abgegriffen werden können, muss die API periodisch abgefragt werden. Um die Ziel-API jedoch nicht zu überlasten oder gar blockiert zu werden, ist es an dieser Stelle ratsam, diesen Vorgang nicht zu oft zu wiederholen. Aus diesem Grund habe ich mich für ein Aktualisierungsintervall von 15 Minuten entschieden. Der Cronjob selbst kann in der AWS EventBridge realisiert werden. Eigentlich ist dieser Service dazu gedacht, auf jegliche Events von AWS Services zu reagieren. Dieser lässt sich jedoch auch so konfigurieren, dass eine zeitliche Abfolge möglich ist.
Das Sammeln der Tickerdaten übernimmt dann eine AWS Lambda Funktion, die von der EventBridge angestossen wird. Dabei handelt es sich um einen Managed Service, der Code Fragmente in unterstützten Sprachen in sich geschlossen ausführen kann. Das ist eine sehr effiziente Variante, da nur die Ausführungszeit an sich berechnet wird. Läuft die Funktion nicht, so kostet diese auch keinen müden Rappen.
Die Daten die über die Lambda gesammelt wurden, werden in eine NoSQL Datenbank gespeichert. Dabei habe ich mich für den Service DynamoDB entschieden, der einen Key-Value-Store abbildet. Dabei wird keine Grundgebühr erhoben, sondern es werden nur die Transfers berechnet. Da die Kurse in Portfolio Performance tagesaktuell sein sollen, reicht hierbei eine Auflösung auf Tage.
Da in Portfolio Performance auch HTTP-APIs angegeben werden können, wird mittels AWS ApiGateway eine REST Schnittstelle geschaffen. Diese greifft direkt auf die DynamoDB zu. Für diese Funktionalität wird keine Lambda Funktion mehr benötigt, da dies bereits im ApiGateway nativ implementiert ist. Auch dies spart wieder Kosten und Entwicklungsaufwand.
In der Architektur hast du gesehen, dass die Datenbank Tabelle das zentrale Element der Lösung ist, weshalb ich mit DynamoDB beginne. Hierbei erstellt man nicht Datenbanken oder gar Serverinstanzen, sondern lediglich Tabellen. Genau wie bei SQL-Datenbanken muss ein Primary Key gesetzt werden, hier Partition key genannt. Da aus dem Datensatz kein eindeutiger Identifier herauszulesen ist, verwende ich eine UUID4. Der Sort key kann in diesem Fall leer bleiben. Ein Schema wird in diesem Schritt übrigens nicht angegeben. Bei der DynamoDB handelt es sich um einen Key-Value Store, deren Felder in der Runtime bestimmt werden können. Dies hat in der Entwicklung erhebliche Vorteile, da man schneller auf Veränderungen reagieren oder verschiedene Datensätze in einer Tabelle ablegen kann. Hätte ich bei einem Asset zum Beispiel ein Tageshoch und -tief, kann ich dies ohne grosse Update Zyklen einfach implementieren.
Für die spätere Verwendung des ApiGateway erstelle ich nich ein expliziter Index, der nach Asset filtern und nach Datum sortieren kann.
Alle anderen Einstellungen habe ich wie von AWS vorgeschlagen so belassen. Sowohl die Global Tables wie auch Anpassungen in den Capacity Einstellungen sind für meinen Fall nicht relevant. Die Verschlüsselung der Tabelle habe ich aber aktiviert gelassen, auch wenn die Daten nicht wirklich shützenswert sind, da diese öffentlich gesammelt wurden.
Damit die eben erstellte Tabelle auch gefüllt wird, habe ich eine Lambda Funktion konzipiert, die alle Preise gewünschten Assets sammelt. Da die Bitpanda Ticker Daten alle verfügbaren Assets enthällt, muss die API glücklicherweise nur ein mal abgefragt werden. Somit ist die Anzahl der gewünschten Assets irrelevant. So wird ein Blockieren durch die Bitpanda API vermieden.
Da die Funktion alle vier Stunden ausgeführt wird, um eine gewisse Aktualität erreichen zu können, muss aber unterschieden werden, ob für den entsprechenden Tag bereits ein Eintrag vorhanden ist. Ist dies der Fall so wird der bestehende Eintrag aktualisiert. Andernfalls wird ein Neuer erstellt. An dieser Stelle wäre noch die Möglichkeit vorhanden, das Tageshoch und -tief zu ermitteln. Der gasamte Funktions Code sieht also wie folgt aus.
import json
import requests
import uuid
from boto3.session import Session
from enum import Enum
from datetime import datetime
from decimal import Decimal
session: Session = Session(region_name='eu-central-1')
ddb = session.resource('dynamodb')
table = ddb.Table('bitpanda-metal-prices')
class Metal(Enum):
GOLD = 'XAU'
PLATINUM = 'XPT'
PALLADIUM = 'XPD'
SILVER = 'XAG'
def lambda_handler(event, context):
r = requests.get(url='https://api.bitpanda.com/v1/ticker')
response = r.json()
today = datetime.now().strftime('%Y-%m-%d')
for metal in Metal.__members__:
asset: str = Metal[metal].value
current_entry_count: list = table.scan(
ScanFilter={
'date': {
'AttributeValueList': [today],
'ComparisonOperator': 'EQ'
},
'asset': {
'AttributeValueList': [asset],
'ComparisonOperator': 'EQ'
}
},
ConditionalOperator='AND'
)['Items']
if len(current_entry_count) == 0:
table.put_item(
Item={
"uuid": str(uuid.uuid4()),
"date": today,
"asset": asset,
"price": Decimal(response[asset]['CHF'])
}
)
print("Set " + asset + " price to " + response[asset]['CHF'])
else:
table.update_item(
Key={
'uuid': current_entry_count[0]['uuid']
},
UpdateExpression='SET price=:price',
ExpressionAttributeValues={
':price': Decimal(response[asset]['CHF']),
},
)
print("Changed " + asset + " price to " + response[asset]['CHF'])
Wie du sehen kannst, enthält der fertige Datensatz die Felder uuid, date, asset und price. Zwar wurden diese nicht in einem Schema vordefiniert, trotzdem muss DynamoDB mitgeteilt werden, um welchen Datentyp es sich dabei handelt. Da ich diesem Fall die AWS SDK, für Python 3 boto3, verwendet habe, werden alle primitiven Datentypen korrekt umgewandelt. Nur die Dezimalzahl musste ich hierbei speziell formatieren. Die dafür notwendige Bibliothek ist jedoch bereits in Python 3 integriert.
Damit die Funktion nicht von Hand ausgeführt werden muss, kommt zu diesem Zweck ein Cronjob zum Einsatz. Dieser wird so eingestellt, dass die Lambda Funktion alle vier Stunden ausgeführt wird. Zum Zweck der täglichen Kurssammlung ist dies mehr als ausreichend und sollte angepasst werden, wenn die Granularität der Zeitspanne verändert wird.
Als Target wird die zuvor erstellte Lambda Funktion ausgewählt. Da in diesem Fall keine Input Parameter notwendig sind, wird als Event Input ein leeres JSON Objekt übergeben. Natürlich kann dies bei Notwendigkeit ebenfalls angepasst werden.
Zu beachten ist allerdings, dass der Cronjob aktiv ist, sobald diese Konfiguration gespeichert wird. Es kann also während der Entwicklung dazu kommen, dass Error Meldungen in den Logs auftauchen.
Um die gespeicherten Daten nun zugänglich machen zu können, wird eine Schnittstelle eingerichtet, die die gesammelten Daten zur Verfügung stellt. Mit AWS ApiGateway kann eine REST API rasch und unkompliziert erstellt werden. Ausserdem ist es möglich, ohne Umwege direkt die AWS DynamoDB anzusprechen.
Somit wird eine neue Ressource «assets» angelegt, die als Pfad Variable das zu suchende Asset entgegennimmt. Nach REST wurde hierbei die GET-Methode gewählt. Die Asset-Information besteht aus dem Kürzel, mit dem die entsprechenden Datenpunkte auch in der Datenbank abgelegt wurden. Somit habe ich die Möglichkeit, nich alle Einträge zurückgeben zu müssen, sondern kann vorgängig bereits über diesen Parameter die Resultate filtern.
{
"TableName": "bitpanda-metal-prices",
"IndexName": "asset-date-index",
"KeyConditionExpression": "asset = :asset",
"Limit": 30,
"ScanIndexForward": false,
"ExpressionAttributeValues": {
":asset": {
"S": "$input.params('asset')"
}
}
}
Damit die Schnittstelle die Resultate direkt auf die AWS DynamoDB zugreiffen kann, muss ein Request Mapping Template definiert werden. Hierbei habe ich eingestellt, dass nur die letzten 30 Resultate vom definierten Asset zurückgegeben werden sollen. Da ich Portfolio Performance mehrmals im Monat öffne, reicht diese Auflösung völlig aus.
#set($inputRoot = $input.path('$'))
{
"data": [
#foreach($elem in $inputRoot.Items) {
"asset": "$elem.asset.S",
"date": "$elem.date.S",
"price": "$elem.price.N"
}#if($foreach.hasNext),#end
#end
]
}
Um die Ausgabe zu formatieren und unnötige Informationen zu elimieren, habe ich ein Execution Mapping Template erstellt. Dieses definiert für jeden in der DB gefundenen Eintrag, dass nur die Informationen «asset», «date», und «price» zurückgegeben werden.
Rufe ich meine API nun mit dem Asset «XAU» (Bitpanda Gold) auf, werden mir die gewünschten Informationen zurückgegeben.
https://meineapiarn.execute-api.eu-central-1.amazonaws.com/live/assets/XAU
{
"data": [
{
"asset": "XAU",
"date": "2021-12-07",
"price": "53.12"
},
{...}
]
}
Um die neue Schnittstelle nun auch in Portfolio Performance nutzen zu können, muss ein neues leeres Wertpapier angelegt werden. Bei den Historischen Kursen besteht nun die möglichkeit, eine JSON Schnittstelle zu nutzen. Die Kurs URL ist dabei identisch wie oben angegeben. Bitte beachte aber, dass auch wirklich nur ein Asset zurückgegeben wird, da es ansonsten zu einem Datenchaos kommen kann.
Die einzelnen Werte können dann zugeordnet werden. Dabei ist eine Notation von Portfolio Performance zu beachten, die die einzelnen Informationen aus dem JSON Objekt extrahieren kann.
Wurde alles sauber konfiguriert, kann der Kurs des Assets nach einigen Monaten sauber nachvollzogen werden. Somit können nun auch Berechnungen zu Profit und Verlust angestellt werden. Ausserdem aktualisiert das Tool die Kurse automatisch, was eine bessere Übersicht über das eigene Vermögen ermöglicht.
Nach einigen Monaten der Nutzung, hat sich die Ressourcen-Belastung nun auf ein Optimum eingependelt. Aufgrund der Architektur und das Setzen auf AWS Services ist die Kostenbelastung für das ganze Projekt minimal.
Aufgrund der geringen Zugriffszahlen, ist die Nutzung des ApiGateways in meinem Fall kostenlos.
Da die DynamoDB stetig mit Daten gefüttert und somit auch niemals kleiner wird, steigen hierbei die Kosten stetig an. Für 4011 Einträge in der Datenbank zahlte ich so im Mai 2022 allerdings lediglich $0.07.
AWS bietet für Lambda ein Free Kontingent für einige Ausführungszeit an. Aufgrund meiner geringen Ausführungszeit komme ich dabei aber nicht über dieses Kontingent hinaus und bezahle auch wieder nichts.
Auch ein Riesentool wie AWS kann sich für private Projekte lohnen. Die Kostenbelastung kann dabei sogar geringer ausfallen, wie wenn ein Server in die eigenen vier Wände gestellt wird. Aufgrund der fertigen AWS Services ist es auch verhältnismässig einfach, komplexere Systeme aufzubauen.
Allerdings sollten die Kosten immer beobachtet werden. Das fiese einer nutzungsbasierten Abrechnung ist, dass diese fast unbemerkt in die Höhe schnellen kann. Es ist also ratsam, über Budgets oder sonstige technische Einschränkung die Nutzung zu steuern. Ich selbst habe für den gesamten AWS Account ein Budget von $50.- eingestellt. Ist dieses Limit erreicht erhalte ich eine Meldung und kann so relativ rasche eingreiffen. Derzeit komme ich aber wie gesehen nicht annähernd auf diesen Betrag.
The post AWS Cloud privat verwenden? Ist das sinnvoll? first appeared on Lorrx.org.]]>