Group
Extension

Finance-Quote/lib/Finance/Quote/FinanceAPI.pm

#!/usr/bin/perl -w
# vi: set ts=2 sw=2 noai ic showmode showmatch:  
#
#    Copyright (C) 2024, Bruce Schuck <bschuck@asgard-systems.com>
#
#    This program 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 2 of the License, or
#    (at your option) any later version.
#
#    This program 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 this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#    02110-1301, USA

#    Changes:
#    Initial Version: 2024-08-31, Bruce Schuck

package Finance::Quote::FinanceAPI;

use strict;
use warnings;

use Encode qw(decode);
use HTTP::Request::Common;
use JSON qw( decode_json );

use constant DEBUG => $ENV{DEBUG};
use if DEBUG, 'Smart::Comments', '###';

our $VERSION = '1.67_01'; # TRIAL VERSION

my $FINANCEAPI_URL = 'https://yfapi.net/v6/finance/quote?region=US&lang=en&symbols=';

# Change DISPLAY and method values in code below
# Modify LABELS to those returned by the method

our $DISPLAY    = 'FinanceAPI';
our $FEATURES   = {'API_KEY' => 'registered user API key'};
our @LABELS     = qw/symbol name open high low last price date volume currency method/;
our $METHODHASH = {subroutine => \&financeapi, 
                   display => $DISPLAY, 
                   labels => \@LABELS,
                   features => $FEATURES};

sub methodinfo {
    return ( 
        financeapi   => $METHODHASH,
        nyse         => $METHODHASH,
        nasdaq       => $METHODHASH,
        usa          => $METHODHASH,
    );
}

sub labels { my %m = methodinfo(); return map {$_ => [@{$m{$_}{labels}}] } keys %m; }

sub methods {
  my %m = methodinfo(); return map {$_ => $m{$_}{subroutine} } keys %m;
}

