Aus Linux-Magazin 03/2008

Den zeitlichen Wertverlauf eines Aktienportfolios grafisch darstellen

Zahlenreihen taugen vielleicht für gelangweilte Buchhalter, nicht aber für Programmierer. Die bevorzugen Grafiken, besonders wenn sie den Wertverlauf ihres Aktienportfolios beschreiben. Das vorgestellte Perl-Skript zeichnet Turmdiagramme und hilft die Performance im Auge zu behalten.

“Entscheidend ist, was hinten rauskommt”, bemerkte schon Helmut Kohl [1]. Auch bei der Vermögensanlage zählt nicht, was eine einzelne Aktie im Depot treibt, sondern wie sich ein Portfolio entwickelt. Wer das Risiko streuen will, sorgt dafür, dass die Einzelposten aus möglichst diversifizierten Branchen stammen. Die großen Finanzseiten im Web zeigen zwar ansprechende Grafiken der Kursentwicklung einzelner Posten oder vergleichen zwei Wertpapiere, bieten aber kein Tool an, das die Kursentwicklung aller Posten eines Portfolios auf einen Blick zeigt.

Ein in Perl geschriebenes Skript schafft Abhilfe. Abbildung 1 zeigt die Konfigurationsdatei »pofo1.txt« eines Portfolios im Texteditor. Jede Zeile beschreibt einen Kauf (»in«) oder einen Verkauf (»out«) eines Aktienpostens, beginnend mit dem Datum in ISO-Schreibweise, danach die Transaktionsart, das Tickersymbol und die Anzahl. Auch Bargeldtransaktionen nimmt die Datei auf, statt des Tickersymbols steht hier »cash«, als Aktion verwendet es »chk« (Check).

Abbildung 1: Ein Anleger investierte im Januar 2007 ganze 20 000 Dollar in sein Depot (erste Zeile). Mit dem Geld kaufte er dann eine Mischung aus Internetaktien (die folgenden vier Zeilen).

Abbildung 1: Ein Anleger investierte im Januar 2007 ganze 20 000 Dollar in sein Depot (erste Zeile). Mit dem Geld kaufte er dann eine Mischung aus Internetaktien (die folgenden vier Zeilen).

Tippfaul

Damit die Depotverwaltung nicht zur sinnfreien Tipparbeit gerät, berechnet das Skript selbstständig die Kosten und Erlöse der Aktientransaktionen zum jeweiligen Tageskurs und passt den Bargeldbestand an. Gebühren berücksichtigt es nicht – wer die Abweichung ausgleichen will, korrigiert den Saldo manuell mit einem »cash«-Eintrag und der Aktion »chk«. Das Tool verfolgt neben den in den Grafiken gezeigten US-Aktien auch deutsche Wertpapiere, »SIE1.de« ist zum Beispiel das Symbol der Siemens-Aktie an der Xetra-Börse in Euro.

Im Portfolio in Abbildung 1 lagen also am 1.1.2007 genau 20 000 Dollar Bargeld (oder Euro, je nach Währung der Aktien im Depot). Neun Tage später hat sein Besitzer 50 Amazon-, 20 IBM-, 10 Google- und 200 Motorola-Aktien jeweils zum Tageskurs erworben. Während des restlichen Jahres ließ der Anleger seine Finger vom Depot. Die Kursentwicklung dieser vier Aktienpakete stellt Abbildung 3 dar. Während die Amazon- und die Google-Aktien kräftig anzogen, schwächelte Motorola und das Gesamtergebnis am Jahresende litt leicht darunter. In Summe verbuchte das Portfolio jedoch einen leichten Gewinn.

Glückliches Händchen

Anders der Inhaber des Depots in Abbildung 2: Auch er investierte zu Jahresanfang 20 000 Dollar und erwarb sofort 200 Aktien der amerikanischen Drogeriekette CVS (nicht zu verwechseln mit dem gleichnamigen Versionskontrollsystem). Gut eine Woche später kaufte der Spekulant 150 Amazon-Aktien, die er bereits vier Monate später wieder losschlug. Im September sah er einen Kursschub der Google-Aktie voraus und deckte sich mit 30 Stück ein.

Abbildung 2: Dieser aktive Anleger wechselte die Positionen seines Portfolios mehrmals im Jahr. Käufe sind an der Aktion »in« zu erkennen, Verkäufe sind mit »out« bezeichnet.

