Day 7: Plotting PDL data in the browser using Javascript

Introduction

In a previous post we saw how to create a trading strategy using PDL::Finance::TA and PDL with Perl.

You may have noticed that I used PDL::Graphics::Gnuplot to do the plotting there. This module requires the user to install Gnuplot on their desktop or server, which may be a limitation in some use cases.

Let us assume you were creating a webservice where you were running some back-end PDL code, you may want to use a Javascript based plotting function. Maybe you want to use your favorite Javascript plotting library to do the plots. Maybe you want to do some live plotting based on some user interactions.

If this is your use case, this blog post will describe how to accomplish that. We continue to use the same example as in the previous blog post related to creating buy and sell trading signals for a trading strategy. But I will show you how to modify the plotting code to generate custom Javascript.

Pre-requisites

We have some additional pre-requisites to accomplish this task, so consolidating all of them into one set of commands as below.

This is for Ubuntu or Debian based Linux. It will be similar for other Unixes or Windows.

## you need Perl installed and Gnuplot installed.
$ sudo apt -y install gnuplot perl perl-modules cpanminus liblocal-lib-perl
## set your local Perl install to $HOME/perl5
$ mkdir -p ~/perl5/lib/perl5
### add this oneliner to the ~/.bashrc or ~/.profile for your terminal
$ eval $(perl -I ~/perl5/lib/perl5 -Mlocal::lib)
$ cpanm PDL PDL::Graphics::Gnuplot PDL::Finance::TA JSON::XS         LWP::UserAgent DateTime Path::Tiny Template::Toolkit Browser::Open
## sometimes this module does not pass the tests
$ cpanm -f Finance::QuoteHist
## check if PDL got installed
$ which perldl

What are we really doing ?

The premise of the plotting library is quite simple. We will create a template file with the HTML required to plot the chart, and auto-generate some JSON data from the PDL objects into the template file using the Template::Toolkit module, process the template and generate a local HTML file, and then use the Browser::Open module to open the locally created HTML file using the file:// scheme.

HighCharts

HighCharts is a very popular open-source and commercial Javascript plotting library used in the finance industry, and also in various other industries.

Their library is easy to use and has a ton of documentation. If you prefer something else like ZingCharts or ChartJS the concepts described here continue to apply.

All you will do in that case is rewrite the template file with the correct Javascript implementation for that library.

For purposes of this blog post, HighCharts will be used.

HighCharts Objects in Perl

We now create custom Perl objects that will then be converted to JSON and processed with the template.

HighCharts plots financial time series data, where the timestamp is in milliseconds.

We will be referencing the code from the previous post here.

Create the Open-High-Low-Close chart object

It is actually really easy to convert PDL to JSON for HighCharts.

We create a PDL object with the timestamp, open price, high price, low price and close price in that order. We then transpose the PDL since the PDL is stored column-wise and we need to convert it to row-wise. Then we call the magical unpdl function which converts the PDL object into a Perl array of arrays, and then we invoke encode_json from the JSON::XS module to convert it to JSON.

After that we create a HASH object that will be passed as a configuration option in the template (shown in the next section), with information such as data which points to the generated JSON. Note the type of the chart here is candlestick. If you want to just plot the close price, you would create a PDL with the timestamp and the close price, and set the type of the chart to line.

The code for this is shown below. We chose not to clean any bad values for PDL since the price data is assumed clean and it actually is.

## plot the data using Javascript in a Browser
## we have to create multiple objects
my @charts = ();
## convert the PDL to javascript and write to a file
## HighCharts requires timestamp in milliseconds
## each object should have the 5 dimensions: timestamp_ms, open, high, low, close - hence we transpose the PDL
my $px_pdl_js = encode_json pdl($timestamp * 1000, $open_px, $high_px, $low_px, $close_px)->transpose->unpdl;
push @charts, {
    title => $symbol,
    data => $px_pdl_js,
    type => 'candlestick',
    id => lc "candlestick-$symbol",
    y_axis => 0,
};

Create the Bollinger Bands objects

Once you have generated the Bollinger Band indicator PDLs, we have to convert them to JSON too. However, the Bollinger Bands are lagging indicators which means that some data points will be blank or marked as bad by PDL::Finance::TA. We need to remove those bad values completely from the JSON and that is done as below.

First we create a PDL with 2 dimensions: timestamp in milliseconds and the upper (or middle or lower) Bollinger band PDL. We then transpose it to get it in row-major form.

Then we collect indices of the resulting PDL, $bb_upper_2 in the below code, where the 2nd dimension values are not zero and not bad using the which function.

We then create a new PDL $bb_upper_clean that will use the dice_axis call and remove all the entries that do not exist in the $bbu_idx indices list.

This new variable $bb_upper_clean is now our clean PDL and we call unpdl on it to convert it to a Perl array of arrays, and then encode it to JSON using encode_json from JSON::XS.

We repeat this process for the lower and middle Bollinger band PDLs.

Lastly, we create chart objects with these cleaned PDL-to-JSON encoded values in the data field and the type fields are set to line.

## add the indicator chart. Bollinger Bands are on the same axis as the price, so y_axis is 0
## we need to remove the BAD values completely from the new PDL
my $bb_upper_2 = pdl($timestamp * 1000, $bb_upper)->transpose;
my $bbu_idx = $bb_upper_2((1))->which;
my $bb_upper_clean = $bb_upper_2->dice_axis(1, $bbu_idx);
my $bb_upper_js = encode_json $bb_upper_clean->unpdl;