sub financeapi {

  my $quoter = shift;
  my @stocks = @_;
  my (%info, $url, $reply);
  my $ua = $quoter->user_agent();

  # Set token. If not passed in as argument, get from environment.
  my $token = exists $quoter->{module_specific_data}->{stockdata}->{API_KEY} ? 
              $quoter->{module_specific_data}->{stockdata}->{API_KEY}        :
              $ENV{"FINANCEAPI_API_KEY"};

  # Set headers. API key is sent as a header.
  my @ua_headers = (
    'Accept' => 'application/json',
    'X-API-KEY' => $token,
  );

  foreach my $stock (@stocks) {

    if ( !defined $token ) {
      $info{ $stock, 'success' } = 0;
      $info{ $stock, 'errormsg' } =
        'FINANCEAPI_API_KEY not defined. Get an API key at https://financeapi.net/';
      next;
    }

    $url   = $FINANCEAPI_URL . $stock;
    $reply = $ua->get( $url, @ua_headers );

    my $code    = $reply->code;
    my $desc    = HTTP::Status::status_message($code);
    my $headers = $reply->headers_as_string;
    my $body    = $reply->content;

    ### Body: $body

    my ($quote, $name, $bid, $ask, $last, $open, $high, $low, $volume, $currency, $date);
    my ($month, $day, $year);

    $info{ $stock, "symbol" } = $stock;

    if ( $code == 200 ) {

      eval {$quote = JSON::decode_json $body};
      if ($@) {
        $info{ $stock, 'success' } = 0;
        $info{ $stock, 'errormsg' } = $@;
        next;
      }

      ### [<now>] JSON quote: $quote

      if (!exists $quote->{'quoteResponse'}) {
        $info{ $stock, 'success' } = 0;
        $info{ $stock, 'errormsg' } = $@;
        next;
      }

      # JSON Result Array is sometimes empty
      my $json_data_count = scalar @{ $quote->{'quoteResponse'}{'result'} };
      if ( $json_data_count < 1 ) {
        $info{ $stock, "success" } = 0;
        $info{ $stock, "errormsg" } =
          "Error retrieving quote for $stock - no listing for this name found. Please check symbol and the two letter extension (if any)";
        next;
      }

      $name     = $quote->{'quoteResponse'}{'result'}[0]{'longName'};
      $open     = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketOpen'};
      $high     = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketDayHigh'};
      $low      = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketDayLow'};
      $last     = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketPrice'};
      $volume   = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketVolume'};
      #$currency = $quote->{'quoteResponse'}{'result'}[0]{'financialCurrency'};
      $currency = $quote->{'quoteResponse'}{'result'}[0]{'currency'};
      $date     = $quote->{'quoteResponse'}{'result'}[0]{'regularMarketTime'};
      ($month, $day, $year) = (localtime($date))[4,3,5];
      $month++;
      $year += 1900;

      $info{$stock, 'name'}      = $name;
      $info{$stock, 'open'}      = $open;
      $info{$stock, 'high'}      = $high;
      $info{$stock, 'low'}       = $low;
      $info{$stock, 'last'}      = $last;
      $info{$stock, 'price'}     = $last;
      $info{$stock, 'volume'}    = $volume;
      $info{$stock, 'currency'}  = $currency;
      $info{ $stock, 'method' }  = 'financeapi';
      $quoter->store_date(\%info, $stock, {month => $month, day => $day, year => $year});

      # Check for stocks traded in pence instead of pounds
      # Convert GBp or GBX to GBP (divide price by 100).
      # GBP.L - pence
      # GBPG.L - pounds

      if ( ($info{$stock,"currency"} eq "GBp") ||
         ($info{$stock,"currency"} eq "GBX")) {
        foreach my $field ( $quoter->default_currency_fields ) {
          next unless ( $info{ $stock, $field } );
          $info{ $stock, $field }
            = $quoter->scale_field( $info{ $stock, $field }, 0.01 );
        }
        $info{ $stock, "currency"} = "GBP";
      }

      # Apply the same hack for Johannesburg Stock Exchange
      # (JSE) prices as they are returned in ZAc (cents)
      # instead of ZAR (rands). JSE symbols are suffixed
      # with ".JO" when querying Yahoo e.g. ANG.JO

      if ($info{$stock,"currency"} eq "ZAc") {
        foreach my $field ( $quoter->default_currency_fields ) {
          next unless ( $info{ $stock, $field } );
          $info{ $stock, $field }
            = $quoter->scale_field( $info{ $stock, $field }, 0.01 );
        }
        $info{ $stock, "currency"} = "ZAR";
      }

      # Apply the same hack for Tel Aviv Stock Exchange
      # (TASE) prices as they are returned in ILA (Agorot)
      # instead of ILS (Shekels). TASE symbols are suffixed
      # with ".TA" when querying Yahoo e.g. POLI.TA

      if ($info{$stock,"currency"} eq "ILA") {
        foreach my $field ( $quoter->default_currency_fields ) {
          next unless ( $info{ $stock, $field } );
          $info{ $stock, $field }
            = $quoter->scale_field( $info{ $stock, $field }, 0.01 );
        }
        $info{ $stock, "currency"} = "ILS";
      }

      $info{ $stock, 'success' } = 1;

    } else {       # HTTP Request failed (code != 200)
      $info{ $stock, "success" } = 0;
      $info{ $stock, "errormsg" } =
        "Error retrieving quote for $stock. Attempt to fetch the URL $url resulted in HTTP response $code ($desc)";
    }

  }  

  return wantarray() ? %info : \%info;
  return \%info;

}

1;

__END__

=head1 NAME

Finance::Quote::FinanceAPI - Obtain quotes from financeapi.net.

=head1 SYNOPSIS

    use Finance::Quote;

    # API Key passed during object creation
    $q = Finance::Quote->new('FinanceAPI', financeapi => {API_KEY => 'your-financeapi-api-key'});

    # FINANCEAPI_API_KEY environment variable set
    $q = Finance::Quote->new;

    %info = $q->fetch("financeapi", "AAPL");  # Only query financeapi

=head1 DESCRIPTION

This module fetches information from L<https://financeapi.net/>. The API URL
is https://yfapi.net/.

This module is loaded by default on a Finance::Quote object. It's also possible
to load it explicitly by placing "financeapi" in the argument list to
Finance::Quote->new().

This module provides the "financeapi" fetch method. In addition it
advertises "nyse", "usa", and "nasdaq".

=head1 API_KEY

L<https://financeapi.net/> requires users to register for an API Key
(token).
The free "Basic" API Key allows 100 queries per day and a 300 per minute
rate.

The API key may be set by either providing a module specific hash to
Finance::Quote->new as in the above example, or by setting the environment
variable FINANCEAPI_API_KEY.

=head1 LABELS RETURNED

The following labels are returned: 

=over

=item name

=item symbol

=item open

=item high

=item low

=item price

=item date

=item volume

=item currency

=back

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.