Finance::TA - Perl Bibliothek zur Nutzung der Technical Analysis Library (http://ta-lib.org)
Veröffentlicht von Thomas Fahle am (Permalink)
TA-Lib
TA-Lib : Technical Analysis Library ist eine Open-Source Bibliothek, die mehr als 200 Indikatoren der Technischen Analyse, wie z.B. ADX, Parabolic SAR bereitstellt. Darüber hinaus können auch Candlestick-Muster erkannt werden.
Das CPAN Modul Finance::TA - Perl wrapper for Technical Analysis Library (http://ta-lib.org) von KMX ermöglicht den einfachen Zugriff auf die Funktionen/Indikatoren der TA-Lib Bibliothek.
Dieser Beitrag beschäftigt sich zunächst an Hand sehr einfacher Beispiele mit den grundlegenden Datenstrukturen und Funktionen der Bibliothek.
In einem abschließendem Beispiel werden Kursdaten aus einer CSV-Datei gelesen, daraus mehrere Indikatoren berechnet und das Ganze als neue CSV-Datei zur Weiterverarbeitung durch andere Programme gespeichert.
Basics
Finance::TA folgt den Konventionen der TA-Lib C/C++ API. Praktisch alle Funktionen von Finance::TA erwarten folgende Eingabeparameter:
- Referenzen auf Arrays mit Input-Daten.
- den Startindex ($startIdx), ab dem Daten berücksichtig werden sollen. Üblicherweise 0, das erste Element der Input-Daten.
- den Endindex ($endIdx), bis zu dem Daten berücksichtigt werden. Üblicherweise das letzte Element der Input-Daten.
- eine Zeitspanne/Zeitfenster ($optInTimePeriod) für die Berechnungen.
Und liefern folgende Werte zurück:
- Referenzen auf Arrays mit berechneten Output-Daten.
- einen Index ($begIdx), welcher auf den Anfang der Ausgabedaten im Array der Inputdaten verweist.
- einen Return-Code $retCode, der den Erfolg oder Misserfolg des Funktionsaufrufs anzeigt.
Dazu ein einfaches Beispiel - dann sollte es klarer werden.
Beispiel: Ein gleitender Durchschnitt
Das nachfolgende simple Beispiel berechnet mittels der Funktion TA_SMA() einen einfachen gleitenden Durchschnitt über drei Messwerte und erzeugt einen Dump der Ein- und Ausgabedaten und deren Struktur.
#!/usr/bin/perl use strict; use warnings; use feature 'say'; use Finance::TA; use Data::Dumper; # Style 2 (the default) outputs a very readable form which lines up the hash keys. # Style 3 is like style 2, but also annotates the elements of arrays with their index # (but the comment is on its own line, so array output consumes twice the number of lines) $Data::Dumper::Indent = 3; # Data straight from https://metacpan.org/dist/Finance-TA/view/TA.pod my @series = ( '91.500000', '94.815000', '94.375000', '95.095000', '93.780000', '94.625000', '92.530000', '92.750000', '90.315000', '92.470000' ); # https://metacpan.org/dist/Finance-TA/view/TA.pod#TA_SMA-(Simple-Moving-Average) # TA_SMA (Simple Moving Average) # ($retCode, $begIdx, $outReal) = TA_SMA($startIdx, $endIdx, \@inReal, $optInTimePeriod); # my $startIdx = 0; my $endIdx = $#series; my $optInTimePeriod = 3; my ( $retCode, $begIdx, $result ) = TA_SMA( $startIdx, $endIdx, \@series, $optInTimePeriod ); # Die on TA errors die "Error TA_SMA $retCode" unless $retCode == $TA_SUCCESS; # Debug say 'Length @series: ', scalar @series; say 'Length @$result: ', scalar @$result; say "begIdx: $begIdx"; # Dump both arrays say '--- series ---'; say Dumper( \@series ); say '--- result ---'; say Dumper($result);
Das Programm erzeugt folgende Ausgabe:
Length @series: 10 Length @$result: 8 begIdx: 2 --- series --- $VAR1 = [ #0 '91.500000', #1 '94.815000', #2 '94.375000', #3 '95.095000', #4 '93.780000', #5 '94.625000', #6 '92.530000', #7 '92.750000', #8 '90.315000', #9 '92.470000' ]; --- result --- $VAR1 = [ #0 '93.5633333333333', #1 '94.7616666666667', #2 '94.4166666666667', #3 '94.5', #4 '93.645', #5 '93.3016666666667', #6 '91.865', #7 '91.845' ];
Da mindestens drei Messwerte für die Berechnung des gleitenden Durchschnitts benötigt werden, stehen für die ersten beiden Messwerte keine Ergebnisse zur Verfügung.
Die beiden Arrays mit den Eingabe- und Ausgabedaten sind zwar parallel aber unterschiedlich lang und starten mit dem Index 0. Die Variable $begIdx verweist auf den Index im Array der Messwerte ab dem Ergebnisse zur Verfügung stehen. Siehe Skizze.
+++++++++++++++++++++++++++++++++++++++ | | series | | results | +++++++++++++++++++++++++++++++++++++++ | 0 | 91.500000 | | N/A | | 1 | 94.815000 | | N/A | | 2 | 94.375000 | 0 | 93.5633333333333| | 3 | 95.095000 | 1 | 94.7616666666667| | 4 | 93.780000 | 2 | 94.4166666666667| | 5 | 94.625000 | 3 | 94.5 | | 6 | 92.530000 | 4 | 93.645 | | 7 | 92.750000 | 5 | 93.3016666666667| | 8 | 90.315000 | 6 | 91.865 | | 9 | 92.470000 | 7 | 91.845 | +++++++++++++++++++++++++++++++++++++++
Beim Loop über Mess- und berechnete Werte muss man sorgfältig aufpassen, welcher Index zu welchem Array gehört.
# Loop over @series and @$results for ( my $i = 0 ; $i < $begIdx ; $i++ ) { say "$i: $series[$i] --> N/A"; } for ( my $i = $begIdx ; $i <= $#series ; $i++ ) { say "$i: $series[$i] --> $result->[ $i - $begIdx ] "; }
Das Programm erzeugt folgende Ausgabe:
0: 91.500000 --> N/A 1: 94.815000 --> N/A 2: 94.375000 --> 93.5633333333333 3: 95.095000 --> 94.7616666666667 4: 93.780000 --> 94.4166666666667 5: 94.625000 --> 94.5 6: 92.530000 --> 93.645 7: 92.750000 --> 93.3016666666667 8: 90.315000 --> 91.865 9: 92.470000 --> 91.845
Bei nur einem Ergebnis/Berechnung ist das noch gut nachvollziehbar. Bei mehreren Indikatoren und somit mehreren parallelen Arrays unterschiedlicher Länge kann das Ganze recht unübersichtlich werden, wie das folgende Beispiel andeutet.
Beispiel: Zwei gleitende Durchschnitte
Das nachfolgende einfache Beispiel berechnet zwei einfache gleitende Durchschnitte über drei bzw. vier Messwerte und läuft in einer for-Schleife über die Eingangs- und Ausgangsdaten.
#!/usr/bin/perl use strict; use warnings; use Finance::TA; # Data straight from https://metacpan.org/dist/Finance-TA/view/TA.pod my @series = ( '91.500000', '94.815000', '94.375000', '95.095000', '93.780000', '94.625000', '92.530000', '92.750000', '90.315000', '92.470000' ); # https://metacpan.org/dist/Finance-TA/view/TA.pod#TA_SMA-(Simple-Moving-Average) # TA_SMA (Simple Moving Average) # ($retCode, $begIdx, $outReal) = TA_SMA($startIdx, $endIdx, \@inReal, $optInTimePeriod); # my $startIdx = 0; my $endIdx = $#series; # Fast SMA my $optInTimePeriod_fast_sma = 3; my ( $retCode_fast_sma, $begIdx_fast_sma, $result_fast_sma ) = TA_SMA( $startIdx, $endIdx, \@series, $optInTimePeriod_fast_sma ); # Die on TA errors die "Error fast TA_SMA $retCode_fast_sma" unless $retCode_fast_sma == $TA_SUCCESS; # Slow SMA my $optInTimePeriod_slow_sma = 4; my ( $retCode_slow_sma, $begIdx_slow_sma, $result_slow_sma ) = TA_SMA( $startIdx, $endIdx, \@series, $optInTimePeriod_slow_sma ); # Die on TA errors die "Error slow TA_SMA $retCode_slow_sma" unless $retCode_slow_sma == $TA_SUCCESS; my $format = "%s | %10s | %10s | %20s |\n"; printf( $format, 'i', 'value', 'slow_sma', 'fast_sma' ); for ( my $i = 0 ; $i <= $#series ; $i++ ) { my $value = $series[$i]; my $fast_sma = 'N/A'; my $slow_sma = 'N/A'; if ( $i >= $begIdx_fast_sma ) { $fast_sma = $result_fast_sma->[ $i - $begIdx_fast_sma ]; } if ( $i >= $begIdx_slow_sma ) { $slow_sma = $result_slow_sma->[ $i - $begIdx_slow_sma ]; } printf( $format, $i, $value, $slow_sma, $fast_sma ); }
Das Programm erzeugt folgende Ausgabe:
i | value | slow_sma | fast_sma | 0 | 91.500000 | N/A | N/A | 1 | 94.815000 | N/A | N/A | 2 | 94.375000 | N/A | 93.5633333333333 | 3 | 95.095000 | 93.94625 | 94.7616666666667 | 4 | 93.780000 | 94.51625 | 94.4166666666667 | 5 | 94.625000 | 94.46875 | 94.5 | 6 | 92.530000 | 94.0075 | 93.645 | 7 | 92.750000 | 93.42125 | 93.3016666666667 | 8 | 90.315000 | 92.555 | 91.865 | 9 | 92.470000 | 92.01625 | 91.845 |
Die drei Arrays mit den Eingabe- und Ausgabedaten sind zwar parallel aber unterschiedlich lang. Die Variablen $begIdx_slow_sma bzw. $begIdx_fast_sma verweisen auf den Index im Array der Messwerte ab dem jeweils Ergebnisse zur Verfügung stehen.
Beispiel: Kursdaten aus CSV-Datei lesen, mehrere Indikatoren berechnen und als CSV-Datei speichern
Parallele Arrays, dazu noch unterschiedlicher Länge, sind nicht wirklich mein Ding. Ich folge daher dem Ansatz von Tom Christiansen (Perl Style: Use Hashes of Records, not Parallel Arrays) in leicht abgewandelter Form und verwende lieber Arrays von Hashes, die manchmal auch als Arrays von Datensätzen (Records) bezeichnet werden.
Das folgende Beispielprogramm verwendet Text::CSV::Slurp - convert CSV into an array of hashes, or an array of hashes into CSV, das ich bereits hier vorgestellt habe, um eine CSV-Datei mit historischen OHLC-Daten des SPDR S&P 500 ETF Trust (Download) als Referenz auf einen Array von Hashes einzulesen.
Aus diesen CSV-Daten
Date,Open,High,Low,Close,Volume 2005-02-25,98.555,99.7,98.466,99.51,74654349 2005-02-28,99.277,99.406,98.357,98.831,84725874 ... ... ... 2021-08-02,440.34,440.93,437.21,437.59,58783297 2021-08-03,438.44,441.28,436.1,441.15,58053896
wird folgende Datenstruktur erzeugt.
$VAR1 = \[ #0 { 'High' => '99.7', 'Volume' => '74654349', 'Low' => '98.466', 'Open' => '98.555', 'Close' => '99.51', 'Date' => '2005-02-25' }, #1 { 'Volume' => '84725874', 'High' => '99.406', 'Open' => '99.277', 'Low' => '98.357', 'Date' => '2005-02-28', 'Close' => '98.831' }, #2 { 'Volume' => '58054340', 'High' => '99.59', 'Open' => '98.97', 'Low' => '98.97', 'Close' => '99.347', 'Date' => '2005-03-01' }, # ... # ... # ... #4135 { 'Date' => '2021-08-02', 'Close' => '437.59', 'Low' => '437.21', 'Open' => '440.34', 'High' => '440.93', 'Volume' => '58783297' }, #4136 { 'Close' => '441.15', 'Date' => '2021-08-03', 'Open' => '438.44', 'Low' => '436.1', 'Volume' => '58053896', 'High' => '441.28' } ];
Die Datenstruktur wird in weiteren Schritten um zwei gleitende Durchschnitte (TA_SMA()), Average Directional Movement Index ADX (TA_ADX()) und Parabolic SAR (TA_SAR()) erweitert - fehlende Werte werden als undef deklariert - und sieht dann wie folgt aus.
$VAR1 = \[ #0 { 'Slow SMA' => undef, 'Close' => '99.51', 'Fast SMA' => undef, 'Volume' => '74654349', 'ADX' => undef, 'Date' => '2005-02-25', 'PSAR' => undef, 'Low' => '98.466', 'Open' => '98.555', 'High' => '99.7' }, #1 { 'High' => '99.406', 'Open' => '99.277', 'PSAR' => '99.7', 'Date' => '2005-02-28', 'Low' => '98.357', 'ADX' => undef, 'Fast SMA' => undef, 'Volume' => '84725874', 'Slow SMA' => undef, 'Close' => '98.831' }, #2 { 'ADX' => undef, 'Volume' => '58054340', 'Fast SMA' => undef, 'Slow SMA' => undef, 'Close' => '99.347', 'High' => '99.59', 'Open' => '98.97', 'Date' => '2005-03-01', 'PSAR' => '99.67314', 'Low' => '98.97' }, # ... # ... # ... #4135 { 'High' => '440.93', 'Open' => '440.34', 'Low' => '437.21', 'Date' => '2021-08-02', 'PSAR' => '426.506011524495', 'ADX' => '14.3931816759973', 'Volume' => '58783297', 'Fast SMA' => '438.395555555556', 'Slow SMA' => '435.723888888889', 'Close' => '437.59' }, #4136 { 'ADX' => '13.4215432008823', 'Slow SMA' => '436.292222222222', 'Close' => '441.15', 'Volume' => '58053896', 'Fast SMA' => '439.12888888889', 'Open' => '438.44', 'High' => '441.28', 'Low' => '436.1', 'PSAR' => '427.423650833026', 'Date' => '2021-08-03' } ];
Im letzten Schritt werden die Kursdaten und die daraus berechneten Indikatoren (Datenstruktur) in einer weiteren CSV-Datei gespeichert. Das sieht dann in etwa so aus.
Date,Open,Low,High,Close,ADX,PSAR,"Fast SMA","Slow SMA" 2005-02-25,98.555,98.466,99.7,99.51,,,, 2005-02-28,99.277,98.357,99.406,98.831,,99.7,, ... ... ... 2021-08-02,440.34,437.21,440.93,437.59,14.3931816759973,426.506011524495,438.395555555556,435.723888888889 2021-08-03,438.44,436.1,441.28,441.15,13.4215432008823,427.423650833026,439.12888888889,436.292222222222
Hier das Beispielprogramm:
#!/usr/bin/perl use strict; use warnings; use feature 'say'; use Text::CSV::Slurp; use Finance::TA; use Data::Dumper; $Data::Dumper::Indent = 3; # CSV Input my $filename = './spy_d.csv'; # CSV Output my $csv_encoding = 'utf-8'; my $out_filename = './spy_d_indicators.csv'; # CSV Options - see Text::CSV my %csv_options = ( sep_char => ',', binary => '1', ); my $slurp = Text::CSV::Slurp->new(); # Reference to an array of hashes my $data = $slurp->load( file => $filename, %csv_options ); # Reference to an array my $highs = [ map { $_->{High} } @$data ]; # Reference to an array my $lows = [ map { $_->{Low} } @$data ]; # Reference to an array my $closes = [ map { $_->{Close} } @$data ]; my $startIdx = 0; # First element of @$data my $endIdx = $#$data; # Last element of @$data my $retCode; my $begIdx; # TA_ADX (Average Directional Movement Index) # ($retCode, $begIdx, $outReal) = TA_ADX($startIdx, $endIdx, \@high, \@low, \@close, $optInTimePeriod); my $optInTimePeriod_adx = 9; # Defaults to 14 my $adx; ( $retCode, $begIdx, $adx ) = TA_ADX( $startIdx, $endIdx, $highs, $lows, $closes, $optInTimePeriod_adx ); die "Error TA_ADX: $retCode" unless $retCode == $TA_SUCCESS; fill_datastructure( data => $data, results => $adx, begIdx => $begIdx, key => 'ADX' ); # TA_SAR (Parabolic SAR) # ($retCode, $begIdx, $outReal) = TA_SAR($startIdx, $endIdx, \@high, \@low, $optInAcceleration, $optInMaximum); my $optInAcceleration = 0.02; my $optInMaximum = 0.2; my $psar; ( $retCode, $begIdx, $psar ) = TA_SAR( $startIdx, $endIdx, $highs, $lows, $optInAcceleration, $optInMaximum ); die "Error TA_SAR: $retCode" unless $retCode == $TA_SUCCESS; fill_datastructure( data => $data, results => $psar, begIdx => $begIdx, key => 'PSAR' ); # TA_SMA (Simple Moving Average) # ($retCode, $begIdx, $outReal) = TA_SMA($startIdx, $endIdx, \@inReal, $optInTimePeriod); ## Fast SMA my $optInTimePeriod_fast_sma = 9; my $fast_sma; ( $retCode, $begIdx, $fast_sma ) = TA_SMA( $startIdx, $endIdx, $closes, $optInTimePeriod_fast_sma ); die "Error fast TA_SMA $retCode" unless $retCode == $TA_SUCCESS; fill_datastructure( data => $data, results => $fast_sma, begIdx => $begIdx, key => 'Fast SMA' ); ## Slow SMA my $optInTimePeriod_slow_sma = 18; my $slow_sma; ( $retCode, $begIdx, $slow_sma ) = TA_SMA( $startIdx, $endIdx, $closes, $optInTimePeriod_slow_sma ); die "Error slow TA_SMA $retCode" unless $retCode == $TA_SUCCESS; fill_datastructure( data => $data, results => $slow_sma, begIdx => $begIdx, key => 'Slow SMA' ); # Debug #say Dumper( \$data ); # Create Output CSV my $field_order = [ 'Date', 'Open', 'Low', 'High', 'Close', 'ADX', 'PSAR', 'Fast SMA', 'Slow SMA' ]; my $csv = $slurp->create( input => $data, field_order => $field_order, %csv_options ); open my $fh, ">:encoding($csv_encoding)", "$out_filename" or die "$!"; print $fh $csv; close($fh) or die $!; exit(); =head1 SUBROUTINES/METHODS =cut =head2 fill_datastructure fill_datastructure(data => $data, results => $results, begIdx => $begIdx, key => 'string'); Fills datastructure C<@$data> with values from Finance:TA C<$results> using C<key> as hash key. Slots below C<$begIdx> will be filled with C<undef> values. Will warn on overwriting existing hash keys. =cut sub fill_datastructure { my %args = @_; my $data_ref = $args{data}; my $results = $args{results}; my $begIdx = $args{begIdx}; my $key = $args{key}; # Check params die("Missing arg 'data'.") unless $data_ref; die("Missing arg 'results'.") unless $results; die("Missing arg 'begIdx'.") unless defined $begIdx; die("Missing arg 'key'.") unless $key; # Minimal params validation unless ( ref($data_ref) eq 'ARRAY' ) { die('No ARRAY reference found for $data.'); } unless ( ref($results) eq 'ARRAY' ) { die('No ARRAY reference found for $results.'); } # Fill datastructure for ( my $i = 0 ; $i <= $#$data_ref ; $i++ ) { # Emitt warning on overwriting data if ( exists $data_ref->[$i]->{$key} ) { warn("Overwriting data key '$key' at '$i'"); } my $value = undef; if ( $i >= $begIdx ) { $value = $results->[ $i - $begIdx ]; } $data_ref->[$i]->{$key} = $value; } } __END__
That's it.
Siehe auch
- Finance::TA - Perl wrapper for Technical Analysis Library (http://ta-lib.org)
- TA-Lib: Technical Analysis Library
- TA-Lib: C/C++ API Documentation
- Github: perl-finance-ta
- PDL::Finance::TA - Technical Analysis Library (http://ta-lib.org) bindings for PDL
- Alien::TALib - Perl extension to install TA-lib
- TA-LIB und Finance::TA auf Ubuntu installieren
Source-Code der Beispiele im Github Repo perl-howto-code.