my $bb_middle_2 = pdl($timestamp * 1000, $bb_middle)->transpose;
my $bbm_idx = $bb_middle_2((1))->which;
my $bb_middle_clean = $bb_middle_2->dice_axis(1, $bbm_idx);
my $bb_middle_js = encode_json $bb_middle_clean->unpdl;

my $bb_lower_2 = pdl($timestamp * 1000, $bb_lower)->transpose;
my $bbl_idx = $bb_lower_2((1))->which;
my $bb_lower_clean = $bb_lower_2->dice_axis(1, $bbl_idx);
my $bb_lower_js = encode_json $bb_lower_clean->unpdl;

push @charts, {
    title => 'Bollinger Band - Upper',
    type => 'line',
    data => $bb_upper_js,
    id => lc "bb-upper-$symbol",
}, {
    title => 'Bollinger Band - Middle',
    type => 'line',
    data => $bb_middle_js,
    id => lc "bb-middle-$symbol",
}, {
    title => 'Bollinger Band - Lower',
    type => 'line',
    data => $bb_lower_js,
    id => lc "bb-lower-$symbol",
};

Create the Buy and Sell signal objects

Similarly, we clean the buy and sell signal PDL objects and encode them to JSON.

What's different here is that the marker_symbol chosen is triangle (upward triangle), and the marker_color chosen is green for the buy signal. Likewise, the marker color for the sell signal is red and the marker symbol is triangle-down (downward triangle). These are marker types specified in the Highcharts documentation.

The type is still line. What this combination does is generate a line chart connecting the buy signals and the sell signals but draws extra markers on them to show the buy and sell trades.

In the template (shown in the next section), we actually set the line-width to 0 so that the unnecessary line is not drawn. We do that by using the is_signal flag which the template will process.

## for buys and sells we just want to avoid empty data
my $buys_2 = pdl($timestamp * 1000, $buys)->transpose;
my $bidx = $buys_2((1))->which;## check if !0 is true
my $clean_buys = $buys_2->dice_axis(1, $bidx);
my $buys_js = encode_json $clean_buys->unpdl;
push @charts, {
    title => 'Buy Signals',
    data => $buys_js,
    y_axis => 0,
    type => 'line',
    marker_symbol => 'triangle',
    marker_color => 'green',
    is_signal => 1,
};

my $sells_2 = pdl($timestamp * 1000, $sells)->transpose;
my $sidx = $sells_2((1))->which;## check if !0 is true
my $clean_sells = $sells_2->dice_axis(1, $sidx);
my $sells_js = encode_json $clean_sells->unpdl;
push @charts, {
    title => 'Sell Signals',
    data => $sells_js,
    y_axis => 0,
    type => 'line',
    marker_symbol => 'triangle-down',
    marker_color => 'red',
    is_signal => 1,
};

The Template Creation

As per the documentation provided by the Template Toolkit developers, we create an HTML file, and save it in the __DATA__ section of our script.

You may also save it as a file with .tt extension and have the template processor read the file as opposed to a string. For purposes of this blog post, we will store the content in the __DATA__ section of our script.

A sample template that works is provided below. As you can see, the <body> tag does not contain anything except for a <div> container where we will plot the chart.

The title of the page is referenced by a variable page.title. Since we are plotting financial data, we will reference the HighStock product from HighCharts.

If you are not plotting financial data, you may be able to just use the HighCharts base product.

Looking at the <script> block where we generate a chart based on some inputs, we can see that there is mainly a single call to the HighCharts library.

The variable chart.charts has an array of chart objects that will add multiple plots to the same chart.

The variable chart.title defines the chart title.

Each chart object, has a type which could be lines, points, candlesticks etc. as supported by the HighCharts library. The data object is what we will be transmitting from the Perl code that has the PDL objects and converting it into JSON for the Javascript code to accept.

Note the is_signal conditional block, which adds additional options for the buy and sell signals to be rendered on the plot. In the previous section, we saw that we were setting the is_signal to 1 for those chart objects.

The rest of the options are based on what HighCharts library requires, and we recommend referring to their documentation.

NOTE: HighCharts does not accept PDL bad values, which get transmitted as strings, so we ended up doing some additional cleanup in the Perl code to handle this (as shown in the sections above).

<!DOCTYPE HTML>
<html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="copyright" content="App::financeta Authors">
    <meta name="author" content="Vikas N Kumar <vikas@cpan.org>">
    <meta name="description" content="App::financeta">
    <link rel="icon" href="chart-line-solid.png" type="image/png">
    <title>[% page.title %]</title>
    <script src="https://code.highcharts.com/stock/highstock.js"></script>
    <style>
    #chart-container {
        min-width: 600px;
        min-height: 400px;
        height: [% chart.height %];
        width: 95%;
        margin: 20px;
    };
    </style>
    </head>
    <body>
        <h1>[% page.title %]</h1>
        <hr/>
        <div id="chart-container">
        </div>
        <hr/>
    <script type="text/javascript">
        [% IF chart %]
        window.chart = new Highcharts.stockChart('chart-container', {
            accessibility: { enabled: false },
            yAxis: [{
                    labels: { align: 'left' },
                    height: "400px",
                    resize: { enabled: true },
            }],
            title: { text: "[% chart.title %]" },
            series:[
            [% FOREACH el IN chart.charts %]
                {
                    type: "[% el.type %]",
                    name: "[% el.title %]",
                    id: "[% el.id %]",
                    data: [% el.data %],
                    [% IF el.y_axis %]
                    yAxis: [% el.y_axis %],
                    [% END %]
                    [% IF el.is_signal %]
                    lineWidth: 0,
                    showInLegend: true,
                    marker: {
                        enabled: true,
                        fillColor: "[% el.marker_color %]",
                        radius: 4,
                        symbol: "[% el.marker_symbol %]",
                    },
                    [% END %]
                    [% IF el.type == 'area' %]
                    color: 'green',
                    negativeColor: 'red',
                    threshold: 0,
                    marker: { enabled: true },
                    [% END %]
                },
            [% END %]
            ],
            responsive: {
                rules: [{
                    condition: { maxWidth: 800 },
                    chartOptions: {
                        rangeSelector: {
                            inputEnabled: false
                        }
                    }
                }]
            }
        });
        [% END %]
    </script>
    </body>
