App-Chart

 view release on metacpan or  search on metacpan

lib/App/Chart/Yahoo.pm  view on Meta::CPAN


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));
          }
        }
      }
    }

    if (defined $record{'last_date'}
        && (my $splits = $result->{'events'}->{'splits'})) {
      while (my ($timet, $href) = each %$splits) {
        my $split_date = $symbol_timezone->iso_date($timet);
        if ($split_date eq $record{'last_date'}) {
          __x('Split {ratio}', ratio => $href->{'splitRatio'})
        }
      }
    }
  }
  return $h;
}


#-----------------------------------------------------------------------------
# Download Data, including dividends and splits
#
# This uses the "v8" historical prices downloads in JSON format like
#
#     https://query2.finance.yahoo.com/v8/finance/chart/IBM?period1=1504028419&period2=1504428419&interval=1d&events=div%7Csplit&close=unadjusted
#
# period1 is the start time, period2 the end time, both as Unix
# seconds since 1 Jan 1970 in GMT.
#
# close=unadjusted means prices are without any adjustment for
# splits, so prices as traded at the time.
#
# If no trading in the date range (eg. before first listing) then
#
#     400 Bad Request
#     {"chart":{"result":null,"error":{"code":"Bad Request","description":"Data doesn't exist for startDate = 1565827200, endDate = 1596153600"}}}
#

# One download for each symbol.
# 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&region=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.



( run in 1.020 second using v1.01-cache-2.11-cpan-39bf76dae61 )