#!/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

# 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.52'; # 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 =>5 , seconds => 60}; # 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
    '.V'   => "CAD",    # 		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
);


sub methods {
    return ( alphavantage => \&alphavantage,
             canada       => \&alphavantage,
             usa          => \&alphavantage,
             nyse         => \&alphavantage,
             nasdaq       => \&alphavantage,
    );
}

{
    my @labels = qw/date isodate open high low close volume last net p_change/;

    sub labels {
        return ( alphavantage => \@labels, );
    }
}

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 );
    my $ua = $quoter->user_agent();
    my $launch_time = time();

    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 ( !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='
            . $stock;

        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 || $json_data->{'Error Message'} ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } =
                $json_data->{'Error Message'} || $json_data->{'Information'};
            next;
        }

        my $quote = $json_data->{'Global Quote'};
        if ( ! %{$quote} ) {
            $info{ $stock, 'success' } = 0;
            $info{ $stock, 'errormsg' } = "json_data doesn't contain Global Quote";
            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 ( $stock =~ /(\..*)/ ) {
            my $suffix = uc $1;
            if ( $currencies_by_suffix{$suffix} ) {
                $info{ $stock, 'currency' } = $currencies_by_suffix{$suffix};

                # divide GBP quotes by 100
                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 );
                    }
                }
                # 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://iexcloud.io

=head1 SYNOPSIS

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

    %info = Finance::Quote->fetch("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.

=cut
