Gemischte Strategie (Linux-Magazin, März 2008)

Mit Perl gestrickte Turmgrafiken zeigen den zeitlichen Wertverlauf eines Aktienportfolios und helfen, Diversifizierung und Performance im Auge zu behalten.

``Entscheidend ist, was hinten rauskommt'' sagte schon Helmut Kohl. Auch bei der Vermögensanlage zählt nicht, was eine einzelne Aktie im Depot treibt, sondern wie sich ein Portfolio aus möglichst diversen Einzelposten entwickelt. Die großen Finanzseiten auf dem Internet zeigen zwar ansprechende Grafiken zur Kursentwicklung einzelner Posten und lassen sogar grafische Vergleiche zweier Wertpapiere zu, bieten aber kein Tool an, das die Kursentwicklung der Einzelposten eines Portfolios auf einen Blick zeigt.

Abbildung 1: Ein Anleger legt im Januar 20.000 Dollar in Internetaktien an.

Abbildung 2: Starke Gewinner übertönen die Verlierer und am Jahresende verbleibt ein kleiner Gewinn.

Ein in Perl handgestricktes Skript schafft Abhilfe. Abbildung 1 zeigt die Konfigurationsdatei pofo1.txt eines Portfolios. Jede Zeile stellt eine Kauf- ('in') oder Verkaufsaktion ('out') eines Aktienpostens dar. Auch Bargeld kann man so rein- und rausschieben, statt dem Aktienticker-Symbol steht bei diesen Aktionen 'cash'.

Damit das Ganze nicht in zu viel Tipparbeit ausartet, soll das Portfolio selbständig die Kosten und die Erlöse der Aktientransaktionen zum aktuellen Tageskurs ausrechnen und den Bargeldbestand entsprechend anpassen. Das stimmt natürlich nicht genau, da eventuell noch Gebühren anfallen, aber das lässt sich leicht korrigieren, in dem der aktuelle Kontostand mit dem Symbol ``cash'' und der Aktion ``chk'' und Datum gesetzt wird, damit der Saldo wieder stimmt.

Das Tool verfolgt natürlich auch deutsche Aktien, wenn die richtigen Tickersymbole vorliegen, ``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. Neun Tage später wurden 50 Amazon-, 20 IBM-, 10 Google- und 200 Motorola-Aktien zum Tageskurs erworben. Während des restlichen Jahres lehnte sich der Anleger zurück und ließ das Portfolio wachsen und gedeihen. Die Kursentwicklung dieser vier Aktienpakete lässt sich in Abbildung 2 grafisch verfolgen. Während die Amazon- und die Google-Aktie kräftig anzogen, schwächelte Motorola und das Gesamtergebnis am Jahresende litt leicht darunter. Unterm Strich konnte das Portfolio jedoch einen leichten Gewinn verbuchen.

Glückliches Händchen

Anders der Inhaber des Portfolios in Abbildung 3: Auch hier lagen zu Jahresanfang 20.000 Dollar, und sofort wurden 200 Aktien der amerikanischen Drogeriekette CVS (nicht zu verwechseln mit dem gleichnamigen Versionskontrollsystem) zum aktuellen Tageskurs gekauft. Gut eine Woche später kaufte der Spekulant 150 Amazon-Aktien, die er dann 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 3: Ein aktiver Anleger, der die Positionen mehrmals im Jahr wechselt und ein glückliches Händchen hat ...

Abbildung 4: ... erspielt am Ende des Jahres einen erklecklichen Gewinn.

Der Graph in Abbildung 4 bestätigt einen deutlich höheren Gewinn am Jahresende und zeigt auch, dass das Stapeln mehrerer Graphen zu verwirrenden Verschiebungen führen kann, obwohl die Reihenfolge der Einzelposten immer gleich bleibt.

