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

 

Source-Code der Beispiele im Github Repo perl-howto-code.

 

Weitere Posts