</html>

Processing the Template

To process the template we will load the contents of the template from the __DATA__ section and then process it in-place in the script. To do that you create an object for the Template module and then call the process function with the first argument being the template content, the second argument being the variables that the template will process such as chart and page in our case, and the third option will be the HTML file name to be generated.

We have the @charts variable as generated in the previous section for plotting the HighCharts chart data. The $ttconf variable is a HASH object that holds all the variables that the template needs.

We have $symbol which is the symbol of the prices we have downloaded as per the previous post. In our case it is the string DOGEUSD.

We then load the __DATA__ contents into $ttcontent. We also save that to a file named pdlchart.tt. We create a path for pdlchart.html using the Path::Tiny module and use that in the process() call.

We then invoke the process() function with the pdlchart.tt full path as the first argument, the $ttconf as the second argument and the $htmlfile string.

If the processing succeeds, we then open the created HTML file in the browser using the Browser::Open module.

Below is the code for this.

## create variables to pass to the template
my $ttconf = {
    page => { title => "Plot $symbol with HighCharts" },
    chart => { height => "600px", charts => \@charts, title => $symbol },
};
## load a pre-designed Template file 
my $ttcontent = do { local $/ = undef; <DATA> };
## dump it as a template file for the browser to load it
my $ttfile = path('pdlchart.tt')->realpath;
path($ttfile)->spew($ttcontent) unless -e $ttfile;
print "TTFile: $ttfile\n";
my $htmlfile = path('pdlchart.html')->realpath;
print "HTMLFile: $htmlfile\n";

my $tt = Template->new({ ABSOLUTE => 1 });
my $ret = $tt->process("$ttfile", $ttconf, "$htmlfile", { binmode => ':utf8' });
if ($ret) {
    my $url = "file://$htmlfile";
    print "opening $url\n";
    my $ok = Browser::Open::open_browser($url, 1);
    if (not defined $ok or $ok != 0) {
        die "Failed to open $url in a browser. Return value: $ok";
    } else {
        print "Successfully opened $url in browser\n";
    }
} else {
    die "Error processing template $ttfile: " . $tt->error() . "\n";
}

Plot Screenshot

Below is a screenshot of the full script that you would see in the browser, along with the Bollinger Bands and the buy and sell signals.

Plotting HighCharts with PDL - Screenshot

The Full Script

We now reference most of the script from the previous post here, until the plotting section.

NOTE: We have removed the dependency of this script on PDL::Finance::Gnuplot and instead added Template and Browser::Open as imported modules.

#!/usr/bin/env perl
use strict;
use warnings;
use PDL;
use PDL::NiceSlice;
use PDL::Finance::TA;
use JSON::XS qw(decode_json encode_json);
use LWP::UserAgent;
use DateTime;
use Try::Tiny;
use Path::Tiny;
use Template;
use Browser::Open;

sub get_data($) {
    my $symbol = shift;
    my $filename = lc "$symbol.json";
    my $content;
    my $qdata;
    my $url = sprintf("https://api.gemini.com/v2/candles/%s/%s", lc $symbol, '1day');
    if (-e $filename) {
        print "Found $filename, loading data from that\n";
        $content = path($filename)->slurp;
    } else {
        my $lwp = LWP::UserAgent->new(timeout => 60);
        $lwp->env_proxy;
        my $resp = $lwp->get($url);
        if ($resp->is_success) {
            $content = $resp->decoded_content;
            path($filename)->spew($content);
        } else {
            warn "Error from request to $url: " . $resp->status_line;
            return undef;
        }
    }
    if (defined $content and length($content)) {
        my $jquotes = decode_json $content;
        if (ref $jquotes eq 'ARRAY' and scalar(@$jquotes)) {
            ## sort quotes by timestamp
            my @sorted = sort { $a->[0] <=> $b->[0] } @$jquotes;
            foreach my $q (@sorted) {
                ## timestamp is the first column in milliseconds
                $q->[0] /= 1000;
            }
            ## convert the quotes to a PDL
            $qdata = pdl(@sorted)->transpose;
        } else {
            warn "No quotes returned by $url or $filename";
            $qdata = undef;
        }
    } else {
        warn "No content received from $url or $filename";
        $qdata = undef;
    }
    ## now we operate on the $qdata PDL object
    return $qdata;
}

my $symbol = $ARGV[0] // 'DOGEUSD';
my $qdata = get_data($symbol);
die "Unable to get data for $symbol" unless ref $qdata eq 'PDL';
print $qdata;