Die Anzeige der Vermögensverhältnisse dieses glücklichen Depotbesitzers entsteht mittels des Skripts pofo, das die Daten über die getätigten Spekulationskäufe aus den gezeigten Konfigurationsdateien bezieht. Diese nimmt das Skript auf der Kommandozeile entgegen und pofo pofo1.txt werkelt dann eine Weile vor sich hin und erzeugt schließlich eine Bild-Datei positions.png mit dem Graphen.

Für jeden dargestellten Tag ermittelt es die aktuelle Konfiguration des Portfolios, holt die aktuellen Tageskurse ein und multipliziert diese mit den Stückzahlen der Einzelposten. Das eigentlich zur Darstellung von Netzwerkverkehr und Rechnerauslastung gedachte Tool rrdtool verpackt diese Tagesdaten in eine übersichtliche Turmgrafik. Den verschiedenen Aktien ordnet es aus einer vorgegebenen Farbpalette zufällig Farben zu und weist diese Zuweisungen in einer Legende am unteren Rand der Grafik aus.

Schnell muss es gehen

Die historischen Tageskurse aller bekannten Aktien sind auf dem Internet verfügbar, allerdings wäre das Skript unerträglich langsam falls es diese tatsächlich einzeln für jeden dargestellten Tag einholen würde. Statt dessen bemüht es das Modul CachedQuote, das nicht nur den angeforderten Tageskurs einer Aktie einholt, sondern gleich alle Kurse in einem Zeitfenster, das von einem Jahr in der Vergangenheit bis zum aktuellen Datum reicht. Die nicht sofort gebrauchten Werte speichert es in einer SQLite-Datenbank zwischen. Fordert der Client -- wie vorausgesehen -- tatsächlich den nächsten Tageskurs an, liest CachedQuote den Wert einfach aus seinem Speicher. Der Client bekommt davon nichts mit, außer dass nachfolgende Requests um ein vielfaches schneller gehen.

Falls ein Kunde den Kurs einer Aktie an einem Sonntag einholt, stellt CachedQuote fest, dass die geforderten Daten zwar im eingeholten Zeitfenster liegen, aber für diesen Tag leider kein Kurs vorhanden ist, da die Börsen der Welt an Sonntagen nicht arbeiten. In diesem Fall ist CachedQuote so schlau, statt einem Datenloch den letzten vorliegenden Kurs (also vom Freitag, falls dieser nicht auf einen Feiertag fiel) auszuliefern.

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;

Das Modul CachedQuote.pm holt die Aktienkurse intern mit dem CPAN-Modul Finance::QuoteHist::Yahoo vom Netz. Als Tageswert nimmt der Cache immer den mit 'close' bezeichneten Schlusskurs. Pro Web-Request kann der kontaktierte Yahoo-Server für eine Aktie die Kursdaten vieler Jahre liefern. Das nutzt CachedQuote.pm voll aus, denn zu jedem per quote() eingehenden Request für einen Tageskurs schickt es einen Request an den Server, der die Daten von einem Jahr vor dem verlangten Zeitpunkt bis zum aktuellen Datum umfasst. Zum Speichern und späteren Wiederfinden der zwischengespeicherten Daten verwendet CachedQuote.pm das CPAN-Modul Cache::Historical. Es bietet eine komfortable Schnittstelle zum Setzen datumsbasierter Daten (set($dt, $value)) und liefert die gespeicherten Werte mit den Methoden get() und get_interpolated() später wieder zurück. Fehlt ein Kurs für einen Tag, greift letztere Methode auf den letzten verfügbaren Kurs vor dem angegebenen Datum zurück.

Cache::Historical wiederum 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.

Die Funktion quote() versucht zuerst, den geforderten Kurs mit get() einzuholen. Schlägt dies fehl, was quote() mit dem Rückgabewert undef quittiert, versucht quote_refresh(), den Cache rund um das geforderte Datum aufzufrischen. Anschließend sollte get_interpolated() in jedem Fall einen ordentlichen Wert zurückliefern.

