# { key => 'commsec-enable',
# name => __('Enable CommSec (must be a client)'),
# type => 'boolean' },
# Copyright 2007, 2008, 2009, 2010, 2011 Kevin Ryde
# This file is part of Chart.
#
# Chart is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
# Chart is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with Chart. If not, see <http://www.gnu.org/licenses/>.
package App::Chart::CommSec;
use 5.008;
use strict;
use warnings;
use Carp;
use Date::Calc;
use File::Basename;
use List::Util;
use Locale::TextDomain ('App-Chart');
use App::Chart;
use App::Chart::Database;
use App::Chart::Download;
use App::Chart::DownloadCost;
use App::Chart::DownloadHandler;
use App::Chart::Sympred;
use App::Chart::Timebase::Months;
use App::Chart::TZ;
my $pred = App::Chart::Sympred::Proc->new (\&is_commsec_symbol);
sub is_commsec_symbol {
my ($symbol) = @_;
if (! is_enabled()) { return 0; }
no warnings 'once';
require App::Chart::Suffix::AX;
return $App::Chart::Suffix::AX::pred_shares->match ($symbol);
}
sub is_enabled {
return App::Chart::Database->preference_get ('commsec-enable');
}
# use App::Chart::Memoize::ConstSecond 'is_enabled';
#-----------------------------------------------------------------------------
# download
#
# The download chooses between
# - commsec whole-day update files
# - commsec individual update files
#
# To update many ASX shares the whole-day files are best, or for just a few
# then the individual files are best. A mixture is used too, if there's a
# few symbols that are quite a bit behind then they'll be done
# individually, and the balance with whole-day.
#
# The main problem with the whole-day file is how many entries it has,
# about 4500 as of Jan 2007. There's about 1600 companies (a lot of them
# small caps), the rest is warrants on the majors, and a few prefs or bonds
# on various.
App::Chart::DownloadHandler->new
(name => __('CommSec'),
pred => $pred,
available_tdate => \&available_tdate,
proc => \&download,
priority => 10);
# today's data available after 10:30pm weekdays, Sydney time
sub available_tdate {
App::Chart::Download::tdate_today_after
(22,30, App::Chart::TZ->sydney);
}
use constant { INDIV_PERMONTH_COST_KEY => 'commsec-indiv',
INDIV_PERMONTH_COST_DEFAULT => 1300,
WHOLEDAY_COST_KEY => 'commsec-wholeday' };
sub download {
my ($symbol_list) = @_;
App::Chart::Download::status (__('CommSec strategy'));
my $avail = available_tdate();
require App::Chart::DownloadCost;
my ($whole_tdate, @indiv_list) = App::Chart::DownloadCost::by_day_or_by_symbol
(available_tdate => $avail,
symbol_list => $symbol_list,
indiv_cost_proc => \&indiv_cost_proc,
whole_cost_key => WHOLEDAY_COST_KEY,
whole_cost_default => 259867); # May 2008
App::Chart::Download::verbose_message
(__x('CommSec whole days from {date} after indiv {symbols}',
date => App::Chart::tdate_to_iso($whole_tdate),
symbols => join(' ', @indiv_list)));
foreach my $symbol (@indiv_list) { indiv_download ($symbol); }
wholeday_download ($whole_tdate, $avail);
}
#------------------------------------------------------------------------------
# download - by each symbol
#
# This uses the download of all data for a symbol like
#
my @indiv_months_list = ([1, '1mo' ],
[2, '2mo' ],
[3, '3mo' ],
[6, '6mo' ],
[12, '1yr' ],
[24, '2yr' ],
[36, '3yr' ],
[48, '4yr' ],
[60, '5yr' ],
[120, '10yr']);
sub indiv_download {
my @symbol_list = @_;
foreach my $symbol (@symbol_list) {
my $tdate = App::Chart::Download::start_tdate_for_update ($symbol);
my $months = indiv_tdate_to_months ($tdate);
my $elem
= (List::Util::first {$_->[0] >= $months } @indiv_months_list)
|| $indiv_months_list[-1];
my $period_str = $elem->[1];
my $url
= 'http://charts.commsec.com.au/HistoryData/HistoryData.dll/GetData'
. '?Symbol='
. URI::Escape::uri_escape (App::Chart::symbol_sans_suffix ($symbol))
. '&TimePeriod=' . $period_str
. '&.csv';
App::Chart::Download::status (__x('CommSec {symbol} {period}',
symbol => $symbol,
period => $period_str));
my $resp = App::Chart::Download->get($url);
my $h = indiv_parse ($resp, $months);
if ($h) {
$h->{'last_download'} = 1;
App::Chart::Download::write_daily_group ($h);
}
}
}
# return number of months needed to cover back to TDATE
sub indiv_tdate_to_months {
my ($tdate) = @_;
$tdate -= 5; # bit of leeway
my ($now_year, $now_month, $now_day)
= App::Chart::TZ->sydney->ymd;
my ($td_year, $td_month, $td_day) = App::Chart::tdate_to_ymd ($tdate);
my $now_mdate
= App::Chart::Timebase::Months::ymd_to_mdate ($now_year, $now_month, 1);
my $td_mdate
= App::Chart::Timebase::Months::ymd_to_mdate ($td_year, $td_month, 1);
return $now_mdate - $td_mdate + ($now_day >= $td_day ? 1 : 0);
}
sub indiv_cost_proc {
my ($tdate) = @_;
my $months = indiv_tdate_to_months ($tdate);
return $months
* App::Chart::DownloadCost::cost_get (INDIV_PERMONTH_COST_KEY,
INDIV_PERMONTH_COST_DEFAULT);
}
sub indiv_parse {
my ($resp, $months) = @_;
my @data = ();
my $h = { source => __PACKAGE__,
currency => 'AUD',
suffix => '.AX',
prefer_decimals => 2,
date_format => 'dmy',
resp => $resp,
data => \@data };
my $body = $resp->decoded_content(raise_error=>1);
if ($body =~ /server error/i) {
# an unknown symbol
return $h;
}
$h->{'cost_key'} = INDIV_PERMONTH_COST_KEY;
$h->{'cost_value'} = int (length($body) / $months);
# Sample line
#
# AEZ,"01 Jun 2007",1.35,1.37,1.305,1.325,4008329\r\n
#
# trailing zeros are omitted, like 4.30 or 95.00 in
#
# ETR,"29 May 2007",4.26,4.3,4.25,4.3,50942\r\n
# NABHA,"27 Dec 2000",94.41,95,94.41,94.99,3495\r\n
#
foreach my $line (App::Chart::Download::split_lines($body)) {
my ($symbol, $date, $open, $high, $low, $close, $volume)
= split (/,/, $line);
$symbol .= '.AX';
$open = pad_decimals ($open, 2);
$high = pad_decimals ($high, 2);
$low = pad_decimals ($low, 2);
$close = pad_decimals ($close, 2);
push @data, { symbol => $symbol,
date => $date,
open => $open,
high => $high,
low => $low,
close => $close,
volume => $volume };
}
return $h;
}
sub pad_decimals {
my ($str, $want) = @_;
if ($str =~ /\.([0-9]*)$/) {
my $got = length ($1);
if ($got < $want) {
$str .= '0' x ($want - $got);
}
} else {
# no decimal point at all
$str .= '.' . ('0' x $want);
}
return $str;
}
#-----------------------------------------------------------------------------
# download - by whole day files
#
# Commsec offers the following formats,
#
# metastock - prices in dollars, date yymmdd
# metastock - prices in dollars, date yymmdd, volume in 100s
# ezychart - prices in cents, date yymmdd
# insight - prices in cents, date mm/dd/yy, space separated
# stockeasy - prices in dollars, date yyyymmdd
#
# Ezychart is used because it's the most compact -- there's no decimal
# points in most prices, and no century on the date.
#
# The web page says only the past 20 days are available and that's all the
# little menu presents, but the server actually goes back beyond that,
# apparently unlimited.
# return a url string
sub wholeday_url {
my ($tdate) = @_;
my ($year, $month, $day) = App::Chart::tdate_to_ymd ($tdate);
return sprintf 'http://charts.commsec.com.au/HistoryData/HistoryData.dll/EzyChart-%d%02d%02d?DownloadDate=%d%02d%02d&DownloadFormat=EzyChart&.txt',
$year, $month, $day,
$year, $month, $day;
}
sub wholeday_download {
my ($start_tdate, $avail_tdate) = @_;
foreach my $tdate ($start_tdate .. $avail_tdate) {
App::Chart::Download::status
(__x('CommSec data {date}',
date => App::Chart::Download::tdate_range_string ($tdate)));
my $url = wholeday_url($tdate);
my $resp = App::Chart::Download->get ($url);
# when public holiday, date too old, etc, still get a successful
# download, with an error message in the body
my $h = ezychart_parse ($resp);
App::Chart::Download::write_daily_group ($h);
}
}
sub ezychart_parse {
my ($resp) = @_;
my @data = ();
my $h = { source => __PACKAGE__,
currency => 'AUD',
prefer_decimals => 2,
date_format => 'ymd',
resp => $resp,
data => \@data,
cost_key => WHOLEDAY_COST_KEY };
my $body = $resp->decoded_content(raise_error=>1);
if ($body =~ /server error/i) {
# an unknown symbol
return $h;
}
# Sample line, 5 Sep 2008
# BHP,080905,3640,3720,3630,3700,14390282
#
foreach my $line (App::Chart::Download::split_lines($body)) {
my ($symbol, $date, $open, $high, $low, $close, $volume)
= split (/,/, $line);
$open = App::Chart::Download::cents_to_dollars ($open);
$high = App::Chart::Download::cents_to_dollars ($high);
$low = App::Chart::Download::cents_to_dollars ($low);
$close = App::Chart::Download::cents_to_dollars ($close);
push @data, { symbol => "$symbol.AX",
date => $date,
open => $open,
high => $high,
low => $low,
close => $close,
volume => $volume };
}
return $h;
}
1;
__END__