my $timestamp = $qdata(, (0));
my $open_px = $qdata(, (1));
my $high_px = $qdata(, (2));
my $low_px = $qdata(, (3));
my $close_px = $qdata(, (4));
## use the default values
## each of these are 1-D PDLs
my ($bb_upper, $bb_middle, $bb_lower) = PDL::ta_bbands($close_px, 5, 2, 2, 0);
my $buys            = zeroes( $close_px->dims );
my $sells           = zeroes( $close_px->dims );
## use a 1 tick lookback
my $lookback        = 1;
## calculate the indexes of the lookback PDL based on LOW price
my $idx_0           = xvals( $low_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_0 = $idx_0->setbadif( $idx_0 < 0 )->setbadtoval(0);
## get the indexes of when the LOW Price < Lower Bollinger Band based on the lookback
my $idx_1 = which( 
        ($low_px->index($idx_0) > $bb_lower->index($idx_0)) &
        ($low_px < $bb_lower)
);
## set the buys to be on the OPEN price for those indexes
$buys->index($idx_1) .= $open_px->index($idx_1);
## set all 0 values to BAD to avoid plotting zeroes
$buys->inplace->setvaltobad(0);

## calculate the indexes of the lookback PDL based on HIGH price
my $idx_2 = xvals( $high_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_2 = $idx_2->setbadif( $idx_2 < 0 )->setbadtoval(0);
## get the indexes of when the HIGH Price > Upper Bollinger Band based on the lookback
my $idx_3 = which(
    ($high_px->index($idx_2) < $bb_upper->index($idx_2)) &
    ($high_px > $bb_upper )
);
## set the sells to be on the CLOSE price for those indexes
$sells->index($idx_3) .= $close_px->index($idx_3);
## set all 0 values to BAD to avoid plotting zeroes
$sells->inplace->setvaltobad(0);

## plot the data using Javascript in a Browser
## we have to create multiple objects
my @charts = ();
## convert the PDL to javascript and write to a file
## HighCharts requires timestamp in milliseconds
## each object should have the 5 dimensions: timestamp_ms, open, high, low, close - hence we transpose the PDL
my $px_pdl_js = encode_json pdl($timestamp * 1000, $open_px, $high_px, $low_px, $close_px)->transpose->unpdl;
push @charts, {
    title => $symbol,
    data => $px_pdl_js,
    type => 'candlestick',
    id => lc "candlestick-$symbol",
    y_axis => 0,
};
## add the indicator chart. Bollinger Bands are on the same axis as the price, so y_axis is 0
## we need to remove the BAD values completely from the new PDL
my $bb_upper_2 = pdl($timestamp * 1000, $bb_upper)->transpose;
my $bbu_idx = $bb_upper_2((1))->which;
my $bb_upper_clean = $bb_upper_2->dice_axis(1, $bbu_idx);
my $bb_upper_js = encode_json $bb_upper_clean->unpdl;

my $bb_middle_2 = pdl($timestamp * 1000, $bb_middle)->transpose;
my $bbm_idx = $bb_middle_2((1))->which;
my $bb_middle_clean = $bb_middle_2->dice_axis(1, $bbm_idx);
my $bb_middle_js = encode_json $bb_middle_clean->unpdl;

my $bb_lower_2 = pdl($timestamp * 1000, $bb_lower)->transpose;
my $bbl_idx = $bb_lower_2((1))->which;
my $bb_lower_clean = $bb_lower_2->dice_axis(1, $bbl_idx);
my $bb_lower_js = encode_json $bb_lower_clean->unpdl;

push @charts, {
    title => 'Bollinger Band - Upper',
    type => 'line',
    data => $bb_upper_js,
    id => lc "bb-upper-$symbol",
}, {
    title => 'Bollinger Band - Middle',
    type => 'line',
    data => $bb_middle_js,
    id => lc "bb-middle-$symbol",
}, {
    title => 'Bollinger Band - Lower',
    type => 'line',
    data => $bb_lower_js,
    id => lc "bb-lower-$symbol",
};

## for buys and sells we just want to avoid empty data
my $buys_2 = pdl($timestamp * 1000, $buys)->transpose;
my $bidx = $buys_2((1))->which;## check if !0 is true
my $clean_buys = $buys_2->dice_axis(1, $bidx);
my $buys_js = encode_json $clean_buys->unpdl;
push @charts, {
    title => 'Buy Signals',
    data => $buys_js,
    y_axis => 0,
    type => 'line',
    marker_symbol => 'triangle',
    marker_color => 'green',
    is_signal => 1,
};

my $sells_2 = pdl($timestamp * 1000, $sells)->transpose;
my $sidx = $sells_2((1))->which;## check if !0 is true
my $clean_sells = $sells_2->dice_axis(1, $sidx);
my $sells_js = encode_json $clean_sells->unpdl;
push @charts, {
    title => 'Sell Signals',
    data => $sells_js,
    y_axis => 0,
    type => 'line',
    marker_symbol => 'triangle-down',
    marker_color => 'red',
    is_signal => 1,
};
## create variables to pass to the template
my $ttconf = {
    page => { title => "Plot $symbol with HighCharts" },
    chart => { height => "600px", charts => \@charts, title => $symbol },
};
## load a pre-designed Template file 
my $ttcontent = do { local $/ = undef; <DATA> };
## dump it as a template file for the browser to load it
my $ttfile = path('pdlchart.tt')->realpath;
path($ttfile)->spew($ttcontent) unless -e $ttfile;
print "TTFile: $ttfile\n";
my $htmlfile = path('pdlchart.html')->realpath;
print "HTMLFile: $htmlfile\n";

my $tt = Template->new({ ABSOLUTE => 1 });
my $ret = $tt->process("$ttfile", $ttconf, "$htmlfile", { binmode => ':utf8' });
if ($ret) {
    my $url = "file://$htmlfile";
    print "opening $url\n";
    my $ok = Browser::Open::open_browser($url, 1);
    if (not defined $ok or $ok != 0) {
        die "Failed to open $url in a browser. Return value: $ok";
    } else {
        print "Successfully opened $url in browser\n";
    }
} else {
    die "Error processing template $ttfile: " . $tt->error() . "\n";
}

__DATA__
<!DOCTYPE HTML>
<html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="copyright" content="App::financeta Authors">
    <meta name="author" content="Vikas N Kumar <vikas@cpan.org>">
    <meta name="description" content="App::financeta">
    <link rel="icon" href="chart-line-solid.png" type="image/png">
    <title>[% page.title %]</title>
    <script src="https://code.highcharts.com/stock/highstock.js"></script>
    <style>
    #chart-container {
        min-width: 600px;
        min-height: 400px;
        height: [% chart.height %];
        width: 95%;
        margin: 20px;
    };
    </style>
    </head>
    <body>
        <h1>[% page.title %]</h1>
        <hr/>
        <div id="chart-container">
        </div>
        <hr/>
    <script type="text/javascript">
        [% IF chart %]
        window.chart = new Highcharts.stockChart('chart-container', {
            accessibility: { enabled: false },
            yAxis: [{
                    labels: { align: 'left' },
                    height: "400px",
                    resize: { enabled: true },
            }],
            title: { text: "[% chart.title %]" },
            series:[
            [% FOREACH el IN chart.charts %]
                {
                    type: "[% el.type %]",
                    name: "[% el.title %]",
                    id: "[% el.id %]",
                    data: [% el.data %],
                    [% IF el.y_axis %]
                    yAxis: [% el.y_axis %],
                    [% END %]
                    [% IF el.is_signal %]
                    lineWidth: 0,
                    showInLegend: true,
                    marker: {
                        enabled: true,
                        fillColor: "[% el.marker_color %]",
                        radius: 4,
                        symbol: "[% el.marker_symbol %]",
                    },
                    [% END %]
                    [% IF el.type == 'area' %]
                    color: 'green',
                    negativeColor: 'red',
                    threshold: 0,
                    marker: { enabled: true },
                    [% END %]
                },
            [% END %]
            ],
            responsive: {
                rules: [{
                    condition: { maxWidth: 800 },
                    chartOptions: {
                        rangeSelector: {
                            inputEnabled: false
                        }
                    }
                }]
            }
        });
        [% END %]
    </script>
    </body>
</html>

Day 6: How to use PDL::Finance::TA to develop a trading strategy

Introduction

So you are a Perl developer and you see a lot of people make money on the stock market and think you could do that too. You could use third party tools provided by the trading platforms, you could use Python with its various related tools, you could use R with its RQuantLib, you could do C++ with quantlib and other similar methods. Maybe you want to use machine learning on the stock price data, or want to just gamble by guessing or following the herd. TIMTOWTDI applies here.

But you decided you want to use Perl, so for that scenario you can use PDL and the financial technical analysis library PDL::Finance::TA, which wraps the TA-lib C++ library. Technical Analysis, also known sometimes by detractors as astrology for traders, can be a useful way to generate buy and sell trading signals for a stock that you may be interested in trading. It consists of invoking a set of functions, that maybe statistical or numerical in nature, to create indicators that generate such trading signals or provide a hint for the trader to make a trade. However, these indicators are always lagging indicators because they cannot predict the future, they can only be based on the past data, similar to what a machine learning model does.

That's why you see disclaimers like past performance is not indicative of future results in your broker's statements or in advertisements.

In this post, I show you how to start using PDL::Finance::TA to test out some theories that you can experiment with. A module named App::financeta exists that is a desktop GUI product that allows you to do this in an easier fashion without writing any code, but for this post we describe how to develop simple functions to do this from scratch. Maybe you want to embed this kind of functionality into a website you already have developed.

Pre-requisites

Let's first install all the prerequisites using App::cpanminus, which is what I use on Linux. This code has been tested on Ubuntu 22.04 LTS and Debian 11. If you find an issue on other types of Linux or on Windows, please inform me.

## you need Perl installed and Gnuplot installed.
$ sudo apt -y install gnuplot perl perl-modules cpanminus liblocal-lib-perl
## set your local Perl install to $HOME/perl5
$ mkdir -p ~/perl5/lib/perl5
### add this oneliner to the ~/.bashrc or ~/.profile for your terminal
$ eval $(perl -I ~/perl5/lib/perl5 -Mlocal::lib)
$ cpanm PDL PDL::Graphics::Gnuplot PDL::Finance::TA JSON::XS         LWP::UserAgent DateTime Path::Tiny
## sometimes this module does not pass the tests
$ cpanm -f Finance::QuoteHist
## check if PDL got installed
$ which perldl

Get Pricing Data from Yahoo Finance

Before we start we need to download some pricing data. You can either use Yahoo Finance and download a CSV, or we can use a web request with Finance::QuoteHist.

Below we show code to download a stock like NVDA from Yahoo Finance and convert to PDL object.

use DateTime;
use Finance::QuoteHist;
use PDL;
use PDL::NiceSlice;
my $finq = Finance::QuoteHist->new(
        symbols => ['NVDA'],
        start_date => '1 year ago',
        end_date => 'today',
        auto_proxy => 1,
        );
my @quotes = ();
foreach my $row ($finq->quotes) {
    my ($sym, $date, $o, $h, $l, $c, $vol) = @$row;
    ## date is in YYYY/MM/DD format
    my ($yy, $mm, $dd) = split /\//, $date;
    ## the data is NASDAQ/NYSE specific
    my $epoch = DateTime->new(
        year => $yy, month => $mm, day => $dd,
        hour => 16, minute => 0, second => 0,
        time_zone => 'America/New_York')->epoch;
    push @quotes, pdl($epoch, $o, $h, $l, $c, $vol);
}
$finq->clear_cache;
## convert the array of PDLs to a single 6-D PDL
my $qdata = pdl(@quotes)->transpose;
## now we operate on the $qdata PDL object

In simple terms, the above code downloads 1 year of open, high, low, close and volume data for the NVDA (NVIDIA) stock symbol from Yahoo Finance and is converted to a 6-dimension PDL to be used for the next steps.

Get Pricing Data from Gemini Exchange

If you want to trade cryptocurrencies, the Gemini Exchange provides a free public REST API that we can use to get data from using LWP::UserAgent.

Here is a link to the candles REST API for Gemini which we will be using to get the open, high, low, close and volume data for a cryptocurrency such as DOGEUSD (Dogecoin).

use LWP::UserAgent
use PDL;
use PDL::NiceSlice;
use JSON::XS qw(decode_json);

my $url = sprintf("https://api.gemini.com/v2/candles/%s/%s", 'dogeusd', '1day');
my $lwp = LWP::UserAgent->new(timeout => 60);
$lwp->env_proxy;
my $resp = $lwp->get($url);
my $qdata;
if ($resp->is_success) {
    my $content = $resp->decoded_content;
    if (defined $content and length($content)) {
        my $jquotes = decode_json $content;
        if (ref $jquotes eq 'ARRAY' and scalar(@$jquotes)) {
            ## sort quotes by timestamp
            my @sorted = sort { $a->[0] <=> $b->[0] } @$jquotes;
            foreach my $q (@sorted) {
                ## timestamp is the first column in milliseconds
                $q->[0] /= 1000;
            }
            ## convert the quotes to a PDL
            $qdata = pdl(@sorted)->transpose;
        } else {
            warn "No quotes returned by $url";
            $qdata = undef;
        }
    } else {
        warn "No content received from $url";
        $qdata = undef;
    }
} else {
    warn "Error from request to $url: " . $resp->status_line;
    $qdata = undef;
}
##
die "Unable to get data for dogeusd" unless ref $qdata eq 'PDL';
## now we operate on the $qdata PDL object

Plot the Quotes using PDL::Graphics::Gnuplot

In this section we will use the $qdata variable and PDL::Graphics::Gnuplot to plot the prices on a chart to view them.

use PDL;
use PDL::NiceSlice;
use PDL::Graphics::Gnuplot;
## let's assume all the data has been loaded into $qdata variable as in the
## above sections.

## create a default Gnuplot window
my $pwin = gpwin(size => [ 1024, 768, 'px' ]);
## now that the window is created, reset it anyway
$pwin->reset();
$pwin->multiplot();
$pwin->plot({
        object => '1 rectangle from screen 0,0 to screen 1,1 fillcolor rgb "black" behind',
        title => ["DOGEUSD Open-High-Low-Close", textcolor => 'rgb "white"'],
        key => ['on', 'outside', textcolor => 'rgb "yellow"'],
        border => 'linecolor rgbcolor "white"',
        xlabel => ['Date', textcolor => 'rgb "yellow"'],
        ylabel => ['Price', textcolor => 'rgb "yellow"'],
        xdata => 'time',
        xtics => {format => '%Y-%m-%d', rotate => -90, textcolor => 'orange', },
        ytics => {textcolor => 'orange'},
        label => [1, 'DOGEUSD', textcolor => 'rgb "cyan"', at => "graph 0.90,0.03"],
    },
    {
        with => 'financebars',
        linecolor => 'white',
        legend => 'Price',
    },
    $qdata(,(0)), #timestamp
    $qdata(,(1)), #open
    $qdata(,(2)), #high
    $qdata(,(3)), #low
    $qdata(,(4)), #close
    );
$pwin->end_multi;
$pwin->pause_until_close;

Run Indicators

Now that we have pricing data stored in the $qdata variable we will show how to use PDL::Finance::TA and PDL::Graphics::Gnuplot to generate some trading signals and indicators.

The PDL that we have has 6 dimensions: timestamp, open price, high price, low price, close price and trading volume. Different data providers have different meanings for volume, but we will assume you can refer to their documentation for more details.

First we try simple indicators like Bollinger Bands which does a moving average around the variable and noisy price distribution with a standard deviation that can be configured. We will use 2 standard deviations in our code below.

The PDL::Finance::TA function that implements Bolling Bands is called ta_bbands. Here's how the code would look if we were to invoke this indicator with some default values on the $qdata variable.

use PDL;
use PDL::NiceSlice;
use PDL::Finance::TA;

## load data as PDL into $qdata as described above

## use the close price
my $timestamp = $qdata(, (0));
my $open_px = $qdata(, (1));
my $high_px = $qdata(, (2));
my $low_px = $qdata(, (3));
my $close_px = $qdata(, (4));
## use the default values
my ($bb_upper, $bb_middle, $bb_lower) = PDL::ta_bbands($close_px, 5, 2, 2, 0);

## plot the data
my $pwin = gpwin(size => [1024, 768, 'px']);
$pwin->reset;
$pwin->multiplot;
$pwin->plot({
        object => '1 rectangle from screen 0,0 to screen 1,1 fillcolor rgb "black" behind',
        title => ["$symbol Open-High-Low-Close", textcolor => 'rgb "white"'],
        key => ['on', 'outside', textcolor => 'rgb "yellow"'],
        border => 'linecolor rgbcolor "white"',
        xlabel => ['Date', textcolor => 'rgb "yellow"'],
        ylabel => ['Price', textcolor => 'rgb "yellow"'],
        xdata => 'time',
        xtics => {format => '%Y-%m-%d', rotate => -90, textcolor => 'orange', },
        ytics => {textcolor => 'orange'},
        label => [1, $symbol, textcolor => 'rgb "cyan"', at => "graph 0.90,0.03"],
    },
    {
        with => 'financebars',
        linecolor => 'white',
        legend => 'Price',
    },
    $timestamp, $open_px, $high_px, $low_px, $close_px,
    ### Bollinger Bands plot
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-green',
        legend => 'Bollinger Band - Upper'
    },
    $timestamp, $bb_upper, #upper band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-magenta',
        legend => 'Bollinger Band - Lower'
    },
    $timestamp, $bb_lower, #lower band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'orange',
        legend => 'Bollinger Band - Middle'
    },
    $timestamp, $bb_middle, #middle band
);
$pwin->end_multi;
$pwin->pause_until_close;