Die SQLite-Datei zum Zwischenspeichern der Daten stellt CachedQuote.pm in Zeile 18 als /tmp/cached-quote.dat ein. Wer den Cache nicht im ständig durch Löschung bedrohten temporären Verzeichnis möchte, kann dies im Aufruf des Konstruktors mit new(sqlite_file => "...") überschreiben.

CachedQuote.pm unterscheidet weiterhin, 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 and einem Sonntag läuft, soll das Modul jedoch nicht jedesmal versuchen, die neuesten Kurse vom Server zu holen, denn es wird bis zum Montag keine neuen geben. Deshalb holt die Funktion quote_refresh() auch noch mit since_last_update() die Zeitspanne seit dem letzten Auffrischen des Caches ein. Sie liegt als DateTime::Duration-Objekt vor und die Methode delta_days() rechnet diese in Tage mit Bruchteilen um. Ist der Cache noch keinen Tag alt, entfällt der Update und der letzte verfügbare Kurs (üblicherweise vom Freitag) kommt zum Einsatz.

DateTime für alles

Das Interface des Moduls DateTime vom CPAN ist so komfortabel, dass man eigentlich gar nichts anderes mehr 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 die Methode strftime für die Umwandlung der DateTime-Objekte.

Den umgekehrten Fall, dass aus einem Datum im Format mm/dd/yyyy ein DateTime-Objekt entsteht, 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 DateTime-Objekt zurückgibt.

Um von einem DateTime-Objekt ein Jahr zurückzurechnen, ruft man dessen Methode subtract() mit den Parametern years => 1 auf. Allerdings modifiziert dies das Objekt selbst. Wenn man den ursprünglichen Wert später noch braucht, empfielt es sich, es vorher mit clone() in ein weiteres Objekt zu retten.

Zeilenweise Spekulation

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

Da die Datumsangaben im Format yyyy-mm-dd vorliegen, hat auch pofo eine Funktion dt_parse, die diesmal wieder ein anderes Format definiert und die Datumseinträge in DateTime-Objekte umwandelt.