Abbildung 2: Dieser aktive Anleger wechselte die Positionen seines Portfolios mehrmals im Jahr. Käufe sind an der Aktion »in« zu erkennen, Verkäufe sind mit »out« bezeichnet.

Der Graph in Abbildung 4 offenbart einen deutlich höheren Gewinn und zeigt auch, dass das Stapeln der Aktienverläufe zu verwirrenden Verschiebungen führt. Die Reihenfolge der Einzelposten bleibt immer gleich. Am Kauf- oder Verkaufstag größerer Posten springen deren Balken aber, besonders der pinkfarbene Bereich am Boden, der den Bargeldbestand symbolisiert.

Die Depotgraphen beider Anleger (Abbildungen 3 und 4) stammen vom Perl-Skript »pofo« (Listing 2), das die Daten der Spekulationskäufe aus Konfigurationsdateien bezieht. Wie das Skript funktioniert steht weiter unten im Artikel. Die Eingabefiles sind in den Abbildungen 1 und 2 zu sehen. Das Skript nimmt sie auf der Kommandozeile entgegen und »pofo pofo1.txt« schreibt nach einer Denkpause die Bilddatei »positions.png« mit dem Graphen.

Abbildung 3: Das per Perl-Skript aus den Daten in Abbildung 1 erzeugt Diagramm stapelt den Kursverlauf jeder einzelnen Aktie aufeinander. Damit ist sowohl die Entwicklung des Depots zu sehen als auch die Performance jedes Einzelpostens.

Abbildung 3: Das per Perl-Skript aus den Daten in Abbildung 1 erzeugt Diagramm stapelt den Kursverlauf jeder einzelnen Aktie aufeinander. Damit ist sowohl die Entwicklung des Depots zu sehen als auch die Performance jedes Einzelpostens.

Abbildung 4: Der Depotbesitzer aus Abbildung 2 beweist ein glückliches Händchen und erspielte am Ende des Jahres einen erklecklichen Gewinn. Die Sprünge in den Kurven entstehen durch An- und Verkäufe.

Abbildung 4: Der Depotbesitzer aus Abbildung 2 beweist ein glückliches Händchen und erspielte am Ende des Jahres einen erklecklichen Gewinn. Die Sprünge in den Kurven entstehen durch An- und Verkäufe.

Tagesaktuell

Für jeden Tag im Graphen ermittelt Pofo die Zusammensetzung des Portfolios, holt die Tageskurse und multipliziert sie mit den Stückzahlen. Das eigentlich zur Darstellung von Netzwerkverkehr und Rechnerauslastung gedachte RRD-Tool (Round Robin Database, [2]) gießt die Tagesdaten in eine übersichtliche Turmgrafik. Den verschiedenen Aktien ordnet es aus einer vorgegebenen Palette Farben zu und erklärt diese Zuweisungen in einer Legende am unteren Rand.

Die historischen Tageskurse aller bekannten Aktien sind online verfügbar, allerdings wäre das Skript unerträglich langsam, falls es diese tatsächlich einzeln für jeden dargestellten Tag einholen würde. Stattdessen bemüht es lieber das Modul »CachedQuote« aus Listing 1. Bei der ersten Frage nach dem Tageskurs einer Aktie holt es gleich alle Kurse in einem Zeitfenster herein, das von einem Jahr in der Vergangenheit bis zum aktuellen Datum reicht. Die nicht sofort gebrauchten Werte lagert es in einer SQLite-Datenbank [3].

Listing 1:
»CachedQuote.pm«

