#!/usr/bin/perl -w
#    This module is based on the Finance::Quote::yahooJSON module
#
#    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

# 2025-12-19: sleep_before_query uncommented. Set its parameters
#             to 1 query every 2 seconds. Error message from AV
#             indicates they are limiting free/basic use to one
#             query per second.
# 2024-04-07: Commented call to sleep_before_query in get_content
#             subroutine. AlphaVantage is not throttling API calls,
#             but limits usage with free API key to 25 per day.
# 2019-12-01: Added additional labels for net and p_change. Set
#             close to previous close as returned in the JSON.
#             Bruce Schuck (bschuck at asgard hyphen systems dot com)

package Finance::Quote::AlphaVantage;

use strict;
use JSON qw( decode_json );
use HTTP::Request::Common;

our $VERSION = '1.68'; # VERSION

# Alpha Vantage recommends that API call frequency does not extend far
# beyond ~1 call per second so that they can continue to deliver
# optimal server-side performance:
#   https://www.alphavantage.co/support/#api-key
our @alphaqueries=();
my $maxQueries = { quantity =>1 , seconds => 2}; # no more than x
                                                  # queries per y
                                                  # seconds, based on
                                                  # https://www.alphavantage.co/support/#support

my $ALPHAVANTAGE_URL =
    'https://www.alphavantage.co/query?function=GLOBAL_QUOTE&datatype=json';

my %currencies_by_suffix = (

                        # Country		City/Exchange Name
    '.US'  => "USD",    # USA		AMEX, Nasdaq, NYSE
    '.A'   => "USD",    # USA		American Stock Exchange (ASE)
    '.B'   => "USD",    # USA		Boston Stock Exchange (BOS)
    '.N'   => "USD",    # USA		Nasdaq Stock Exchange (NAS)
    '.O'   => "USD",    # USA		NYSE Stock Exchange (NYS)
    '.OB'  => "USD",    # USA		OTC Bulletin Board
    '.PK'  => "USD",    # USA		Pink Sheets
    '.X'   => "USD",    # USA		US Options
    '.BA'  => "ARS",    # Argentina	Buenos Aires
    '.VI'  => "EUR",    # Austria	Vienna
    '.AX'  => "AUD",    # Australia
    '.SA'  => "BRL",    # Brazil	Sao Paolo
    '.BR'  => "EUR",    # Belgium	Brussels
    '.TO'  => "CAD",    # Canada	Toronto
    '.TRV' => "CAD",    # Canada        Toronto Venture
    '.V'   => "CAD",    # Canada	Toronto Venture
    '.TRT' => "CAD",    # Canada        Toronto
    '.SN'  => "CLP",    # Chile		Santiago
    '.SS'  => "CNY",    # China		Shanghai
    '.SZ'  => "CNY",    # 		Shenzhen
    '.CO'  => "DKK",    # Denmark		Copenhagen
    '.PA'  => "EUR",    # France		Paris
    '.BE'  => "EUR",    # Germany		Berlin
    '.BM'  => "EUR",    # 		Bremen
    '.D'   => "EUR",    # 		Dusseldorf
    '.F'   => "EUR",    # 		Frankfurt
    '.FRK' => "EUR",    # 		Frankfurt
    '.H'   => "EUR",    # 		Hamburg
    '.HA'  => "EUR",    # 		Hanover
    '.MU'  => "EUR",    # 		Munich
    '.DEX' => "EUR",    # 		Xetra
    '.ME'  => "RUB",    # Russia	Moscow
    '.SG'  => "EUR",    # 		Stuttgart
    '.DE'  => "EUR",    # 		XETRA
    '.HK'  => "HKD",    # Hong Kong
    '.BO'  => "INR",    # India		Bombay
    '.CL'  => "INR",    # 		Calcutta
    '.NS'  => "INR",    # 		National Stock Exchange
    '.JK'  => "IDR",    # Indonesia	Jakarta
    '.I'   => "EUR",    # Ireland		Dublin
    '.TA'  => "ILS",    # Israel		Tel Aviv
    '.MI'  => "EUR",    # Italy		Milan
    '.KS'  => "KRW",    # Korea		Stock Exchange
    '.KQ'  => "KRW",    # 		KOSDAQ
    '.KL'  => "MYR",    # Malaysia	Kuala Lampur
    '.MX'  => "MXP",    # Mexico
    '.NZ'  => "NZD",    # New Zealand
    '.AS'  => "EUR",    # Netherlands	Amsterdam
    '.AMS' => "EUR",    # Netherlands	Amsterdam
    '.OL'  => "NOK",    # Norway		Oslo
    '.LM'  => "PEN",    # Peru		Lima
    '.IN'  => "EUR",    # Portugal	Lisbon
    '.SI'  => "SGD",    # Singapore
    '.BC'  => "EUR",    # Spain		Barcelona
    '.BI'  => "EUR",    # 		Bilbao
    '.MF'  => "EUR",    # 		Madrid Fixed Income
    '.MC'  => "EUR",    # 		Madrid SE CATS
    '.MA'  => "EUR",    # 		Madrid
    '.VA'  => "EUR",    # 		Valence
    '.ST'  => "SEK",    # Sweden		Stockholm
    '.STO' => "SEK",    # Sweden		Stockholm
    '.HE'  => "EUR",    # Finland		Helsinki
    '.S'   => "CHF",    # Switzerland	Zurich
    '.TW'  => "TWD",    # Taiwan		Taiwan Stock Exchange
    '.TWO' => "TWD",    # 		OTC
    '.BK'  => "THB",    # Thialand	Thailand Stock Exchange
    '.TH'  => "THB",    # 		??? From Asia.pm, (in Thai Baht)
    '.L'   => "GBP",    # United Kingdom	London
    '.IL'  => "USD",    # United Kingdom	London USD*100
    '.VX'  => "CHF",    # Switzerland
    '.SW'  => "CHF",    # Switzerland
);