Similarly, you can read the documentation of PDL::Finance::TA and pick whatever indicators you would like to plot.

Generate Buy or Sell Signals

Now let's take the case where we want to buy the security (whether a stock or a cryptocurrency), when the low price crosses the lower Bollinger band and sell when the high price crosses the upper Bollinger band.

To do that we will write the below PDL code.

use PDL;
use PDL::NiceSlice;
use PDL::Finance::TA;

## load data as PDL into $qdata as described above

## use the close price
my $timestamp = $qdata(, (0));
my $open_px = $qdata(, (1));
my $high_px = $qdata(, (2));
my $low_px = $qdata(, (3));
my $close_px = $qdata(, (4));
## use the default values
my ($bb_upper, $bb_middle, $bb_lower) = PDL::ta_bbands($close_px, 5, 2, 2, 0);

## generate buy and sell signals
## we want to sell at the CLOSE price when the HIGH price cuts the Upper Bollinger Band
## we want to buy at the OPEN price when the LOW price cuts the Lower Bollinger Band
my $buys            = zeroes( $close_px->dims );
my $sells           = zeroes( $close_px->dims );
## use a 1 tick lookback
my $lookback        = 1;
## calculate the indexes of the lookback PDL based on LOW price
my $idx_0           = xvals( $low_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_0 = $idx_0->setbadif( $idx_0 < 0 )->setbadtoval(0);
## get the indexes of when the LOW Price < Lower Bollinger Band based on the lookback
my $idx_1 = which(
        ($low_px->index($idx_0) > $bb_lower->index($idx_0)) &
        ($low_px < $bb_lower)
);
## set the buys to be on the OPEN price for those indexes
$buys->index($idx_1) .= $open_px->index($idx_1);
## set all 0 values to BAD to avoid plotting zeroes
$buys->inplace->setvaltobad(0);

## calculate the indexes of the lookback PDL based on HIGH price
my $idx_2 = xvals( $high_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_2 = $idx_2->setbadif( $idx_2 < 0 )->setbadtoval(0);
## get the indexes of when the HIGH Price > Upper Bollinger Band based on the lookback
my $idx_3 = which(
    ($high_px->index($idx_2) < $bb_upper->index($idx_2)) &
    ($high_px > $bb_upper )
);
## set the sells to be on the CLOSE price for those indexes
$sells->index($idx_3) .= $close_px->index($idx_3);
## set all 0 values to BAD to avoid plotting zeroes
$sells->inplace->setvaltobad(0);

## plot the data
my $pwin = gpwin(size => [1024, 768, 'px']);
$pwin->reset;
$pwin->multiplot;
$pwin->plot({
        object => '1 rectangle from screen 0,0 to screen 1,1 fillcolor rgb "black" behind',
        title => ["$symbol Open-High-Low-Close", textcolor => 'rgb "white"'],
        key => ['on', 'outside', textcolor => 'rgb "yellow"'],
        border => 'linecolor rgbcolor "white"',
        xlabel => ['Date', textcolor => 'rgb "yellow"'],
        ylabel => ['Price', textcolor => 'rgb "yellow"'],
        xdata => 'time',
        xtics => {format => '%Y-%m-%d', rotate => -90, textcolor => 'orange', },
        ytics => {textcolor => 'orange'},
        label => [1, $symbol, textcolor => 'rgb "cyan"', at => "graph 0.90,0.03"],
    },
    {
        with => 'financebars',
        linecolor => 'white',
        legend => 'Price',
    },
    $timestamp,
    $open_px,
    $high_px,
    $low_px,
    $close_px,
    ### Bollinger Bands plot
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-green',
        legend => 'Bollinger Band - Upper'
    },
    $timestamp,
    $bb_upper, #upper band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-magenta',
        legend => 'Bollinger Band - Lower'
    },
    $timestamp,
    $bb_lower, #lower band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'orange',
        legend => 'Bollinger Band - Middle'
    },
    $timestamp,
    $bb_middle, #middle band
    {
        with => 'points',
        pointtype => 5, #triangle
        linecolor => 'green',
        legend => 'Buys',
    },
    $timestamp,
    $buys,
    {
        with => 'points',
        pointtype => 7, #inverted triangle
        linecolor => 'red',
        legend => 'Sells',
    },
    $timestamp,
    $sells,
);
$pwin->end_multi;
$pwin->pause_until_close;