001 ###########################################
002 package CachedQuote;
003 # Cache stock closing prices
004 # Mike Schilli, 2007 (m@perlmeister.com)
005 ###########################################
006 use strict;
007 use warnings;
008 use Cache::Historical;
009 use Log::Log4perl qw(:easy);
010 use Finance::QuoteHist::Yahoo;
011 
012 ###########################################
013 sub new {
014 ###########################################
015   my($class, %options) = @_;
016 
017   my $self = {
018     file => "/tmp/cached-quote.dat",
019     %options,
020   };
021 
022   $self->{cache} = Cache::Historical->new(
023         sqlite_file => $self->{file});
024 
025   bless $self, $class;
026 }
027 
028 ###########################################
029 sub quote {
030 ###########################################
031   my($self, $date, $key) = @_;
032 
033   my $quote = $self->{cache}->get(
034           $date, $key);
035 
036   return $quote if defined $quote;
037   $self->quote_refresh( $date, $key );
038 
039   return $self->{cache}->get_interpolated(
040             $date, $key);
041 }
042 
043 ###########################################
044 sub quote_refresh {
045 ###########################################
046   my($self, $date, $symbol) = @_;
047 
048   my($from, $to) =
049     $self->{cache}->time_range($symbol);
050 
051   my $upd = $self->{cache}->
052              since_last_update($symbol);
053 
054     # Date available, no refresh
055   if(defined $to and defined $from and
056      $date <= $to and $date >= $from) {
057       DEBUG "Date within, no refresh";
058       return 1;
059   }
060 
061   if(defined $date and defined $to and
062      defined $upd and $date > $to and
063      $upd->delta_days < 1) {
064       DEBUG "Date ($date) above cached",
065        " range ($from-$to), but cache ",
066        "is up-to-date.";
067       return 1;
068   }
069 
070   my $start = $date->clone->subtract(
071                             years => 1 );
072   if(defined $start and defined $from and
073      $start > $from and $to > $start) {
074         # no need to refresh old data
075       $start = $to;
076   }
077 
078   $self->quotes_fetch(
079     $start,
080     DateTime->today(),
081     $symbol);
082 }
083 
084 ###########################################
085 sub quotes_fetch {
086 ###########################################
087   my($self, $start, $end, $symbol) = @_;
088 
089   DEBUG "Refreshing $symbol ",
090         "($start - $end)";
091 
092   my $q = Finance::QuoteHist::Yahoo->new(
093     symbols    => [$symbol],
094     start_date => date_format($start),
095     end_date   => date_format($end),
096   );
097 
098   foreach my $row ($q->quotes()) {
099     my($symbol, $date, $open, $high, $low,
100        $close, $volume) = @$row;
101 
102     $self->{cache}->set( dt_parse($date),
103                   $symbol, $close );
104   }
105 }
106 
107 ###########################################
108 sub date_format {
109 ###########################################
110   my($dt) = @_;
111   return $dt->strftime("%m/%d/%Y");
112 }
113 
114 ###########################################
115 sub dt_parse {
116 ###########################################
117   my($string) = @_;
118   my $fmt =
119       DateTime::Format::Strptime->new(
120         pattern => "%Y/%m/%d");
121   $fmt->parse_datetime($string);
122 }
123 
124 1;

Geschwindigkeitsschub

Fordert der Client – wie vorausgesehen – tatsächlich den nächsten Tageskurs an, dann liest »CachedQuote« den Wert einfach aus seinem Datenbankspeicher statt ihn erneut übers Netz zu laden. Der Client bemerkt davon nichts, außer dass das Modul nachfolgende Requests um ein Vielfaches schneller beantwortet.

Falls ein Kunde den Kurs einer Aktie an einem Sonntag einholt, stellt »CachedQuote« fest, dass die geforderten Daten zwar im Zeitfenster liegen, aber für diesen Tag kein Kurs vorhanden ist, da die Börsen der Welt an Wochenenden und Feiertagen nicht arbeiten. In diesem Fall ist »CachedQuote« so schlau, statt eines Datenlochs den letzten vorliegenden Kurs auszuliefern.

Aktienkurs

Das Modul »CachedQuote.pm« holt die Aktienkurse mit dem CPAN-Modul Finance::QuoteHist::Yahoo vom Netz (Listing 1, Zeile 10 sowie 92 bis 104). Als Tageswert nimmt der Cache immer den mit »close« bezeichneten Schlusskurs, den er in der Variablen »$close« ablegt. Pro Web-Request kann der kontaktierte Yahoo-Server für eine Aktie die Kursdaten vieler Jahre liefern. Das nutzt »CachedQuote.pm« und schickt einen Request an den Server, der die Daten von einem Jahr vor dem verlangten Zeitpunkt bis zum aktuellen Datum umfasst (Zeilen 70 und 80). Liegen schon Daten im Cache, verzichtet das Modul auf einen erneuten Request (Zeile 36).

