File Coverage

blib/lib/Finance/Quote/ASX.pm
Criterion Covered Total %
statement 23 105 21.9
branch 0 30 0.0
condition 0 15 0.0
subroutine 9 14 64.2
pod 0 7 0.0
total 32 171 18.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Copyright (C) 1998, Dj Padzensky <djpadz@padz.net>
4             # Copyright (C) 1998, 1999 Linas Vepstas <linas@linas.org>
5             # Copyright (C) 2000, Yannick LE NY <y-le-ny@ifrance.com>
6             # Copyright (C) 2000, Brent Neal <brentn@users.sourceforge.net>
7             # Copyright (C) 2001, Leigh Wedding <leigh.wedding@telstra.com>
8             # Copyright (C) 2000-2004, Paul Fenwick <pjf@cpan.org>
9             # Copyright (C) 2014, Chris Good <chris.good@@ozemail.com.au>
10             #
11             # This program is free software; you can redistribute it and/or modify
12             # it under the terms of the GNU General Public License as published by
13             # the Free Software Foundation; either version 2 of the License, or
14             # (at your option) any later version.
15             #
16             # This program is distributed in the hope that it will be useful,
17             # but WITHOUT ANY WARRANTY; without even the implied warranty of
18             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19             # GNU General Public License for more details.
20             #
21             # You should have received a copy of the GNU General Public License
22             # along with this program; if not, write to the Free Software
23             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24             # 02110-1301, USA
25             #
26             #
27             # This code derived from Padzensky's work on package Finance::YahooQuote,
28             # but extends its capabilites to encompas a greater number of data sources.
29             #
30             # This code was developed as part of GnuCash <http://www.gnucash.org/>
31              
32             require 5.005;
33              
34 5     5   3279 use strict;
  5         14  
  5         178  
35 5     5   28 use warnings;
  5         13  
  5         250  
36              
37             package Finance::Quote::ASX;
38              
39 5     5   80 use LWP::UserAgent;
  5         10  
  5         59  
40 5     5   177 use JSON qw/decode_json/;
  5         34  
  5         59  
41              
42 5     5   818 use constant DEBUG => $ENV{DEBUG};
  5         27  
  5         415  
43 5     5   38 use if DEBUG, 'Smart::Comments';
  5         10  
  5         56  
44              
45 5     5   246 use vars qw/$ASX_URL_PRIMARY $ASX_URL_ALTERNATE/;
  5         14  
  5         6713  