Final Script

So let's aggregate all the code into one script and it looks like below. We have moved the data retrieval into a simple function get_data() that does not make web requests unnecessarily. You can copy this code and run it as is in the shell and it will plot a Gnuplot window as seen in the screenshot image linked below.

#!/usr/bin/env perl
use strict;
use warnings;
use PDL;
use PDL::NiceSlice;
use PDL::Finance::TA;
use PDL::Graphics::Gnuplot;
use JSON::XS qw(decode_json);
use LWP::UserAgent;
use DateTime;
use Try::Tiny;
use Path::Tiny;

sub get_data($) {
    my $symbol = shift;
    my $filename = lc "$symbol.json";
    my $content;
    my $qdata;
    my $url = sprintf("https://api.gemini.com/v2/candles/%s/%s", lc $symbol, '1day');
    if (-e $filename) {
        print "Found $filename, loading data from that\n";
        $content = path($filename)->slurp;
    } else {
        my $lwp = LWP::UserAgent->new(timeout => 60);
        $lwp->env_proxy;
        my $resp = $lwp->get($url);
        if ($resp->is_success) {
            $content = $resp->decoded_content;
            path($filename)->spew($content);
        } else {
            warn "Error from request to $url: " . $resp->status_line;
            return undef;
        }
    }
    if (defined $content and length($content)) {
        my $jquotes = decode_json $content;
        if (ref $jquotes eq 'ARRAY' and scalar(@$jquotes)) {
            ## sort quotes by timestamp
            my @sorted = sort { $a->[0] <=> $b->[0] } @$jquotes;
            foreach my $q (@sorted) {
                ## timestamp is the first column in milliseconds
                $q->[0] /= 1000;
            }
            ## convert the quotes to a PDL
            $qdata = pdl(@sorted)->transpose;
        } else {
            warn "No quotes returned by $url or $filename";
            $qdata = undef;
        }
    } else {
        warn "No content received from $url or $filename";
        $qdata = undef;
    }
    ## now we operate on the $qdata PDL object
    return $qdata;
}