Als zusätzlichen Service nimmt cfg_read() eine Referenz auf den Array @symbols entgegen, den sie mit allen auftretenden Aktientickersymbolen ohne Duplikate füllt. Sie gibt eine Referenz auf den von ihr gefüllten Hash %by_date zurück. Dieser enthält als Schlüssel Datumsangaben als stringifizierte DateTime-Objekte. Als Werte sind den Schlüsseln jeweils eine Reihe von Transaktionen zugeordnet, die an diesen Tagen stattgefunden haben. Jede Transaktion besteht wiederum aus einem Array, der die Felder der zugehörigen Konfigurationszeile enthalten, also Datum, Aktion, Tickersymbol und Anzahl der Aktien. Cashaktionen stehen auch dort, mit cash als Tickersymbol.

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      => tick_clean($_),
    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   INFO "*** TOTAL *** = $total\n";
    066 
    067   $rrd->update(
    068     time   => $dt->epoch(),
    069     values => \%parts,
    070   ) if scalar keys %parts;
    071 }
    072 
    073 $rrd->graph(
    074     width => 800,
    075     height => 600,
    076     lower_limit    => 0,
    077     image          => "positions.png",
    078     vertical_label => "Positions",
    079     start          => $start->epoch(),
    080     end            => $end->epoch(),
    081     map { ( draw           => {
    082               type   => "stack",
    083               dsname => tick_clean($_),
    084               color  => $symbol_colors{$_},
    085               legend => $_,
    086             } )
    087         } @symbols,
    088 );
    089 
    090 ###########################################
    091 sub sum_up {
    092 ###########################################
    093   my($all, $dt, $parts) = @_;
    094 
    095   my $sum = 0;
    096 
    097   for my $tick (keys %$all) {
    098     my $q = 1;
    099     $q = $cq->quote($dt, $tick) if 
    100                          $tick ne 'cash';
    101     my $add = $all->{$tick} * $q;
    102     $parts->{tick_clean($tick)} = $add;
    103     $sum += $add;
    104 
    105     DEBUG "Add: $all->{$tick} $tick $add";
    106   }
    107   return $sum;
    108 }
    109 
    110 ###########################################
    111 sub pos_add {
    112 ###########################################
    113   my($all, $pos) = @_;
    114 
    115   my($dt, $act, $tick, $n) = @{ $pos };
    116   die "pos: @$pos" if ! defined $n;
    117   DEBUG "Action: $act $n $tick";
    118 
    119   my $q = 1;
    120   $q = $cq->quote($dt, $tick) if 
    121                            $tick ne 'cash';
    122   my $val = $n * $q;
    123 
    124   if($tick eq "cash") {
    125     $all->{cash} += $val if $act eq "in";
    126     $all->{cash} -= $val if $act eq "out";
    127     $all->{cash}  = $val if $act eq "chk";
    128   } else {
    129     if($act eq "in") {
    130       $all->{$tick} += $n;
    131       $all->{cash}  -= $val;
    132     } elsif($act eq "out") {
    133       $all->{$tick} -= $n;
    134       $all->{cash}  += $val;
    135     } elsif($act eq "find") {
    136       $all->{$tick} += $n;
    137     }
    138     DEBUG "After: $tick: $all->{$tick}";
    139   }
    140 
    141   $all->{cash} ||= 0;
    142   DEBUG "After: Cash: $all->{cash}";
    143 }
    144 
    145 ###########################################
    146 sub cfg_read {
    147 ###########################################
    148   my($cfgfile, $symbols) = @_;
    149 
    150   my %by_date = ();
    151 
    152   open FILE, "<$cfgfile" or 
    153     die "Cannot open $cfgfile ($!)";
    154 
    155   while(<FILE>) {
    156     chomp;
    157     s/#.*//;
    158     my @fields = split ' ', $_;
    159     next unless @fields; # empty line
    160 
    161     my $dt = dt_parse( $fields[0] );
    162     $fields[0] = $dt;
    163 
    164     push @$symbols, $fields[2] unless 
    165        grep { $_ eq $fields[2] } @$symbols;
    166 
    167     push @{ $by_date{ $dt } }, [ @fields ];
    168   }
    169   
    170   close FILE;
    171   return \%by_date;
    172 }
    173 
    174 ###########################################
    175 sub dt_parse {
    176 ###########################################
    177   my($string) = @_;
    178 
    179   my $fmt = DateTime::Format::Strptime->
    180               new( pattern => "%Y-%m-%d" );
    181     return $fmt->parse_datetime($string);
    182 }
    183 
    184 ###########################################
    185 sub tick_clean {
    186 ###########################################
    187     my($tick) = @_;
    188 
    189     $tick =~ s/./_/g;
    190     return $tick;
    191 }

Posten für Posten

Um nun herauszufinden, wie viele Aktien einer bestimmten Sorte an einem bestimmten Tag im Depot liegen, muss das Skript alle Transaktionen abarbeiten, die bis zu diesem Datum im Portfolio erfolgt sind. Deshalb wühlt sich die For-Schleife ab Zeile 24 zunächst durch alle Aktionen, die vor dem darzustellenden Zeitfenster erfolgt sind. Die Funktion pos_add() fügt jeweils eine Transaktion durch und markiert das Endergebnis als Portfolioinhalt in der Variablen %pos. Dieser Hash weist jedem im Portfolio enthaltenen Tickersymbol einen numerischen Wert zu. Bei Aktien ist dies deren Anzahl, bei Bargeld einfach die Summe.

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

RRDTool abstrakt