Zum Speichern und späteren Wiederfinden der Kursdaten verwendet »CachedQuote.pm« das CPAN-Modul Cache::Historical. Es bietet mit »set(Datum, Schlüssel, Wert)« eine komfortable Schnittstelle zum Setzen datumsbasierter Werte und liefert die gespeicherten Werte mit den Methoden »get(Datum, Schlüssel)« und »get_interpolated(Datum, Schlüssel)« wieder zurück. Der Parameter »Schlüssel« funktioniert dabei wie der Key in einem Hash. Fehlt ein Kurs für einen Tag, greift »get_interpolated()« auf den letzten verfügbaren Kurs vor dem angegebenen Datum zurück.

Leichte Datenbank

Cache::Historical verwendet hinter den Kulissen eine SQLite-Datenbank, auf die es mit dem Modul DBD::SQLite zugreift. SQLite ist keine freie Software, sondern steht unter einer Public-Domain-Lizenz und das CPAN-Modul liefert kurzerhand den Sourcecode der dateibasierten Datenbank gleich mit. Charmanterweise erlaubt SQLite Abfragen in SQL-Syntax, verzichtet jedoch auf einen Datenbankserver und schreibt stattdessen direkt in eine lokale Datei.

»CachedQuote.pm« stellt die SQLite-Datenbankdatei in Zeile 18 auf »/tmp/cached-quote.dat« ein. Wer den Cache nicht im gefährlichen temporären Verzeichnis haben möchte, überschreibt die Vorgabe im Aufruf des Konstruktors mit »new(sqlite_file => “Dateiname”)«.

Auf Kurs

Die Funktion »quote()« (Zeile 29) versucht zunächst, den geforderten Kurs mit »get()« einzuholen (Zeilen 33 bis 34). Schlägt diese Aktion fehl, dann hinterlässt die Get-Methode einen undefinierten Wert, was Zeile 36 erkennt. Das Modul versucht anschließend per »quote_refresh()«, den Cache rund um das geforderte Datum aufzufrischen. Anschließend sollte »get_interpolated()« in jedem Fall einen ordentlichen Wert zurückliefern.

Zudem unterscheidet der Code, ob ein Tageskurs nur nicht verfügbar ist, weil die Börse an diesem Tag geschlossen war, oder ob der Bereich noch nicht im Cache liegt. Falls das ansteuernde Skript an einem Sonntag läuft, soll das Modul nicht jedes Mal versuchen die neuesten Kurse vom Server zu holen, denn es wird bis zum Montag keine neuen geben.

Die Funktion »quote_refresh()« prüft daher in Zeile 52 mit »since_last_update()« die Zeitspanne seit dem letzten Auffrischen des Cache. Sie liegt als DateTime::Duration-Objekt vor und die Methode »delta_days()« rechnet diese in ganze Tage um. Ist der Cache noch keinen Tag alt, entfällt dieses Update (Zeilen 51, 52 sowie 61 bis 67) und der letzte verfügbare Kurs (üblicherweise vom Freitag) kommt zum Einsatz (Interpolate-Methode in Zeile 39).

Datum und Zeit

Das Interface des DateTime-Moduls vom CPAN ist so komfortabel, dass man eigentlich nichts anderes nutzen möchte – doch das zum Kursholen genutzte Modul Finance::QuoteHist::Yahoo besteht auf Datumsangaben im amerikanischen Format mm/dd/yyyy. Die Funktion »date_format()« ab Zeile 108 bemüht daher die Methode »strftime()« für die Umwandlung der »DateTime«-Objekte.

Den umgekehrten Fall, aus einem Datum im Format mm/dd/yyyy ein »DateTime«-Objekt zu machen, erledigt die Funktion »dt_parse()« ab Zeile 115. Das Modul DateTime::Format::Strptime definiert ein neues Format, dessen »parse_datetime()«-Methode einen hereingereichten String analysiert und im Erfolgsfall ein neues Objekt zurückgibt.

Um von einem »DateTime«-Objekt ein Kalenderjahr zurückzurechnen, genügt es, dessen Methode »subtract()« mit dem Parameter »years => 1« aufzurufen. Allerdings modifiziert dies das Objekt selbst. Wer den ursprünglichen Wert später noch braucht, kopiert den Inhalt vorher mit »clone()« in ein neues Objekt (Zeilen 25 und 26).

Zeilenweise Spekulation

Das Skript »pofo« (Listing 2) nimmt auf der Kommandozeile eine Konfigurationsdatei wie »pofo1.txt« aus Abbildung 1 entgegen. Die Funktion »cfg_read()« ab Zeile 142 arbeitet sich durch deren Zeilen, die jeweils eine Aktientransaktion beschreiben. Sie ignoriert Kommentare, die mit »#« beginnen (Zeile 153), und Zeilen, die nichts als Leerzeichen und Kommentare enthalten (Zeile 155).

