File Coverage

blib/lib/App/Chart/Suffix/RBA.pm
Criterion Covered Total %
statement 24 26 92.3
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 33 35 94.2


line stmt bran cond sub pod time code
1             # Reserve Bank of Australia setups.
2              
3             # Copyright 2007, 2008, 2009, 2010, 2011, 2012, 2016 Kevin Ryde
4              
5             # This file is part of Chart.
6             #
7             # Chart is free software; you can redistribute it and/or modify it under the
8             # terms of the GNU General Public License as published by the Free Software
9             # Foundation; either version 3, or (at your option) any later version.
10             #
11             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
12             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14             # details.
15             #
16             # You should have received a copy of the GNU General Public License along
17             # with Chart. If not, see <http://www.gnu.org/licenses/>.
18              
19             package App::Chart::Suffix::RBA;
20 1     1   382 use 5.010;
  1         3  
21 1     1   4 use strict;
  1         2  
  1         16  
22 1     1   3 use warnings;
  1         2  
  1         20  
23 1     1   3 use Carp;
  1         2  
  1         44  
24 1     1   215 use Date::Calc;
  1         4207  
  1         33  
25 1     1   6 use List::Util qw(min max);
  1         3  
  1         99  
26 1     1   259 use Set::IntSpan::Fast;
  1         8209  
  1         46  
27 1     1   12 use Locale::TextDomain ('App-Chart');
  1         2  
  1         11  
28              
29 1     1   517 use App::Chart;
  0            
  0            