Mit der grafischen Darstellung der turmartig aufgeschichteten Einzelpositionen hilft rrdtool von Tobias Oetiker ([3], [4]). Die etwas ungewöhnliche Syntax dieses praktischen Tools versucht das CPAN-Modul RRDTool::OO (``OO'' für ``Objekt-Orientiert''), etwas Perl-artiger und übersichtlicher zu gestalten.

RRDTool legt Daten von ``Data Sources'' in RRD-Archiven ab, indem es sie die Messpunkte der ``Data Sources'' aufkumuliert und über die eingestellte step-Dauer mittelt. pofo stellt den Parameter step in Zeile 43 auf 24 Stunden, damit die damit die RRD-Datenbank nur einen Update pro Tag erwartet. Jeder Aktie wird eine ``Data Source'' zugeordnet und das RRD-Archiv hält bis zu 5000 Werte, 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'') legt fest, dass die ankommenden Werte direkt übernommen und nicht etwa von RRDTool aufkumuliert werden.

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

Zeile 9 in pofo definiert eine relativ zufällige Palette von möglichst unterschiedlichen Farben, die als RGB-Hexwerte vorliegen. Aus dem Array @colors wählt pofo-report für jede dargestellte Aktie einen Wert aus, so dass man diese in der Grafik problemlos auseinanderhalten kann. Der Hash %symbol_colors weist jedem Symbol anschließend eine Farbe aus dieser Palette zu. Die Reihenfolge, in der die einzelnen Aktionen in der Konfigurationsdatei stehen, bestimmt deren Darstellungsabfolge im Graph.

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 an diesem 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 (bzw. cash) die Geldwerte der einzelnen Positionen ab.

Diesen Hash überreicht anschließend die update()-Methode des RRD-Objekts der RRD-Datenbank unter dem Zeitstempel des gerade bearbeiteten Tages. Die Methode graph zeichnet anschließend den Graphen in der Datei positions.png und legt die Legende am unteren Bildrand an. Wie aus dem Listing ersichtlich, ist die Anzahl der darstellbaren Aktien zur Zeit auf 6 beschränkt, es spricht allerdings nichts dagegen, den Array @colors einfach mit neuen Farbkombinationen zu erweiteren.

Erweiterungen

Gehen die Farben aus, kann pofo auch dahingehend modifiziert werden, dass nur diejenigen Tickersymbole Farben zugewiesen bekommen, die auch im ausgewählten Zeitfenster dargestellt werden.

In Zeile 22 setzt pofo den dargestellten Zeitraum auf das zurückliegende Jahr, wer einen anderen Zeitraum darstellen möchte, modifiziert einfach die Variablen $start und $end.

Wer mehr Informationen über die internen Abläufe braucht, kommentiert die Zeile 7 in pofo aus, damit Log4perl mit easy_init seine Initialisierung betreibt und die über das Skript und das CachedQuote-Module verstreuten DEBUG-Anweisungen auf dem Bildschirm ausgibt während die Berechnung läuft.

Ein Manko des Skripts sind Stocksplits. Diese verarbeitet pofo noch nicht, denn dann ändern sich die historischen Kursdaten rückwirkend und der Cache enthält ungültige Daten. In diesem Fall löscht der Anwender einfach die Cache-Datei /tmp/cached-quotes.dat und verwirft damit den gesamten Cache. Ihn wieder aufzufüllen ist nicht übermässig kostspielig, denn Web-Requests an den Finanz-Server holen die Daten effizient in großen Mengen ein.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2008/03/Perl

[2]
``Vom Perl- zum Börsen-Profi'', Michael Schilli, http://perlmeister.com/snapshots/200206/index.html

[3]
``rrdtool'', http://rrdtool.org

[4]
``Daten ausgesiebt - Messdaten mit RRDtool und Perl verwalten'', Michael Schilli, http://www.linux-magazin.de/heft_abo/ausgaben/2004/06/daten_ausgesiebt

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. Seine Homepage: http://perlmeister.com.