Listing 2:
»pofo«

001 #!/usr/bin/perl -w
002 use strict;
003 use CachedQuote;
004 use DateTime;
005 use RRDTool::OO;
006 use Log::Log4perl qw(:easy);
007 #Log::Log4perl->easy_init($DEBUG);
008 
009 my @colors = qw(f35b78 e80707 7607e8
010                 0a5316 073f6f 59b0fb);
011 my $cq = CachedQuote->new();
012 
013 my($cfg_file) = @ARGV;
014 die "usage: $0 cfgfile" unless $cfg_file;
015 
016 my @symbols;
017 my $acts = cfg_read($cfg_file, @symbols);
018 my %pos  = ();
019 
020 my $end     = DateTime->today();
021 my $start   = $end->clone->subtract(
022                                years => 1);
023 
024 for my $act (sort keys %$acts) {
025   next if $acts->{$act}->[0]->[0]
026           >= $start;
027   pos_add(%pos, $_) for @{$acts->{$act}};
028 }
029 
030 my $counter = 0;
031 my %symbol_colors;
032 for (@symbols) {
033   my $idx = ($counter++ % @colors);
034   $symbol_colors{$_} = $colors[$idx];
035 }
036 
037 unlink my $rrdfile = "holdings.rrd";
038 my $rrd = RRDTool::OO->new(
039     file => $rrdfile,
040 );
041 
042 $rrd->create(
043   step  => 24*3600,
044   start => $start->epoch() - 1,
045     map({
046       ( data_source => {
047           name      => $_,
048           type      => "GAUGE",
049         },
050       )} @symbols),
051      archive     => { rows => 5000,
052                       cfunc => "MAX" }
053 );
054 
055 for(my $dt = $start->clone;
056   $dt <= $end;
057   $dt->add( days => 1)) {
058 
059   if(exists $acts->{$dt}) {
060     pos_add(%pos, $_) for @{$acts->{$dt}};
061   }
062 
063   my %parts = ();
064   my $total = sum_up(%pos, $dt, %parts);
065 
066   $rrd->update(
067     time   => $dt->epoch(),
068     values => %parts,
069   );
070 }
071 
072 $rrd->graph(
073     width => 800,
074     height => 600,
075     lower_limit    => 0,
076     image          => "positions.png",
077     vertical_label => "Positions",
078     start          => $start->epoch(),
079     end            => $end->epoch(),
080     map { ( draw           => {
081               type   => "stack",
082               dsname => $_,
083               color  => $symbol_colors{$_},
084               legend => $_,
085             } )
086         } @symbols,
087 );
088 
089 ###########################################
090 sub sum_up {
091 ###########################################
092   my($all, $dt, $parts) = @_;
093 
094   my $sum = 0;
095 
096   for my $tick (keys %$all) {
097     my $q = 1;
098     $q = $cq->quote($dt, $tick) if
099                          $tick ne 'cash';
100     my $add = $all->{$tick} * $q;
101     $parts->{$tick} = $add;
102     $sum += $add;
103 
104     DEBUG "Add: $all->{$tick} $tick $add";
105   }
106   return $sum;
107 }
108 
109 ###########################################
110 sub pos_add {
111 ###########################################
112   my($all, $pos) = @_;
113 
114   my($dt, $act, $tick, $n) = @{ $pos };
115   DEBUG "Action: $act $n $tick";
116 
117   my $q = 1;
118   $q = $cq->quote($dt, $tick) if
119                            $tick ne 'cash';
120   my $val = $n * $q;
121 
122   if($tick eq "cash") {
123     $all->{cash} += $val if $act eq "in";
124     $all->{cash} -= $val if $act eq "out";
125     $all->{cash}  = $val if $act eq "chk";
126   } else {
127     die "chk: cash-only" if $act eq "chk";
128     if($act eq "in") {
129       $all->{$tick} += $n;
130       $all->{cash}  -= $val;
131     } elsif($act eq "out") {
132       $all->{$tick} -= $n;
133       $all->{cash}  += $val;
134     }
135     DEBUG "After: $tick: $all->{$tick}";
136   }
137 
138   DEBUG "After: Cash: $all->{cash}";
139 }
140 
141 ###########################################
142 sub cfg_read {
143 ###########################################
144   my($cfgfile, $symbols) = @_;
145 
146   my %by_date = ();
147 
148   open FILE, "<$cfgfile" or
149     die "Cannot open $cfgfile ($!)";
150 
151   while(<FILE>) {
152     chomp;
153     s/#.*//;
154     my @fields = split ' ', $_;
155     next unless @fields; # empty line
156 
157     my $dt = dt_parse( $fields[0] );
158     $fields[0] = $dt;
159 
160     push @$symbols, $fields[2] unless
161        grep { $_ eq $fields[2] } @$symbols;
162 
163     push @{ $by_date{ $dt } }, [ @fields ];
164   }
165 
166   close FILE;
167   return %by_date;
168 }
169 
170 ###########################################
171 sub dt_parse {
172 ###########################################
173   my($string) = @_;
174 
175   my $fmt = DateTime::Format::Strptime->
176               new( pattern => "%Y-%m-%d" );
177     return $fmt->parse_datetime($string);
178 }