our $DISPLAY    = 'AlphaVantage';
our $FEATURES   = {'API_KEY' => 'registered user API key'};
our @LABELS     = qw/date isodate open high low close volume last net p_change/;
our $METHODHASH = {subroutine => \&alphavantage, 
                   display => $DISPLAY, 
                   labels => \@LABELS,
                   features => $FEATURES};

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

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

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

sub sleep_before_query {
    # wait till we can query again
    my $q = $maxQueries->{quantity}-1;
    if ( $#alphaqueries >= $q ) {
        my $time_since_x_queries = time()-$alphaqueries[$q];
        # print STDERR "LAST QUERY $time_since_x_queries\n";
        if ($time_since_x_queries < $maxQueries->{seconds}) {
            my $sleeptime = ($maxQueries->{seconds} - $time_since_x_queries) ;
            # print STDERR "SLEEP $sleeptime\n";
            sleep( $sleeptime );
            # print STDERR "CONTINUE\n";
        }
    }
    unshift @alphaqueries, time();
    pop @alphaqueries while $#alphaqueries>$q; # remove unnecessary data
    # print STDERR join(",",@alphaqueries)."\n";
}

sub alphavantage {
    my $quoter = shift;

    my @stocks = @_;
    my $quantity = @stocks;
    my ( %info, $reply, $url, $code, $desc, $body, $ticker, $adjust );
    my $ua = $quoter->user_agent();
    my $launch_time = time();

#   Since the JSON returned by the GLOBAL_QUOTE API does not specify
#   the currency of the price data, there is no way to determine the
#   correct currency without an additional call to the SYMBOL_SEARCH
#   API. To avoid even slower throttling, this module expects the
#   user to know which securties from certain countries may be traded
#   in the non-ISO4217 currency.
#   Example is LSE traded GBP.L and GBPG.L. GBP.L is traded in GBX,
#   which is also known as GBp, and GBPG.L is traded in the iso-4217
#   currency GBP (Great Britain Pounds).
#   The user will add ".X" to symbols to return GBX priced securities
#   as GBP.

    my $token = exists $quoter->{module_specific_data}->{alphavantage}->{API_KEY} ? 
                $quoter->{module_specific_data}->{alphavantage}->{API_KEY}        :
                $ENV{"ALPHAVANTAGE_API_KEY"};

    foreach my $stock (@stocks) {

        if ($stock =~ /\.X$/) {
            $adjust = 1;
            ($ticker = $stock) =~ s/\.X$//;
        } else {
            $adjust = 0;
            $ticker = $stock
        }

        if ( !defined $token ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } =
                'An AlphaVantage API is required. Get an API key at https://www.alphavantage.co';
            next;
        }

        $url =
              $ALPHAVANTAGE_URL
            . '&apikey='
            . $token
            . '&symbol='
            . $ticker;

        my $get_content = sub {
            sleep_before_query();
            my $time=int(time()-$launch_time);
            # print STDERR "Query at:".$time."\n";
            $reply = $ua->request( GET $url);

            $code = $reply->code;
            $desc = HTTP::Status::status_message($code);
            $body = $reply->content;
            # print STDERR "AlphaVantage returned: $body\n";
        };

        &$get_content();

        if ($code != 200) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = $desc;
            next;
        }

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

        my $try_cnt = 0;
        while (($try_cnt < 5) && ($json_data->{'Note'})) {
            # print STDERR "NOTE:".$json_data->{'Note'}."\n";
            # print STDERR "ADDITIONAL SLEEPING HERE !";
            sleep (20);
            &$get_content();
            eval {$json_data = JSON::decode_json $body};
            $try_cnt += 1;
        }

        if ( !$json_data ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = 'Query returned no JSON';
            next;
        } elsif ( $json_data->{'Error Message'} ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = $json_data->{'Error Message'};
            next;
        } elsif ( $json_data->{'Information'} ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = $json_data->{'Information'};
            next;
        }

        my $quote = $json_data->{'Global Quote'};
        if ( ! $quote ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = "json_data does not contain Global Quote";
            next;
        }

        # AlphaVantage can return an empty "Global Quote" when
        # a symbol is not found.
        if ( ! $quote->{'01. symbol'} ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = "Symbol $stock not found.";
            next;
        }

        # %ts holds data as
        #  {
        #     "Global Quote": {
        #         "01. symbol": "SOLB.BR",
        #         "02. open": "104.2000",
        #         "03. high": "104.9500",
        #         "04. low": "103.4000",
        #         "05. price": "104.0000",
        #         "06. volume": "203059",
        #         "07. latest trading day": "2019-11-29",
        #         "08. previous close": "105.1500",
        #         "09. change": "-1.1500",
        #         "10. change percent": "-1.0937%"
        #     }
        # }

        # remove trailing percent sign, if present
        $quote->{'10. change percent'} =~ s/\%$//;

        $info{ $stock, 'success' } = 1;
        $info{ $stock, 'success' }  = 1;
        $info{ $stock, 'symbol' }   = $quote->{'01. symbol'};
        $info{ $stock, 'open' }     = $quote->{'02. open'};
        $info{ $stock, 'high' }     = $quote->{'03. high'};
        $info{ $stock, 'low' }      = $quote->{'04. low'};
        $info{ $stock, 'last' }     = $quote->{'05. price'};
        $info{ $stock, 'volume' }   = $quote->{'06. volume'};
        $info{ $stock, 'close' }    = $quote->{'08. previous close'};
        $info{ $stock, 'net' }      = $quote->{'09. change'};
        $info{ $stock, 'p_change' } = $quote->{'10. change percent'};
        $info{ $stock, 'method' }  = 'alphavantage';
        $quoter->store_date( \%info, $stock, { isodate => $quote->{'07. latest trading day'} } );

        # deduce currency
        if ( $ticker =~ /(\..*)/ ) {
            my $suffix = uc $1;
            if ( $currencies_by_suffix{$suffix} ) {
                $info{ $stock, 'currency' } = $currencies_by_suffix{$suffix};

                # divide .X quotes by 100
                if ( $adjust == 1 ) {
                    foreach my $field ( $quoter->default_currency_fields ) {
                        next unless ( $info{ $stock, $field } );
                        $info{ $stock, $field } =
                            $quoter->scale_field( $info{ $stock, $field },
                                                  0.01 );
                    }
                }
                # divide USD quotes by 100 if suffix is '.IL'
                if ( ($suffix eq '.IL') && ($info{$stock,'currency'} eq 'USD') ) {
                    foreach my $field ( $quoter->default_currency_fields ) {
                        next unless ( $info{ $stock, $field } );
                        $info{ $stock, $field } =
                            $quoter->scale_field( $info{ $stock, $field },
                                                  0.01 );
                    }
                }
            }
        }
        else {
            $info{ $stock, 'currency' } = 'USD';
        }

        $info{ $stock, "currency_set_by_fq" } = 1;

    }

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