46              
47             our $VERSION = '1.57_03'; # TRIAL VERSION
48              
49             $ASX_URL_PRIMARY = 'https://www.asx.com.au/asx/1/share/';
50             $ASX_URL_ALTERNATE = 'https://asx.api.markitdigital.com/asx-research/1.0/companies/';
51              
52 5     5 0 26 sub methods {return (australia => \&asx,asx => \&asx)}
53              
54             {
55             my @labels = qw/
56             ask
57             bid
58             cap
59             close
60             date
61             eps
62             high
63             last
64             low
65             name
66             net
67             open
68             p_change
69             pe
70             type
71             volume/;
72              
73             # Function that lists the data items available from the Australian Securities Exchange (ASX)
74              
75 5     5 0 18 sub labels { return (australia => \@labels,
76             asx => \@labels); }
77             }
78              
79             # Australian Stock Exchange (ASX)
80             # The ASX provides free delayed quotes through their webpage:
81             # https://www2.asx.com.au/markets/company/NAB
82             #
83             # Maintainer of this section is Paul Fenwick <pjf@cpan.org>
84             # 5-May-2001 Updated by Leigh Wedding <leigh.wedding@telstra.com>
85             # 24-Feb-2014 Updated by Chris Good <chris.good@@ozemail.com.au>
86             # 12-Oct-2020 Updated by Jeremy Volkening
87             # Jan-2021 Updated by Geoff <cleanoutmyshed@gmail.com>
88             # October 2020 the ASX revamped their website with dynamic content for quotes
89             # which prevented the previous HTML screen scraping from working, but exposed
90             # a number of JSON data sources, two of which are used here.
91             # The primary source returns data elements for almost all securities, but
92             # does not return prices for certain security types (some bonds and exchange
93             # traded products, options, and warrants), and returns an error for indices.
94             # The alternate source returns less data elements, but provides usable quote
95             # data for all the known exceptions, including indices.
96             # This version will always call the primary source, and call the alternate if
97             # a price is not returned by the primary.
98             #
99             # Smart::Comments implemented to conform with the Hackers Guide:
100             # https://github.com/finance-quote/finance-quote/blob/master/Documentation/Hackers-Guide
101             #
102              
103             # Main function to fetch quotes from the Australian Securities Exchange (ASX)
104              
105             sub asx {
106              
107 0     0 0   my $quoter = shift;
108 0 0         my @symbols = @_
109             or return;
110              
111 0           my($error, %info, $status, $ua);
112              
113 0           $ua = $quoter->user_agent;
114              
115             SYMBOL:
116 0           for my $symbol (@symbols) {
117             ### ASX.pm Processing symbol: $symbol
118 0           $info{ $symbol, 'symbol' } = $symbol;
119 0           $info{ $symbol, 'method' } = 'asx';
120 0           $info{ $symbol, 'exchange' } = 'Australian Securities Exchange';
121              
122 0           $symbol =~ s/\s+$//;
123 0 0         if ($symbol !~ m/^[A-Za-z0-9]{1,6}$/) {
124 0           $info{ $symbol, 'success' } = 0;
125 0           $info{ $symbol, 'errormsg' } = 'Invalid symbol. ASX symbols must be alpha numeric maximum length 6 characters.';
126             ### ASX.pm: $info{ $symbol, 'errormsg' }
127 0           next SYMBOL;
128             }
129              
130 0           ($status, $error) = asx_primary($symbol, $ua, \%info);
131              
132 0 0 0       if (($status != 1) ||
133             ($info{$symbol, 'last'} eq '')) {
134             ### ASX.pm Calling Alternate URL as Primary URL failed or doesn't have a Last Price
135 0           ($status, $error) = asx_alternate($symbol, $ua, \%info);
136             }
137              
138 0 0         if ($status != 1) {
139 0           $info{ $symbol, 'success' } = 0;
140 0           $info{ $symbol, 'errormsg' } = "$error";
141             ### ASX.pm Unsuccessful calls to ASX URLs - symbol cannot be processed: $symbol
142 0           next SYMBOL;
143             }
144              
145             ### ASX.pm We have valid data, apply various clean ups and add remaining data for symbol: $symbol
146              
147             # Remove trailing percentage sign from p_change
148 0           $info{ $symbol, 'p_change' } =~ s/\%$//;
149              
150 0           $info{ $symbol, 'price' } = $info{ $symbol, 'last' };
151              
152 0 0 0       if ((exists $info{ $symbol, 'date' }) &&
153             ($info{ $symbol, 'date' } =~ m/([0-9]{4}-[0-9]{2}-[0-9]{2})T/)) {
154 0           $quoter->store_date(\%info, $symbol, {isodate => $1});
155             ### ASX.pm Converted Last Trade Date to ISO format: "$info{ $symbol, 'date' } --> $1"
156             }
157              
158             # Technically indices don't have a currency, but it is not possible to distinguish them
159 0           $info{ $symbol, 'currency' } = 'AUD';
160              
161 0           $info{ $symbol, 'success' } = 1;
162 0           $info{ $symbol, 'errormsg' } = '';
163              
164             }
165              
166             ### ASX.pm Returning data for all symbols to Finance-Quote and exiting <file>[<line>]
167 0 0         return %info if wantarray;
168 0           return \%info;
169              
170             }
171              
172             # Internal function to handle ASX Alternate data source
173              
174             sub asx_primary {
175              
176 0     0 0   my ($symbol, $ua, $info) = @_;
177              
178 0           my($data, $error, %label_map, $status, $url);
179              
180 0           $url = $ASX_URL_PRIMARY . $symbol;
181 0           ($status, $error, $data) = get_asx_data($url, $ua);
182 0 0         return $status, $error unless $status == 1;
183              
184             # Map the Finance::Quote labels (left) to the corresponding ASX labels (right)
185 0           %label_map = (
186             'bid' => 'bid_price',
187             'p_change' => 'change_in_percent',
188             'net' => 'change_price',
189             'name' => 'desc_full',
190             'high' => 'day_high_price',
191             'low' => 'day_low_price',
192             'eps' => 'eps',
193             'last' => 'last_price',
194             'date' => 'last_trade_date',
195             'cap' => 'market_cap',
196             'ask' => 'offer_price',
197             'open' => 'open_price',
198             'pe' => 'pe',
199             'close' => 'previous_close_price',
200             'volume' => 'volume',
201             );
202              
203 0           process_asx_data($symbol, $data, \%label_map, $info);
204              
205 0           return 1, '';
206              
207             }
208              
209             # Internal function to handle ASX Alternate data source
210              
211             sub asx_alternate {
212              
213 0     0 0   my ($symbol, $ua, $info) = @_;
214              
215 0           my($data, $error, %label_map, $status, $url);
216              
217 0           $url = $ASX_URL_ALTERNATE . $symbol . '/header';
218 0           ($status, $error, $data) = get_asx_data($url, $ua);
219 0 0         return $status, $error unless $status == 1;
220              
221 0 0         if (exists $data->{error}) {
222 0           $status = 0;
223 0           $error = "Error returned by ASX server '$url'. Code: " . $data->{error}{code} . ' Message: ' . $data->{error}{message};
224             ### ASX.pm Error: $error
225 0           return $status, $error;
226             }
227              
228 0 0         if (! exists $data->{data}) {
229 0           $status = 0;
230 0           $error = "Cannot parse content from ASX server '$url'. Expected a top level JSON element named data.";
231             ### ASX.pm Error: $error
232 0           return $status, $error;
233             }
234              
235             # Map the Finance::Quote labels (left) to the corresponding ASX labels (right)
236             %label_map = (
237 0           'name' => 'displayName',
238             'ask' => 'priceAsk',
239             'bid' => 'priceBid',
240             'net' => 'priceChange',
241             'p_change' => 'priceChangePercent',
242             'last' => 'priceLast',
243             'type' => 'securityType',
244             'volume' => 'volume',
245             );
246              
247 0           process_asx_data($symbol, $data->{data}, \%label_map, $info);
248              
249 0           return 1, '';
250              
251             }
252              
253             # Internal function to fetch, validate, and decode data from an ASX URL using LWP User Agent
254             # Handle any errors
255              
256             sub get_asx_data {
257              
258 0     0 0   my ($url, $ua) = @_;
259              
260 0           my($data, $error, $json, $response, $status);
261              
262             ### ASX.pm Retrieving data from ASX URL: $url
263 0           $response = $ua->get($url);
264 0 0         if (! $response->is_success) {
265 0           $status = 0;
266 0           $error = "Unable to fetch data from the ASX server '$url'. Status: " . $response->status_line;
267             ### ASX.pm Error: $error
268 0           return $status, $error, undef;
269             }
270              
271 0 0         if ($response->header('content-type') !~ m|application/json|i) {
272 0           $status = 0;
273 0           $error = "Invalid content-type from ASX server '$url'. Expected: application/json, received: " . $response->header('content-type');
274             ### ASX.pm Error: $error
275 0           return $status, $error, undef;
276             }
277              
278 0           $json = $response->content;
279              
280             # The JSON module will croak on errors, so use eval to trap this.
281 0           $data = eval{ decode_json($json) };
  0            
282 0 0         if ($@) {
283 0           $status = 0;
284 0           $error = "Failed to parse JSON data from ASX server '$url'. Error: '$@'.";
285             ### ASX.pm Error: $error
286 0           return $status, $error, undef;
287             }
288              
289             # Return valid, decoded data
290 0           $status = 1;
291 0           return $status, $error, $data;
292              
293             }
294              
295             # Internal function to push the ASX data elements into the Finance::Quote structure (%info)
296              
297             sub process_asx_data {
298              
299 0     0 0   my ($symbol, $data, $label_map, $info) = @_;
300              
301 0           foreach my $label (sort(keys %{$label_map})) {
  0            
302 0 0 0       if ((exists $data->{$label_map->{$label}}) &&
303             (defined $data->{$label_map->{$label}})) {
304             # Concatenate Primary and Alternate Names
305 0 0 0       if (($label eq 'name') &&
      0        
306             (exists $info->{$symbol, $label}) &&
307             (uc($info->{$symbol, $label}) ne uc($data->{$label_map->{$label}}))) {
308 0           $info->{$symbol, $label} = $data->{$label_map->{$label}} . ' ' . $info->{$symbol, $label};
309             }
310             # Overwrite all other labels
311             else {
312 0           $info->{$symbol, $label} = $data->{$label_map->{$label}};
313             }
314             ### ASX.pm Mapped ASX data element to Finance-Quote: sprintf("%-22s%-15s%-s", $label_map->{$label}, $label, $data->{$label_map->{$label}})
315             }
316             else {
317 0           $info->{$symbol,$label} = '';
318             }
319             }
320              
321 0           return;
322              
323             }
324              
325             1;
326             __END__
327              
328             =head1 NAME
329              
330             Finance::Quote::ASX - Obtain quotes from the Australian Stock Exchange.
331              
332             =head1 SYNOPSIS
333              
334             use Finance::Quote;
335             $q = Finance::Quote->new;
336             %stockinfo = $q->fetch("asx","BHP"); # Only query ASX.
337             %stockinfo = $q->fetch("australia","BHP"); # Failover to other sources OK.
338              
339             =head1 DESCRIPTION
340              
341             This module obtains information from the Australian Stock Exchange
342             http://www.asx.com.au/. Data for all Australian listed securities and indices
343             is available. Indexes start with the letter 'X'. For example, the
344             All Ordinaries is "XAO". But some securities also start with the letter 'X'.
345              
346             This module is loaded by default on a Finance::Quote object. It's
347             also possible to load it explicitly by placing "ASX" in the argument
348             list to Finance::Quote->new().
349              
350             This module provides both the "asx" and "australia" fetch methods.
351             Please use the "australia" fetch method if you wish to have failover
352             with other sources for Australian stocks (such as Yahoo). Using
353             the "asx" method will guarantee that your information only comes
354             from the Australian Stock Exchange.
355              
356             Information returned by this module is governed by the Australian
357             Stock Exchange's terms and conditions.
358              
359             =head1 LABELS RETURNED
360              
361             The following labels may be returned by Finance::Quote::ASX:
362             bid, offer, open, high, low, last, net, p_change, volume,
363             and price.
364              
365             =head1 SEE ALSO
366              
367             Australian Stock Exchange, http://www.asx.com.au/
368              
369             Finance::Quote::Yahoo::Australia.
370             =cut