Formatwandler

Da die Datumsangaben im Format yyyy/mm/dd vorliegen, hat auch Pofo eine Funktion »dt_parse()« (Zeilen 171 bis 178), die das Format definiert (Zeile 176) und die Datumseinträge in »DateTime«-Objekte umwandelt. Als zusätzlichen Service nimmt die Funktion »cfg_read()« (Zeilen 142 bis 168) eine Referenz auf das Array »@symbols« entgegen, das sie mit allen auftretenden Aktientickersymbolen füllt und dabei Duplikate vermeidet (Zeilen 160, 161).

Die Funktion gibt eine Referenz auf den von ihr gefüllten Hash »%by_date« zurück. Die Schlüssel in diesem Hash sind Datumsangaben in Form stringifizierter »DateTime«-Objekte. Als Werte sind den Schlüsseln jeweils ein Array von Transaktionen zugeordnet, die an diesen Tagen stattgefunden haben (Zeile 163). Jede Transaktion besteht wiederum aus einem Array, das die Felder der zugehörigen Konfigurationszeile enthält, also Datum, Aktion, Tickersymbol und Anzahl der Aktien. Cash-Aktionen stehen auch dort, mit »cash« als Tickersymbol.

Posten für Posten

Um zu sehen, wie viele Aktien einer Sorte an einem bestimmten Tag im Depot liegen, muss das Skript alle Transaktionen abarbeiten, die bis zu diesem Datum im Portfolio erfolgten. Deshalb wühlt sich die For-Schleife ab Zeile 24 zunächst durch alle früheren Aktionen, also durch die Schlüssel im Hash, den bereits in Zeile 17 die Funktion »cfg_read()« geliefert hat.

Die Schleife in Zeile 27 ruft für jede Transaktion die Funktion »pos_add()« auf und markiert das Ergebnis als Depotinhalt in der Variablen »%pos«. Dieser Hash weist jedem im Portfolio enthaltenen Tickersymbol einen numerischen Wert zu. Bei Aktien ist dies die Anzahl, bei Bargeld einfach die Summe.

Aktienkäufe und -verkäufe lösen zusätzlich Bewegung im Bargeldposten aus, denn neue Aktien kosten Geld und der Erlös der verkauften wird dem Girokonto gutgeschrieben. Das erfolgt immer zum jeweiligen Tageskurs, die Daten hierzu liefert »CachedQuote.pm«.

RRD-Tool abstrakt

Mit der grafischen Darstellung der turmartig aufgeschichteten Einzelpositionen hilft RRD-Tool von Tobias Oetiker ([2] und [4]). Die ungewöhnliche Syntax dieses praktischen Tools versucht das objektorientierte CPAN-Modul RRDTool::OO etwas Perl-artiger und übersichtlicher zu gestalten.

Das RRD-Tool legt Daten von so genannten Data Sources in RRD-Archiven ab (Round Robin Databases), indem es die Messpunkte der Datenquellen kumuliert und über die eingestellte Schrittgröße mittelt. Das Pofo-Programm stellt den »step«-Parameter in Zeile 43 von Listing 2 auf 24 Stunden, damit die RRD-Datenbank nur ein Update pro Tag erwartet. Jeder Aktie weist das Programm eine eigene Datenquelle zu (Map-Aufruf in den Zeilen 45 bis 50).