=head1 NAME

Finance::Quote::AlphaVantage - Obtain quotes from https://alphavantage.co

=head1 SYNOPSIS

    use Finance::Quote;
    
    $q = Finance::Quote->new('AlphaVantage', alphavantage => {API_KEY => 'your-alphavantage-api-key'});

    %info = $q->fetch('alphavantage', 'IBM', 'AAPL');

=head1 DESCRIPTION

This module fetches information from https://www.alphavantage.co.

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

This module provides the "alphavantage" fetch method.

=head1 API_KEY

https://www.alphavantage.co requires users to register and obtain an API key, which
is also called a token.  The token is a sequence of random characters.

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 ALPHAVANTAGE_API_KEY.

=head1 LABELS RETURNED

The following labels may be returned by Finance::Quote::AlphaVantage :
symbol, open, close, high, low, last, volume, method, isodate, currency.

=head1 CAVEATs

Since the JSON returned by the GLOBAL_QUOTE API does not specify
the currency of the price data, there is no way to determine the
correct currency without an additional call to the SYMBOL_SEARCH
API. To avoid even slower throttling, this module expects the
user to know which securties from certain countries may be traded
in the non-ISO4217 currency.

An example are London Stock Exchange traded GBP.L (Global Petroleum Limited)
and GBPG.L
(Goldman Sachs Access UK Gilts 1-10 Years UCITS ETF CLASS GBP (Dist)).
GBP.L is traded in GBX, which is also known as GBp (Great Britain Pence),
and GBPG.L is traded in the iso-4217 currency GBP (Great Britain Pounds).
The user will need to add ".X" to symbols to return GBX priced securities
as GBP. For the example above the user would use the symbol GBP.L.X in
the call to the alphavantage method for the prices to be output as GBP.

=cut
