App-Chart
view release on metacpan or search on metacpan
lib/App/Chart/Yahoo.pm view on Meta::CPAN
return $h;
}
#-----------------------------------------------------------------------------
# Info - Share Names
#
# This uses the info pages like
#
# https://query2.finance.yahoo.com/v1/finance/search?q=CSCO&enableFuzzyQuery=false
#
# which is a JSON format of company information like
#
# {"explains":[],
# "count":7,
# "quotes":[{"exchange":"NMS",
# "shortname":"Cisco Systems, Inc.",
# "quoteType":"EQUITY",
# "symbol":"CSCO",
# ...
#
# There can be multiple exchanges in the quotes list. Use the first.
# Other fields include
#
# longname occasionally shorter than shortname actually
# but use shortname
#
# exchDisp longer display name for the exchange,
# eg. exchange=NMS and exchDisp=NASDAQ.
# or exchange=ASX and exchDisp=Australian.
# Think the exchange code more useful.
#
# The URL has an optional &newsCount=0 for no news events.
# Think that's the default anyway.
# {"chart":{"result":null,"error":{"code":"Bad Request","description":"Data doesn't exist for startDate = 1565740800, endDate = 1596067200"}}}
App::Chart::DownloadHandler::IndivInfo->new
(name => __('Yahoo info'),
key => 'Yahoo-info',
pred => $download_pred,
url_func => \&info_url_func,
parse => \&info_parse,
recheck_days => 14);
sub info_url_func {
my ($symbol) = @_;
return 'https://query2.finance.yahoo.com/v1/finance/search?q='
. URI::Escape::uri_escape ($symbol)
. '&enableFuzzyQuery=false';
}
sub info_parse {
my ($symbol, $resp) = @_;
my @info;
my $h = { source => __PACKAGE__,
info => \@info };
my $content = $resp->decoded_content (raise_error => 1);
my $json = JSON::decode_json($content) // {};
my $quotes = $json->{'quotes'} // [];
my $e = $quotes->[0] // {};
# Should have symbol in the data equal to $symbol requested.
# May want to be relaxed about that, as the Yahoo server allows
# different upper/lower case.
#
# my $quotes_symbol = $e->{'symbol'} // '';
# if ($quotes_symbol ne $symbol) {
# die "Yahoo info: oops, wanted symbol $symbol got \"$quotes_symbol\"";
# }
my $name = $e->{'shortname'};
if (defined $name) {
# ASX shares have the symbol repeated at the end of shortname,
# like "BHP FPO [BHP]". Seems unnecessary, so strip that.
my $end = '[' . App::Chart::symbol_sans_suffix ($symbol) .']';
$name =~ s/\s*\Q$end\E$//;
}
return { symbol => $symbol,
name => $name,
exchange => $e->{'exchange'},
};
}
#------------------------------------------------------------------------------
# Latest
#
# This uses for example
#
# https://query1.finance.yahoo.com/v7/finance/chart/BHP.AX?period1=1718841600&period2=1719532800&interval=1d&events=history&close=unadjusted
#
# periodi1 and period2 are Unix style seconds since 1 Jan 1970 GMT.
#
# https://stackoverflow.com/questions/47076404/currency-helper-of-yahoo-sorry-unable-to-process-request-at-this-time-erro
# ->
# https://stackoverflow.com/questions/47064776/has-yahoo-suddenly-today-terminated-its-finance-download-api
#
# FUTURE: Intending to switch this over to v8 the same as the daily
# data, and possibly a single common parse. But this latest quote
# way has survived without problem during cooking and crumb troubles,
# not don't need to rush to change what's working.
App::Chart::LatestHandler->new
(pred => $latest_pred,
proc => \&latest_download,
max_symbols => 1); # downloads go 1 at a time
sub latest_download {
my ($symbol_list) = @_;
foreach my $symbol (@$symbol_list) {
my $tdate = daily_available_tdate ($symbol);
App::Chart::Download::status(__('Yahoo quote'), $symbol);
my $lo_timet = tdate_to_unix($tdate - 4);
my $hi_timet = tdate_to_unix($tdate + 2);
my $events = 'history';
my $url = "https://query1.finance.yahoo.com/v7/finance/chart/"
. URI::Escape::uri_escape($symbol)
."?period1=$lo_timet"
."&period2=$hi_timet"
."&interval=1d"
."&events=$events"
."&close=unadjusted";
# unknown symbol is 404 with JSON error details
#
my $resp = App::Chart::Download->get ($url, allow_404 => 1,);
App::Chart::Download::write_latest_group
(latest_parse($symbol,$resp,$tdate));
}
}
sub latest_parse {
my ($symbol, $resp, $tdate) = @_;
my $content = $resp->decoded_content (raise_error => 1);
my $json = JSON::from_json($content);
my %record = (symbol => $symbol,
);
my $h = { source => __PACKAGE__,
resp => $resp,
prefer_decimals => 2,
date_format => 'ymd',
data => [ \%record ],
};
if (defined (my $error = $json->{'chart'}->{'error'}->{'code'})) {
$record{'error'} = $error;
}
if (my $result = $json->{'chart'}->{'result'}->[0]) {
my $meta = $result->{'meta'}
// die "Yahoo JSON oops, no meta";
$record{'currency'} = $meta->{'currency'};
$record{'exchange'} = $meta->{'exchangeName'};
my $symbol_timezone = App::Chart::TZ->for_symbol ($symbol);
# Delisted shares are known symbols and currency,
# possibly junk name and exchange,
# and no data which means no timestamp field exists.
my $timestamps = $result->{'timestamp'} // [];
if (@$timestamps) {
# timestamps are time of last trade, as can be seen by looking at
# something with low enough volume, eg. RMX.AX
#
if (defined (my $timet = $timestamps->[-1])) {
($record{'last_date'}, $record{'last_time'})
= $symbol_timezone->iso_date_time($timet);
}
if (my $indicators = $result->{'indicators'}->{'quote'}->[0]) {
foreach my $key ('open','high','low') {
if (my $aref = $indicators->{$key}) {
$record{$key} = crunch_trailing_nines($aref->[$#$timestamps]);
}
}
if (my $aref = $indicators->{'volume'}) {
$record{'volume'} = $aref->[$#$timestamps];
}
if (my $aref = $indicators->{'close'}) {
my $last = $record{'last'}
= crunch_trailing_nines($aref->[$#$timestamps]);
# "change" from second last timestamp, if there is one.
# As of Nov 2017, XAUUSD=X only ever gives a single latest
# quote from v7, no previous day to compare.
#
if (defined $last
&& scalar(@$timestamps) >= 2
&& defined(my $prev = $aref->[$#$timestamps - 1])) {
$record{'change'}
= App::Chart::decimal_sub($last, crunch_trailing_nines($prev));
}
}
}
lib/App/Chart/Yahoo.pm view on Meta::CPAN
# Date ranges in limited size chunks.
# (Don't know whether Yahoo has a limit on size of download,
# but let's try not to discover one.)
#
App::Chart::DownloadHandler::IndivChunks->new
(name => __('Yahoo'),
pred => $download_pred,
url_func => \&daily_url_func,
parse => \&daily_parse,
allow_http_codes => [400,404],
chunk_size => 250, # about 1 year at a time
available_tdate_by_symbol => \&daily_available_tdate,
available_tdate_extra => 2,
);
sub daily_available_tdate {
my ($symbol) = @_;
# As of September 2017, daily data is present for the current
# day's trade, during the trading session.
# Try reckoning it complete at 6pm.
return App::Chart::Download::tdate_today_after
(18,0, App::Chart::TZ->for_symbol ($symbol));
}
sub daily_url_func {
my ($symbol, $lo_tdate, $hi_tdate) = @_;
my $lo_timet = tdate_to_unix($lo_tdate - 2);
my $hi_timet = tdate_to_unix($hi_tdate);
# As of September 2024, dividends only appear on (or after?)
# the ex date. But try hi_timet well ahead hoping for
# upcoming dividends (ex date announced).
if ($hi_tdate >= daily_available_tdate($symbol)) {
$hi_timet += 60 * 86400;
}
return "https://query1.finance.yahoo.com/v8/finance/chart/"
. URI::Escape::uri_escape($symbol)
."?formatted=true&lang=en-US®ion=US"
."&period1=$lo_timet"
."&period2=$hi_timet"
."&interval=1d"
."&events=". URI::Escape::uri_escape('div|split')
."&close=unadjusted";
}
# $resp is a HTTP::Response object with is Yahoo v8 JSON data for $symbol..
# Return $h which is a write_daily_group() style hashref of the data.
#
sub daily_parse {
my ($symbol, $resp) = @_;
my $hi_tdate = daily_available_tdate ($symbol);
my @data = ();
my $h = { source => __PACKAGE__,
prefer_decimals => 2, # default
date_format => 'ymd',
data => \@data,
};
my $content = $resp->decoded_content (raise_error => 1);
my $json = JSON::decode_json($content);
my $result = $json->{'chart'}->{'result'}->[0];
my $meta = $result->{'meta'} // {};
# Should have symbol in the data equal to $symbol requested.
# May want to be relaxed about that, as the Yahoo server allows
# different upper/lower case. The intention is to have all
# symbols in the database as a canonical form.
#
# my $meta_symbol = $meta->{'symbol'} // '';
# if ($meta_symbol ne $symbol) {
# die "Yahoo JSON oops, symbol wanted $symbol got $meta_symbol";
# }
# Trading in pence Sterling is "GBp", such as TSCO.L
$h->{'currencies'}->{$symbol} = $meta->{'currency'};
$h->{'exchanges'}->{$symbol} = $meta->{'exchangeName'};
my $decimals = $meta->{'priceHint'};
if (defined $decimals) {
$h->{'prefer_decimals'} = $decimals;
}
$decimals //= 2;
my $gmtoffset = $meta->{'gmtoffset'};
my $timet_to_iso = sub {
my ($t) = @_;
return (defined $t ? POSIX::strftime('%Y-%m-%d', gmtime($t + $gmtoffset)) : undef);
};
my $timestamps = $result->{'timestamp'};
my $quote= $result->{'indicators'}->{'quote'}->[0];
my $opens = $quote->{'open'} // [];
my $highs = $quote->{'high'} // [];
my $lows = $quote->{'low'} // [];
my $closes = $quote->{'close'} // [];
my $volumes = $quote->{'volume'} // [];
foreach my $i (0 .. $#$timestamps) {
my $date = $timet_to_iso->($timestamps->[$i]);
if (App::Chart::Download::iso_to_tdate_floor($date) > $hi_tdate) {
# Current day's trading shows in the data.
# Don't enter it in the database until close of trade.
next;
}
push @data, { symbol => $symbol,
date => $date,
open => crunch_trailing_nines($opens->[$i]),
high => crunch_trailing_nines($highs->[$i]),
low => crunch_trailing_nines($lows->[$i]),
close => crunch_trailing_nines($closes->[$i]),
volume => $volumes->[$i] };
}
my $events = $result->{'events'} // {};
### $events
# Eg. BHP.AX
# date 1709766000
# amount 1.096196
#
( run in 1.480 second using v1.01-cache-2.11-cpan-437f7b0c052 )