Das RRD-Archiv hält bis zu 5000 Werte (Zeile 51), bevor das RRD-typische Überschreiben beginnt. Da jeder Tag einen Wert liefert, tritt dieser Fall erst bei dargestellten Zeitspannen von weit über zehn Jahren ein. Der Parameterwert »GAUGE« (sprich “Geydsch”, Zeile 48) legt fest, dass RRD-Tool die ankommenden Werte direkt übernimmt und nicht kumuliert – das wäre doppelt gemoppelt.

Allerdings weigert sich RRD-Tool, Werte für Zeiten anzunehmen, die vor dem letzten eingespeisten Tageswert liegen, und so löscht Pofo in Zeile 37 kurzerhand übrig gebliebene RRD-Dateien, die der Konstruktor von RRDTool::OO dann schnell neu erzeugt.

Farbenfroh

Zeile 9 in Listing 2 definiert eine frei gewählte Farbpalette mit RGB-Hexwerten. Aus dem Array »@colors« wählt Pofo in den Zeilen 32 bis 35 für jede dargestellte Aktie einen Wert aus, sodass der Betrachter sie in der Grafik auseinanderhalten kann. Der Hash »%symbol_colors« nimmt die Zuordnung von Symbol zu einer Farbe aus der Palette auf. Die Reihenfolge, in der die Aktionen in der Konfigurationsdatei stehen, bestimmt deren Darstellungsabfolge im Graphen.

Die For-Schleife ab Zeile 55 arbeitet sich durch alle im Graphen darzustellenden Tage. Jedes Mal prüft die If-Bedingung in Zeile 59, ob für diesen Tag Transaktionen vorliegen, und führt diese mit »pos_add()« aus, damit der globale Hash »%pos« die aktuelle Portfolio-Konfiguration enthält. Die Funktion »sum_up()« ermittelt anschließend den Tageswert des Portfolios und legt im Hash »%parts« unter den Schlüsseln der Aktienticker (oder »cash«) die Geldwerte der einzelnen Positionen ab.

Diesen Hash überreicht anschließend die »update()«-Methode des RRD-Objekts an die RRD-Datenbank unter dem Zeitstempel des gerade bearbeiteten Tages (Zeilen 66 bis 69). Die Methode »graph()« zeichnet anschließend den Graphen in der Datei »positions.png« und legt die Legende am unteren Bildrand an (Zeilen 72 bis 87). Das abgedruckte Listing zeigt nur sechs Aktienfarben, es spricht aber nichts dagegen, das Array »@colors« ab Zeile 9 mit neuen Farben zu erweitern.

Erweiterungen

In Zeile 22 setzt Pofo den dargestellten Zeitraum auf das vergangene Jahr. Für andere Zeiträume lassen sich die Variablen »$start« und »$end« modifizieren. Wer mehr Informationen über die internen Abläufe wünscht, streicht in Zeile 7 das Kommentarzeichen. Dann initialisiert sich Log4perl mit »easy_init()« und die über den Quellcode verstreuten Ausgaben der »DEBUG«-Anweisungen landen auf dem Bildschirm.

Mit Stocksplits weiß das Skript nichts anzufangen, denn dann ändern sich die historischen Kursdaten rückwirkend und der Cache enthält ungültige Daten. In diesem Fall löscht der Anleger einfach die Cache-Datei »/tmp/cached-quotes.dat« und verwirft damit den gesamten Cache. Ihn wieder aufzufüllen ist nicht übermäßig kostspielig, denn Web-Requests an den Finanzserver holen die Daten effizient in großen Mengen ein. Viel Spaß beim Spekulieren! (fjl)

Infos

[1] Zitate von Helmut Kohl: [http://de.wikiquote.org/wiki/Helmut_Kohl]

[2] RRD-Tool: [http://www.rrdtool.org]

[3] Christoph Dalitz, “SQLite – Datenspeicher für Sparsame”: Linux-Magazin 09/04, S. 96 und [http://www.sqlite.org]

[4] Michael Schilli, “Daten ausgesiebt – Messdaten mit RRD-Tool und Perl verwalten”: [https://www.linux-magazin.de/heft_abo/ausgaben/2004/06/daten_ausgesiebt]

[5] Michael Schilli, “Vom Perl- zum Börsen-Profi”: Linux-Magazin 06/02, S. 110

[6] Listings zum Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2008/03/Perl]

Der Autor


Michael Schilli arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen.

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