An Börsenstatistiken herrscht kein Mangel. Aber wer hätte gewusst, dass die Microsoft-Aktie an Freitagen am teuersten ist? Oder IBM am 1. jedes Monats auf dem Tiefststand? Die historischen Kursdaten sind alle verfügbar, fehlt nur noch ein Skript, um darin zu stöbern ...
Das Modul Finance::QuoteHist
von Matt Sisk bringt
die auf yahoo.com
verfügbaren historischen Kursdaten beliebiger
Aktien blitzschnell ins Wohnzimmer -- uns erlaubt es uns Börsenhobbyisten,
Skripts darauf abzufeuern und die abgefahrensten Statistiken zu
erstellen. Um die Sache zu beschleunigen und nicht jedes Mal Kontakt
mit den freundlichen Yahoo-Servern aufnehmen zu müssen, holt das
heute vorgestellte Modul QuoteDB.pm
beim ersten Aufruf auf einen
Schlag gleich die täglichen Daten für sieben Jahre ab und speichert
sie persistent in einer lokalen Datenbank, sodass auch nachfolgende
Aufrufe der Wühlskripts für die gleiche Aktie nie wieder den relativ
langsamen Netzzugriff ausführen müssen.
In welchen Monaten im Jahr ist die Microsoft-Aktie durchschnittlich
am billigsten, in welchen am teuersten?
Listing bestmonth
holt mittels der von QuoteDB.pm
exportierten
Funktion quote()
alle Tageskurse der Microsoftaktie der Jahre
1995 bis 2001, bildet die monatlichen Mittelwerte und gibt
sie nach dem Durchschnittspreis sortiert aus. Das Ergebnis ist verblüffend:
12: 50.96 07: 50.57 11: 48.98 ... 02: 45.68 01: 45.03 05: 44.80
Microsoft ist im Dezember durchschnittlich am teuersten und im Mai am billigsten. Dieses Ergebnis bestätigt die alten Börsenhasen bekannte Regel, dass Aktien in der zweiten Jahreshälfte deutlich besser zulegen als in den ersten 6 Monaten jeden Jahres.
Listing bestmonth
holt mittels
use QuoteDB 'quotes.db';
das neue Modul herein und gibt ihm den Namen einer Datei mit,
unter der es die Kursdatenbank aufbauen soll. QuoteDB.pm
wird
daraufhin die Funktion
quote($symbol, $y, $m, $d);
exportieren, die das Börsentickersymbol der gewünschten Aktie (z.B.
MSFT) und das aktuelle Datum im Format
Jahr ($y
), Monat ($m
), Tag ($d
) entgegennimmt und den
Tageskurs (in diesem Fall in Dollar) zurückliefert. Da die Datenbank beim
ersten Aufruf des Skripts noch leer ist, wird sich zunächst
Updating ...
zeigen, wenn QuoteDB.pm
die Microsoft-Kursdaten
der Jahre 1995 bis 2001 vom Yahoo-Finanzservice abholt. Beim nächsten
Aufruf von bestmonth
ist die Datenbank schon heiß und das Ergebnis
steht praktisch sofort da.
quote()
ist außerdem so schlau, dass es zu Tagen, an denen keine
Börse stattfand (z.B. sonntags) einfach den Kurs des letzten Börsentags
liefert. Außerdem behandelt es ungültige Datumsangaben wie den
31. Februar großzügig und gibt ohne Murren
einfach den Tageskurs des letzten gültigen Datums/Börsentags zurück.
Das vereinfacht die Implementierung von bestmonth
beträchtlich, denn
es iteriert einfach über alle Jahre 1995 bis 2001 und alle Monatstage
1 bis 31 und bildet den Mittelwert über alle Januarkurse, Februarkurse
usw. Hierzu addiert es einfach für jeden Monat über die Jahre
die Tageswerte in $sum
auf und
zählt in $count
mit, wieviele Einträge es insgesamt waren. In Zeile
23 bildet es dann den Mittelwert, indem es $sum
durch
$count
teilt und unter der Monatsnummer als Schlüssel im Hash
%per_month
ablegt. Die for
-Schleife ab Zeile 26 iteriert
dann über alle Hash-Schlüssel, sortiert die entstehende Liste
mit dem Spaceship-Operator (<=>
)
numerisch absteigend nach den Hash-Werten und gibt die Elemente
untereinander aus. Während der letzten sieben Jahre war also
im Schnitt der Dezember der günstigste Monat, um die alten
Lappen loszuschlagen.
01 #!/usr/bin/perl 02 ########################################### 03 # In welchen Monat ist MSFT am teuersten? 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use QuoteDB 'quotes.db'; 10 11 my %per_month; 12 13 for my $month (1 .. 12) { 14 my $sum = 0; 15 my $count = 0; 16 for my $year (1995 .. 2001) { 17 for my $day (1 .. 31) { 18 $sum += 19 quote("MSFT", $year, $month, $day); 20 $count++; 21 } 22 } 23 $per_month{$month} = $sum/$count; 24 } 25 26 for my $month (sort { 27 $per_month{$b} <=> $per_month{$a} 28 } keys %per_month) { 29 printf "%02d: %.2f\n", $month, 30 $per_month{$month}; 31 }
Ist es besser, die IBM-Aktie eher am Anfang oder am Ende des Monats
zu kaufen, was sagt die Erfahrung der letzten Jahre dazu? Wie immer
findet man solche Tipps nicht in den Publikationen
schlipstragender Börsenschlaumeier --
sondern eben nur im Linux-Magazin.
Die Ausgabe von Listing bestday
zeigt wissenschaftlich einwandfrei,
dass diejenigen
Leute besser bedient waren, die IBM am Monatsanfang kauften:
26: 69.50 27: 69.35 25: 69.29 ... 03: 68.30 02: 68.08 01: 67.92
Wer zum Beispiel monatlich einen festen Betrag in IBM investierte, kuckte mit dem Ofenrohr ins Gebirge, falls die Buchung immer um den 26. abging -- wer hingegen fünf Tage wartete, sparte bares Geld!
Listing bestmonth
iteriert hierzu über alle Tages-, Monats- und
Jahreswerte während der Jahre 1995 bis 2001 und legt in einem
Hash %per_day
unter dem Schlüssel des Tags im Monat (1..31)
jeweils ein dreielementiges Array an: Unter der Indexnummer 0
steht dort die Summe aller bislang für diesem Tageswert gefundenen Kurse,
unter 1
deren Anzahl und unter Indexwert 2
der bisherige Mittelwert,
der sich einfach aus Division der Positionen 0
und 1
ergibt.
Die for
-Schleife am Ende iteriert über alle Hash-Einträge, sortiert
numerisch absteigend nach dem Mittelwert für den jeweiligen Tageswert
und gibt die Elemente der Liste untereinander aus.
01 #!/usr/bin/perl 02 ########################################### 03 # An welchem Tag im Monat is IBM am 04 # teuersten? 05 # Mike Schilli, 2002 (m@perlmeister.com) 06 ########################################### 07 use warnings; 08 use strict; 09 10 use QuoteDB 'quotes.db'; 11 12 my %per_day = (); 13 14 for my $d (1 .. 31) { 15 for my $y (1995 .. 2001) { 16 for my $m (1 .. 12) { 17 my ($p) = ($per_day{$d} ||= []); 18 $p->[0] += quote("IBM", $y, $m, $d); 19 $p->[1]++; 20 $p->[2] = $p->[0] / $p->[1]; 21 } 22 } 23 } 24 25 for my $d (sort { $per_day{$b}->[2] <=> 26 $per_day{$a}->[2] } 27 keys %per_day) { 28 printf "%02d: %.2f\n", $d, 29 $per_day{$d}->[2]; 30 }
Listing bestdow
(dow = Day of Week) wählt einen etwas anderen Ansatz
und rennt nicht mehr durch Datumsangaben, die es gar nicht gibt, sondern
nutzt das Modul Date::Calc
und dessen Funktion Add_Delta_Days()
,
um Tag für Tag im Kalender voranzuschreiten. Die Funktion
Day_of_Week()
teilt jeweils den zu einem Datum gehörenden Wochentag
(von 1=Montag bis 7=Sonntag) mit, was bestdow
dazu nutzt, in Zeile
23 explizit die Wochenenden auszulassen. Das Ergebnis für Microsoft
zeigt durchschnittlich, dass die Aktie übers Wochenende leicht absackt,
um dann von Montag bis Freitag wieder kontinuierlich aufzuholen:
Freitag : 47.39 Donnerstag: 47.33 Mittwoch : 47.27 Dienstag : 47.22 Montag : 47.18
Dieses Phänomen manifestiert sich jedoch in einer sehr geringen Streuung, das könnte ebenso purer Zufall sein.
01 #!/usr/bin/perl 02 ########################################### 03 # An welchen Wochentagen ist die 04 # Microsoft-Aktie am teuersten? 05 # Mike Schilli, 2002 (m@perlmeister.com) 06 ########################################### 07 use warnings; 08 use strict; 09 10 use QuoteDB 'quotes.db'; 11 use Date::Calc qw(Add_Delta_Days 12 Day_of_Week Language Decode_Language 13 Day_of_Week_to_Text); 14 15 my %per_dow; 16 17 Language(Decode_Language("Deutsch")); 18 19 for(my @date = (1995, 1, 1); 20 "@date" ne "2002 1 1"; 21 @date = Add_Delta_Days(@date, 1)) { 22 my $dow = Day_of_Week(@date); 23 next if $dow >=6; 24 my $p = ($per_dow{$dow} ||= []); 25 $p->[0] += quote("MSFT", @date); 26 $p->[1]++; 27 $p->[2] = $p->[0] / $p->[1]; 28 } 29 30 for my $dow (sort { $per_dow{$b}->[2] <=> 31 $per_dow{$a}->[2] } 32 keys %per_dow) { 33 printf "%-10s: %.2f\n", 34 Day_of_Week_to_Text($dow), 35 $per_dow{$dow}->[2]; 36 }
Listing QuoteDB.pm
definiert ab Zeile 35 die Funktion quote()
,
die mit dem Börsensymbol einer Aktie (z.B. MSFT) und
dem Datum im Format Jahr, Monat, Tag den jeweiligen Tageskurs
zutage fördert. Sie versucht zunächst, den Wert in dem persistenten
Datenbank-Hash %QUOTES
zu finden. Wenn das nichts hilft,
kommt die Funktion update_db()
zum Zug und gibt
Finance::QuoteHist
den Auftrag, die Kursdaten dieser Aktie vom
20.12.1994 bis zum heutigen Tag
vom Yahoo-Finanzservice abzuholen, damit wir ganz sicher die Kurse
ab dem 1.1.1995 haben, die irgendwann Ende Dezember 1994 an
der Börse ausgekartelt wurden.
quote()
setzt den Eintrag $QUOTES{SYMBOL}
auf 1 (Zeile 58,
damit wir nächstes Mal wissen, dass die Kurse dieser Aktie vorliegen)
und speichert die Kursdaten für jeden Tag im Hash
jeweils unter $QUOTES{"SYMBOL,JAHR,MONAT,TAG"}
ab, wobei
SYMBOL das Tickersymbol der Aktie ist (z.B. MSFT).
Die quote_get()
-Methode des Moduls Finance::QuoteHist
liefert
eine Liste mit Arrayreferenzen zurück, von denen jede auf folgende
Tagesdaten verweist:
$stock # Aktienname $date # Datum JJJJ/MM/TT $open # Öffnungskurs $high # Tageshöchstkurs $low # Tagestiefstkurs $close # Schlusskurs $volume # Anzahl gehandelter Aktien
Wir extrahieren lediglich den Schlusskurs ($close
) und
das Datum $date
im Format JJJJ/MM/TT
. die Zeilen 63-65 werfen eventuelle
Nullen raus ("07"
=> "7"
) und legen Tag, Monat und Jahr in $day
,
$month
und $year
ab.
Die ab Zeile 72 definierte Funktion latest_price()
ermittelt den
im Hash %QUOTES
gespeicherten Tagespreis einer Aktie für ein
angegebenes Datum. Nun ist aber nicht jeden Tag ein Börsentag --
und deswegen marschiert latest_price()
bis zu 10 Tage in
die Vergangenheit zurück, falls es zu einem Datum keinen Kurs findet.
Die rare Spezies des continue
-Blocks des while
-Konstrukts
kommt immer dann zum Einsatz, wenn while
in die nächste Runde springt.
Falls nach 10 Versuchen noch kein Börsentag gefunden wurde, stimmt
wahrscheinlich etwas mit %QUOTES
nicht (vielleicht gibt's die angegebene
Aktie auch noch nicht so lange) und latest_price()
gibt mit
einer die()
-Meldung auf.
latest_price()
verkraftet auch falsche Datumsangaben, damit das
aufrufende Programm nicht mit Kalenderfeinheiten herumschlagen muss.
So kann es ruhig nach dem Kurs für den 31. Februar fragen -- die
check_date()
-Funktion aus dem Date::Calc
-Modul wird dies
in Zeile 81 sofort merken und die zugehörige while
-Schleife
den Tagesteil des Datums solange herunterzählen, bis ein gültiger
Wert erreicht ist.
Zum Schluss noch einige Eigenheiten von QuoteDB.pm
: Der globale
Hash %QUOTES
ist tatsächlich permanent, behält also auch
beim nächsten Skriptaufruf die dort abgelegten Daten, da er
über die DB_File
-Technologie und die tie()
-Funktion in
Zeile 23 mit einer Datenbankdatei verknüpft ist. Wann immer jemand
aus dem Hash liest (z.B. mit $QUOTES{MSFT}
) oder hineinschreibt,
liest und schreibt Perl hinter den Kulissen eigentlich in einer Datei.
Die Konstante O_CREAT
kommt aus dem in Zeile 13 hinzugezogenen
POSIX
-Modul und veranlasst tie()
, einfach und ohne
zu grummeln eine neue Datenbankdatei anzulegen, falls diese
noch nicht existiert.
Deren Name legt das aufrufende Skript fest, indem es beim Hereinziehen des Moduls einfach den Dateinamen mit angibt:
use QuoteHist 'quotes.db';
Dies ist etwas ungewöhnlich, da man Modulen sonst auf diese Weise
nur die Namen von Funktionen oder Variablen mitgibt, die diese dann
in den Namensraums des Skripts exportieren. QuoteDB.pm
ist
aber eigen und nimmt nicht nur einen Dateinamen entgegen, sondern
exportiert ungefragt die Funktion quote()
in den Namensraum
des aufrufenden Skripts.
Während Module sonst vom Modul Exporter
erben, das den
traditionellen Symbolexport erledigt, implementiert QuoteDB.pm
ab Zeile 19
einfach seine eigene import()
-Funktion, die perl
anspringt,
falls ein Skript ein Modul mit use
hereinzieht.
perl
liefert import
als ersten Parameter automatisch den
Namen des Moduls, gefolgt von eventuell beim use
-Aufruf
darangehängten Parametern. Im obigen Fall steht der zweite
Parameter auf "quotes.db"
, den der in Zeile 23 stehende
tie()
-Befehl als Dateinamen interpretiert und unter den
Unix-Zugriffsrechten 0644
(rw-r--r--) eine DB_File
-Datenbank
anlegt.
Die Zeilen 26 und 27 exportieren anschließend die Funktion
quote
des QuoteDB
-Moduls in den Namensraum des aufrufenden
Skipts. Wie? Zunächst ermittelt die Funktion caller()
den package
-Namen des Aufrufers -- im Fall des Hauptprogramms
ist das main
. Zeile 27 setzt dann zwei sogenannte Typeglobs
gleich und sagt sinngemäß: Wann immer du main::quote()
siehst,
interpretiere es als quote()
-- und quote()
ist in diesem Fall gerade
im Paket QuoteDB
, also wird perl
statt
main::quote()
(also quote()
im Hauptprogramm)
einfach QuoteDB::quote()
aufrufen. So arbeitet auch der Exporter
intern. Aber -- pssst! -- das wissen nur die Perl-Haudegen mit
Augenklappe. Bis zum nächsten Mal, Piraten!
01 package QuoteDB; 02 ########################################### 03 # Store and retrieve historical quotes 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict qw(vars subs); 08 09 use Date::Calc qw(Add_Delta_Days 10 check_date); 11 use Finance::QuoteHist; 12 use DB_File; 13 use POSIX; 14 15 our $VERSION = "1.0"; 16 our %QUOTES = (); 17 18 ########################################### 19 sub import { 20 ########################################### 21 my($class, $filename) = @_; 22 23 tie %QUOTES, "DB_File", $filename, 24 O_CREAT, 0644; 25 26 my $callerpkg = caller(0); 27 *{"$callerpkg\::quote"} = *quote; 28 29 1; 30 } 31 32 END { untie %QUOTES; } 33 34 ########################################### 35 sub quote { 36 ########################################### 37 my($symbol, $year, $month, $day) = @_; 38 39 if(!exists $QUOTES{$symbol}) { 40 print "Updating ...\n"; 41 db_update($symbol); 42 } 43 44 return latest_price($symbol, $year, 45 $month, $day); 46 } 47 48 ########################################### 49 sub db_update { 50 ########################################### 51 my($sym) = @_; 52 53 my $q = Finance::QuoteHist->new( 54 symbols => [$sym], 55 start_date => '12/20/1994', 56 end_date => 'today'); 57 58 $QUOTES{$sym}++; 59 60 foreach my $row ($q->quote_get()) { 61 my ($stock, $date, $open, $high, 62 $low, $close, $volume) = @$row; 63 my($year, $month, $day) = 64 map { sprintf "%d", $_ } 65 split(m#/#, $date); 66 $QUOTES{"$sym,$year,$month,$day"} = 67 $close; 68 } 69 } 70 71 ########################################### 72 sub latest_price { 73 ########################################### 74 my($symbol, $year, $month, $day) = @_; 75 76 my @try = ($year, $month, $day); 77 78 my $maxtries = 10; 79 80 while($maxtries--) { 81 while(!check_date($year, $month, 82 $day)) { 83 $day--; 84 } 85 my $key = 86 "$symbol,$year,$month,$day"; 87 if(exists($QUOTES{$key})) { 88 return $QUOTES{$key}; 89 } 90 } continue { 91 ($year, $month, $day) = 92 Add_Delta_Days($year, $month, 93 $day, -1); 94 } 95 96 die "Can't find date (@try)"; 97 } 98 99 1;
Michael Schilliarbeitet 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. |