30             use App::Chart::Database;
31             use App::Chart::Download;
32             use App::Chart::DownloadHandler;
33             use App::Chart::Sympred;
34             use App::Chart::Latest;
35             use App::Chart::TZ;
36             use App::Chart::Weblink;
37              
38             # uncomment this to run the ### lines
39             # use Smart::Comments;
40              
41             # Not yet using Finance::Quote::RBA
42              
43             my $pred = App::Chart::Sympred::Suffix->new ('.RBA');
44             App::Chart::TZ->sydney->setup_for_symbol ($pred);
45             App::Chart::setup_source_help
46             ($pred, __p('manual-node','Reserve Bank of Australia'));
47              
48             use constant RBA_COPYRIGHT_URL =>
49             'http://www.rba.gov.au/copyright/';
50              
51              
52             #------------------------------------------------------------------------------
53             # weblink - home page
54              
55             App::Chart::Weblink->new
56             (pred => $pred,
57             name => __('_RBA Home Page'),
58             desc => __('Open web browser at the Reserve Bank of Australia home page'),
59             url => 'http://www.rba.gov.au');
60              
61              
62             #------------------------------------------------------------------------------
63             # three day page
64             #
65             # This uses the rates at:
66             #
67             use constant RBA_EXCHANGE_URL =>
68             'http://www.rba.gov.au/statistics/frequency/exchange-rates.html';
69             use constant RBA_EXCHANGE_URL_DAYS => 3;
70              
71             # would it be a few minutes after 4pm ?
72             sub threeday_available_date_time {
73             return (App::Chart::Download::weekday_date_after_time
74             (16,0, App::Chart::TZ->sydney),
75             '16:01:00');
76             }
77              
78             sub threeday_available_tdate {
79             my ($iso, $time) = threeday_available_date_time();
80             return App::Chart::Download::iso_to_tdate_floor ($iso);
81             }
82              
83             sub threeday_parse {
84             my ($resp) = @_;
85             my @data = ();
86             my $h = { url => RBA_EXCHANGE_URL,
87             copyright => RBA_COPYRIGHT_URL,
88             source => __PACKAGE__,
89             resp => $resp,
90             cover_pred => $pred,
91             data => \@data };
92              
93             my $content = $resp->decoded_content(raise_error=>1);
94              
95             # mung <tr id="USD"> to add <td>USD</td> so it appears in the TableExtract
96             $content =~ s{<tr>}{<tr><td></td>}ig;
97             $content =~ s{(<tr +id="([^"]*)">)}{$1<td>$2</td>}ig;
98              
99             require HTML::TableExtract;
100             my $te = HTML::TableExtract->new
101             (headers => ['Units of foreign currency per'],
102             slice_columns => 0);
103             $te->parse($content);
104             my $ts = $te->first_table_found();
105             if (! $ts) { die "RBA: html table not found\n"; }
106              
107             my $rows = $ts->rows();
108             my $lastrow = $#$rows;
109             my $lastcol = $#{$rows->[0]};
110              
111             # date like "03 Sep 2007"
112             my @dates;
113             foreach my $c (2 .. $lastcol) {
114             $dates[$c] = App::Chart::Download::Decode_Date_EU_to_iso($rows->[0]->[$c]);
115             }
116             $h->{'lo_date'} = List::Util::minstr (grep {defined} @dates);
117              
118             foreach my $r (1 .. $lastrow) {
119             my $row = $rows->[$r];
120              
121             my $symbol = $row->[0] // next;
122             $symbol =~ s/_.*//; # _4pm on TWI
123             $symbol = "AUD$symbol.RBA";
124              
125             my $name = $row->[1];
126             $name =~ s/ \(4pm\)$//; # 4pm on TWI
127              
128             foreach my $c (2 .. $lastcol) {
129             my $rate = $row->[$c];
130             # bank holiday columns have "BANK HOLIDAY" with one letter per row or
131             # blank which comes through as undef, skip those
132             next if ! Scalar::Util::looks_like_number($rate);
133              
134             push @data, { symbol => $symbol,
135             name => $name,
136             date => $dates[$c],
137             last_time => '16:00:00',
138             close => $rate,
139             currency => substr($symbol,3,3),
140             };
141             }
142             }
143              
144             return $h;
145             }
146              
147             #------------------------------------------------------------------------------
148             # latest quotes
149              
150             App::Chart::LatestHandler->new
151             (pred => $pred,
152             url_tags_key => 'RBA-latest',
153             proc => \&latest_download,
154             available_date_time => \&threeday_available_date_time);
155              
156             sub latest_download {
157             my ($symbol_list) = @_;
158              
159             App::Chart::Download::status (__('RBA past three days'));
160             my $resp = App::Chart::Download->get (RBA_EXCHANGE_URL,
161             url_tags_key => 'RBA-latest');
162             App::Chart::Download::write_latest_group (threeday_parse ($resp));
163             }
164              
165              
166             #------------------------------------------------------------------------------
167             # historical xls page
168             #
169             # This downloads and parses up the page:
170             # http://www.rba.gov.au/statistics/historical-data.html
171             #
172             # 2014 to present
173             # http://www.rba.gov.au/statistics/tables/csv/f11.1-data.csv
174             # 213k or 47k compressed, also byte ranges
175             # http://www.rba.gov.au/statistics/tables/xls-hist/2014-current.xls
176             # 438k
177             #
178             use constant RBA_HISTORICAL_PAGE_URL =>
179             'http://www.rba.gov.au/statistics/hist-exchange-rates/index.html';
180             #
181             # which offers various xls files for past rates.
182              
183             sub historical_info {
184             require App::Chart::Pagebits;
185             return App::Chart::Pagebits::get
186             (name => __('RBA historical page'),
187             url => RBA_HISTORICAL_PAGE_URL,
188             key => 'rba-historical',
189             freq_days => 1,
190             parse => \&historical_parse);
191             }
192              
193             sub historical_parse {
194             my ($content) = @_;
195              
196             # Eg.
197             # <a href="/statistics/hist-exchange-rates/2007-2009.xls"
198             # target="_blank" title="Link, opening in a new window, to XLS file.">
199             # 2007 to 2009</a> <span class="nonHtml">[XLS 227K]</span>
200             #
201             # because the size is outside the link it doesn't really suit
202             # HTML::LinkExtor / HTML::Parser etc
203             #
204             my @files;
205             while ($content =~ m%href=\"([^\"]*[0-9][0-9][0-9][0-9]\.xls)\"
206             (.*\n){0,3}
207             .*\[XLS\s*([0-9.]+)([MK])
208             %igmx) {
209             my $link = $1;
210             my $size = $3;
211             my $size_units = uc($4);
212              
213             if ($size_units eq 'K') { $size *= 1_000 }
214             if ($size_units eq 'M') { $size *= 1_000_000 }
215              
216             my $uri = URI->new_abs($link, RBA_HISTORICAL_PAGE_URL);
217              
218             # eg per above: 2003-2007.xls
219             # or just: 2007.xls
220             $link =~ /([0-9][0-9][0-9][0-9])((to|-)([0-9][0-9][0-9][0-9]))?\.xls$/
221             or die "RBA: oops, unrecognised link: $link";
222             my $lo_year = $1;
223             my $hi_year = $4 || $1;
224              
225             push @files, { url => $uri->as_string,
226             cost => $size,
227             lo_year => $lo_year,
228             hi_year => $hi_year,
229             };
230             }
231             return { files => \@files };
232             }
233              
234              
235             #-----------------------------------------------------------------------------
236             # download - historical monthly prices
237             #
238             # This parses the monthly rates spreadsheet file from the
239             # RBA_HISTORICAL_PAGE_URL page above,
240             #
241             # http://www.rba.gov.au/statistics/tables/xls/f11hist-1969-2009.xls
242             #
243             # but only the part from 1983 back is wanted since there's daily data for
244             # 1983 onwards.
245              
246             my %monthly_fx_to_currency
247             = (
248             # 'TWI' # trade weighted index
249             'CR' => 'CNY', # chinese renminbi
250             'JY' => 'JPY', # japanese yen
251             # 'EUR'
252             # 'USD'
253             'SKW' => 'KRW', # South Korean won
254             'UKPS' => 'GBP', # british pound sterling
255             'SD' => 'SGD', # singapore dollar
256             'IRE' => 'INR', # Indian rupee
257             'TB' => 'THB', # Thai baht
258             # 'NZD'
259             'NTD' => 'TWD', # taiwan dollar
260             'MR' => 'MYR', # malaysian ringgit
261             'IR' => 'IDR', # indonesian rupiah
262             'VD' => 'VND', # Vietnamese dong
263             'UAED' => 'AED', # UAE dirham
264             'PNGK' => 'PGK', # PNG kina
265             # 'HKD' # Hong Kong dollar
266             'CD' => 'CAD', # Canadian dollar
267             'SARD' => 'ZAR', # South African rand
268             'SARY' => 'SAR', # Saudi riyal
269             'SF' => 'CHF', # Swiss franc
270             'SK' => 'SEK', # Swedish krona
271             # 'SDR' # special drawing right
272             );
273              
274             sub monthly_parse {
275             my ($resp, $stop_iso) = @_;
276             ### RBA monthly_parse() ...
277             my $content = $resp->decoded_content(raise_error=>1);
278              
279             my @data = ();
280             my $h = { source => __PACKAGE__,
281             copyright => RBA_COPYRIGHT_URL,
282             data => \@data };
283              
284             require Spreadsheet::ParseExcel;
285             require Spreadsheet::ParseExcel::Utility;
286              
287             my $excel = Spreadsheet::ParseExcel::Workbook->Parse (\$content);
288             my $sheet = $excel->Worksheet (0);
289             ### SheetCount: $excel->{'SheetCount'}
290             ### Name: $sheet->{'Name'}
291              
292             my ($minrow, $maxrow) = $sheet->RowRange;
293             my ($mincol, $maxcol) = $sheet->ColRange;
294             ### rows: $minrow, $maxrow
295             ### cols: $mincol, $maxcol
296              
297             # heading row repeats the filename "F11HIST.XLS" and then the currencies
298             # in columns as say "FXRJY"
299             my $heading_row = List::Util::first {
300             my $cell = $sheet->Cell($_,$mincol);
301             $cell && $cell->Value eq 'F11HIST.XLS' }
302             ($minrow .. $maxrow)
303             or die "RBA monthly: headings not found";
304             ### $heading_row
305              
306             my @currencies = map {
307             my $cell = $sheet->Cell($heading_row,$_);
308             my $currency = $cell ? $cell->Value : '';
309             $currency =~ s/^FXR//;
310             ($monthly_fx_to_currency{$currency} || $currency)
311             } ($mincol .. $maxcol);
312             ### @currencies
313             ### count: scalar(@currencies)
314              
315             my %currency_started;
316              
317             ROW: foreach my $row ($heading_row+1 .. $maxrow) {
318             my $datecell = $sheet->Cell($row,0) or next;
319             # seen 'Numeric', but presumably 'Date' is ok
320             if ($datecell->{'Type'} ne 'Numeric'
321             && $datecell->{'Type'} ne 'Date') {
322             next; # skip blanks at end
323             }
324             my $month = Spreadsheet::ParseExcel::Utility::ExcelFmt
325             ('yyyy-mm-dd', $datecell->{'Val'}, $excel->{'Flg1904'});
326              
327             foreach my $col ($mincol+1 .. $maxcol) {
328             my $currency = $currencies[$col-$mincol] or next;
329             my $ratecell = $sheet->Cell($row,$col) or next;
330             my $rate = $ratecell->Value;
331              
332             # avoid empty records until the start of data for a given currency is
333             # reached
334             if (! $rate && ! $currency_started{$currency}) { next; }
335              
336             my $symbol = "AUD$currency.RBA";
337             $currency_started{$currency} = 1;
338             foreach my $date (iso_weekdays_in_month ($month)) {
339             if ($date gt $stop_iso) { last ROW; }
340             push @data, { symbol => $symbol,
341             currency => $currency,
342             date => $date,
343             close => $rate,
344             };
345             }
346             }
347             }
348              
349             return $h;
350             }
351              
352             # return a list of ISO date strings like '2008-09-08' which is all the
353             # weekdays in the month of ISO date $str
354             sub iso_weekdays_in_month {
355             my ($str) = @_;
356             my ($lo_year, $lo_month, undef) = App::Chart::iso_to_ymd ($str);
357             my ($hi_year, $hi_month, undef) = Date::Calc::Add_Delta_YM
358             ($lo_year,$lo_month,1, 0,1);
359             my $lo = App::Chart::ymd_to_tdate_ceil ($lo_year, $lo_month, 1);
360             my $hi = App::Chart::ymd_to_tdate_ceil ($hi_year, $hi_month, 1) - 1;
361             return map { App::Chart::tdate_to_iso($_) } ($lo .. $hi);
362             }
363              
364              
365             #------------------------------------------------------------------------------
366             # xls parse
367             #
368             # This parses xls spreadsheet files like
369             #
370             # http://www.rba.gov.au/Statistics/HistoricalExchangeRates/2003to2007.xls
371             #
372             # The files aren't huge (500k upwards) but there's a lot of cells, which
373             # makes Spreadsheet::ParseExcel fairly slow and eat up about 50Mb of core
374             # (as of version 0.32), even before getting to the $h data build.
375              
376             sub xls_parse {
377             my ($resp) = @_;
378             ### RBA xls_parse() ...
379             my $content = $resp->decoded_content(raise_error=>1);
380              
381             my @data = ();
382             my $h = { source => __PACKAGE__,
383             copyright => RBA_COPYRIGHT_URL,
384             data => \@data };
385              
386             require Spreadsheet::ParseExcel;
387             require Spreadsheet::ParseExcel::Utility;
388              
389             my $excel = Spreadsheet::ParseExcel::Workbook->Parse (\$content);
390             my $sheet = $excel->Worksheet (0);
391             ### sheet: $sheet->{'Name'}
392              
393             my ($minrow, $maxrow) = $sheet->RowRange;
394             my ($mincol, $maxcol) = $sheet->ColRange;
395              
396             # heading row "DAILY 4PM", or "Mnemonic" and the currencies in columns
397             my $heading_row = List::Util::first {
398             my $cell = $sheet->Cell($_,$mincol);
399             $cell && ($cell->Value eq 'DAILY 4PM'
400             || $cell->Value eq 'Mnemonic') }
401             ($minrow .. $maxrow)
402             or die "RBA historical: currency code headings row not found";
403             ### heading row: $heading_row
404              
405             foreach my $row ($heading_row+1 .. $maxrow) {
406             my $datecell = $sheet->Cell($row,$mincol) or next;
407             $datecell->{'Type'} eq 'Date' or next; # skip blanks
408             my $date = Spreadsheet::ParseExcel::Utility::ExcelFmt
409             ('yyyy-mm-dd', $datecell->{'Val'}, $excel->{'Flg1904'});
410              
411             foreach my $col ($mincol+1 .. $maxcol) {
412             my $cell = $sheet->Cell($row,$col)
413             or next; # skip lots of blanks
414             my $rate = $cell->Value
415             or next; # skip lots of blanks
416             my $currency = $sheet->Cell($heading_row,$col)->Value;
417             $currency =~ s/^FXR//; # leading "FXR" circa 2012
418             $currency = ($monthly_fx_to_currency{$currency} || $currency);
419              
420             push @data, { symbol => "AUD$currency.RBA",
421             currency => $currency,
422             date => $date,
423             close => $rate,
424             };
425             }
426             }
427              
428             return $h;
429             }
430              
431              
432              
433             #------------------------------------------------------------------------------
434             # csv parse
435             #
436             # This parses csv download files like
437             #
438             # http://www.rba.gov.au/statistics/tables/csv/f11.1-data.csv
439             #
440             # row Title, A$1=USD, Trade-weighted Index May 1970 = 100, A$1=CNY, A$1=JPY,
441             #
442             # row Description,AUD/USD Exchange Rate; see notes for further detail.,
443             # Australian Dollar Trade-weighted Index,
444             # AUD/CNY Exchange Rate,AUD/JPY Exchange Rate,
445             #
446             # row Frequency,Daily,Daily,Daily
447             # row Type,Indicative,Indicative,
448             # row Units,USD,Index,CNY,JPY,
449             # row Source,WM/Reuters,RBA,RBA,
450             # row Publication date,17-Feb-2016,17-Feb-2016
451             # row Series ID,FXRUSD,FXRTWI,FXRCR,FXRJY,FXREUR,
452             #
453             # row 17-Feb-2016,0.7090,60.80,4.6265,80.61,
454             #
455             # csv
456             # AED CAD CHF CNY EUR GBP HKD IDR INR JPY KRW MYR NZD PGK PHP SDR SGD THB TWD TWI USD VND ZAR
457             #
458             # exchange page
459             # AED CAD CHF CNY EUR GBP HKD IDR INR JPY KRW MYR NZD PGK PHP SDR SGD THB TWD TWI USD VND
460              
461             use constant RBA_CURRENT_CSV_URL =>
462             'http://www.rba.gov.au/statistics/tables/csv/f11.1-data.csv';
463              
464             sub csv_parse {
465             my ($resp) = @_;
466             ### RBA csv_parse() ...
467             my $content = $resp->decoded_content(raise_error=>1);
468              
469             my @data = ();
470             my $h = { source => __PACKAGE__,
471             copyright => RBA_COPYRIGHT_URL,
472             date_format => 'dmy',
473             data => \@data };
474              
475             my @currencies;
476             foreach my $line (split /\n/, $content) {
477             my @fields = split /,\s*/, $line;
478             my $key = shift @fields // next;
479             ### $key
480              
481             if ($key eq 'Title') {
482             @currencies = map {
483             my $field = $_;
484             if ($field eq '') { # empty fields at end of line
485             undef;
486             } elsif ($field =~ /Trade.Weighted.Index/i) {
487             'TWI';
488             } elsif ($field =~ /A\$1=(.*)/i) {
489             $1;
490             } else {
491             warn "RBA: unrecognised Title field: $field";
492             undef;
493             }
494             } @fields;
495              
496             } elsif ($key =~ /\d+-[a-z]+-\d+/i) {
497             # row like 17-Feb-2016,0.7090,60.80,4.6265,80.61,
498             foreach my $i (0 .. $#fields) {
499             my $currency = $currencies[$i] // next;
500             push @data, { symbol => "AUD$currency.RBA",
501             currency => $currency,
502             date => $key,
503             close => $fields[$i],
504             };
505             }
506             }
507             }
508             return $h;
509             }
510              
511              
512              
513             #------------------------------------------------------------------------------
514             # data downloading
515              
516             App::Chart::DownloadHandler->new
517             (name => __('RBA'),
518             pred => $pred,
519             proc => \&download,
520             # backto => \&backto,
521             available_date_time => \&threeday_available_date_time);
522              
523             sub download {
524             my ($symbol_list) = @_;
525              
526             my $lo_tdate = App::Chart::Download::start_tdate_for_update (@$symbol_list);
527             my $hi_tdate = threeday_available_tdate();
528             ### RBA wanting ...
529             ### $lo_tdate
530             ### $hi_tdate
531              
532             # desired range is <= 3 days, so try the threeday page
533             if ($hi_tdate - $lo_tdate + 1 <= RBA_EXCHANGE_URL_DAYS) {
534             App::Chart::Download::status (__('RBA past three days'));
535             my $resp = App::Chart::Download->get (RBA_EXCHANGE_URL);
536             my $h = threeday_parse ($resp);
537             App::Chart::Download::write_latest_group($h);
538              
539             # if $lo_tdate is within the threeday data then write that and done
540             my $threeday_lo_tdate
541             = App::Chart::Download::iso_to_tdate_ceil ($h->{'lo_date'});
542             if ($threeday_lo_tdate <= $lo_tdate) {
543             App::Chart::Download::write_daily_group ($h);
544             return;
545             }
546             }
547              
548             # desired range > 3 days, or the threeday found was in fact not enough,
549             # get the csv
550              
551             my $url = RBA_CURRENT_CSV_URL;
552             require File::Basename;
553             my $filename = File::Basename::basename($url);
554             App::Chart::Download::status (__x('RBA data {filename}',
555             filename => $filename));
556             my $resp = App::Chart::Download->get($url);
557             my $h = csv_parse($resp);
558             App::Chart::Download::write_daily_group ($h);
559              
560             # This code looked at the historical downloads page and chose among the
561             # various XLS files.
562             #
563             # my $info = historical_info();
564             # my $files = $info->{'files'};
565             # $files = App::Chart::Download::choose_files ($files, $lo_tdate, $hi_tdate);
566             # $files = [ sort {$a->{'lo_tdate'} <=> $b->{'lo_tdate'}} @$files ];
567             # foreach my $f (@$files) {
568             # my $url = $f->{'url'};
569             # require File::Basename;
570             # my $filename = File::Basename::basename($url);
571             # App::Chart::Download::status (__x('RBA data {filename}',
572             # filename => $filename));
573             # my $resp = App::Chart::Download->get ($url);
574             # my $h = xls_parse ($resp);
575             # App::Chart::Download::write_daily_group ($h);
576             # }
577             }
578              
579             sub backto {
580             my ($symbol_list, $backto_tdate) = @_;
581             die "Not implemented";
582             }
583              
584             1;
585             __END__