my $symbol = $ARGV[0] // 'DOGEUSD';
my $qdata = get_data($symbol);
die "Unable to get data for $symbol" unless ref $qdata eq 'PDL';
print $qdata;
my $timestamp = $qdata(, (0));
my $open_px = $qdata(, (1));
my $high_px = $qdata(, (2));
my $low_px = $qdata(, (3));
my $close_px = $qdata(, (4));
## use the default values
## each of these are 1-D PDLs
my ($bb_upper, $bb_middle, $bb_lower) = PDL::ta_bbands($close_px, 5, 2, 2, 0);
my $buys            = zeroes( $close_px->dims );
my $sells           = zeroes( $close_px->dims );
## use a 1 tick lookback
my $lookback        = 1;
## calculate the indexes of the lookback PDL based on LOW price
my $idx_0           = xvals( $low_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_0 = $idx_0->setbadif( $idx_0 < 0 )->setbadtoval(0);
## get the indexes of when the LOW Price < Lower Bollinger Band based on the lookback
my $idx_1 = which(
        ($low_px->index($idx_0) > $bb_lower->index($idx_0)) &
        ($low_px < $bb_lower)
);
## set the buys to be on the OPEN price for those indexes
$buys->index($idx_1) .= $open_px->index($idx_1);
## set all 0 values to BAD to avoid plotting zeroes
$buys->inplace->setvaltobad(0);

## calculate the indexes of the lookback PDL based on HIGH price
my $idx_2 = xvals( $high_px->dims ) - $lookback;
## if the lookback index is negative set it to 0
$idx_2 = $idx_2->setbadif( $idx_2 < 0 )->setbadtoval(0);
## get the indexes of when the HIGH Price > Upper Bollinger Band based on the lookback
my $idx_3 = which(
    ($high_px->index($idx_2) < $bb_upper->index($idx_2)) &
    ($high_px > $bb_upper )
);
## set the sells to be on the CLOSE price for those indexes
$sells->index($idx_3) .= $close_px->index($idx_3);
## set all 0 values to BAD to avoid plotting zeroes
$sells->inplace->setvaltobad(0);
## plot the data
my $pwin = gpwin(size => [1024, 768, 'px']);
$pwin->reset;
$pwin->multiplot;
$pwin->plot({
        object => '1 rectangle from screen 0,0 to screen 1,1 fillcolor rgb "black" behind',
        title => ["$symbol Open-High-Low-Close", textcolor => 'rgb "white"'],
        key => ['on', 'outside', textcolor => 'rgb "yellow"'],
        border => 'linecolor rgbcolor "white"',
        xlabel => ['Date', textcolor => 'rgb "yellow"'],
        ylabel => ['Price', textcolor => 'rgb "yellow"'],
        xdata => 'time',
        xtics => {format => '%Y-%m-%d', rotate => -90, textcolor => 'orange', },
        ytics => {textcolor => 'orange'},
        label => [1, $symbol, textcolor => 'rgb "cyan"', at => "graph 0.90,0.03"],
    },
    {
        with => 'financebars',
        linecolor => 'white',
        legend => 'Price',
    },
    $timestamp,
    $open_px,
    $high_px,
    $low_px,
    $close_px,
    ### Bollinger Bands plot
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-green',
        legend => 'Bollinger Band - Upper'
    },
    $timestamp,
    $bb_upper, #upper band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'dark-magenta',
        legend => 'Bollinger Band - Lower'
    },
    $timestamp,
    $bb_lower, #lower band
    {
        with => 'lines',
        axes => 'x1y1',
        linecolor => 'orange',
        legend => 'Bollinger Band - Middle'
    },
    $timestamp,
    $bb_middle, #middle band
    {
        with => 'points',
        pointtype => 5, #triangle
        linecolor => 'green',
        legend => 'Buys',
    },
    $timestamp,
    $buys,
    {
        with => 'points',
        pointtype => 7, #inverted triangle
        linecolor => 'red',
        legend => 'Sells',
    },
    $timestamp,
    $sells,
);
$pwin->end_multi;
$pwin->pause_until_close;

Image of the final chart