File Coverage

blib/lib/Finance/Quote/AlphaVantage.pm
Criterion Covered Total %
statement 12 103 11.6
branch 0 34 0.0
condition 0 9 0.0
subroutine 6 9 66.6
pod 0 5 0.0
total 18 160 11.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             # This module is based on the Finance::Quote::yahooJSON module
3             #
4             # This program is free software; you can redistribute it and/or modify
5             # it under the terms of the GNU General Public License as published by
6             # the Free Software Foundation; either version 2 of the License, or
7             # (at your option) any later version.
8             #
9             # This program is distributed in the hope that it will be useful,
10             # but WITHOUT ANY WARRANTY; without even the implied warranty of
11             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12             # GNU General Public License for more details.
13             #
14             # You should have received a copy of the GNU General Public License
15             # along with this program; if not, write to the Free Software
16             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17             # 02110-1301, USA
18              
19             # 2019-12-01: Added additional labels for net and p_change. Set
20             # close to previous close as returned in the JSON.
21             # Bruce Schuck (bschuck at asgard hyphen systems dot com)
22              
23             package Finance::Quote::AlphaVantage;
24              
25 5     5   3207 use strict;
  5         20  
  5         178  
26 5     5   28 use JSON qw( decode_json );
  5         9  
  5         35  
27 5     5   506 use HTTP::Request::Common;
  5         11  
  5         7433  
28              
29             our $VERSION = '1.57_03'; # TRIAL VERSION
30              
31             # Alpha Vantage recommends that API call frequency does not extend far
32             # beyond ~1 call per second so that they can continue to deliver
33             # optimal server-side performance:
34             # https://www.alphavantage.co/support/#api-key
35             our @alphaqueries=();
36             my $maxQueries = { quantity =>5 , seconds => 60}; # no more than x
37             # queries per y
38             # seconds, based on
39             # https://www.alphavantage.co/support/#support
40              
41             my $ALPHAVANTAGE_URL =
42             'https://www.alphavantage.co/query?function=GLOBAL_QUOTE&datatype=json';
43              
44             my %currencies_by_suffix = (
45              
46             # Country City/Exchange Name
47             '.US' => "USD", # USA AMEX, Nasdaq, NYSE
48             '.A' => "USD", # USA American Stock Exchange (ASE)
49             '.B' => "USD", # USA Boston Stock Exchange (BOS)
50             '.N' => "USD", # USA Nasdaq Stock Exchange (NAS)
51             '.O' => "USD", # USA NYSE Stock Exchange (NYS)
52             '.OB' => "USD", # USA OTC Bulletin Board
53             '.PK' => "USD", # USA Pink Sheets
54             '.X' => "USD", # USA US Options
55             '.BA' => "ARS", # Argentina Buenos Aires
56             '.VI' => "EUR", # Austria Vienna
57             '.AX' => "AUD", # Australia
58             '.SA' => "BRL", # Brazil Sao Paolo
59             '.BR' => "EUR", # Belgium Brussels
60             '.TO' => "CAD", # Canada Toronto
61             '.TRV' => "CAD", # Canada Toronto Venture
62             '.V' => "CAD", # Canada Toronto Venture
63             '.TRT' => "CAD", # Canada Toronto
64             '.SN' => "CLP", # Chile Santiago
65             '.SS' => "CNY", # China Shanghai
66             '.SZ' => "CNY", # Shenzhen
67             '.CO' => "DKK", # Denmark Copenhagen
68             '.PA' => "EUR", # France Paris
69             '.BE' => "EUR", # Germany Berlin
70             '.BM' => "EUR", # Bremen
71             '.D' => "EUR", # Dusseldorf
72             '.F' => "EUR", # Frankfurt
73             '.FRK' => "EUR", # Frankfurt
74             '.H' => "EUR", # Hamburg
75             '.HA' => "EUR", # Hanover
76             '.MU' => "EUR", # Munich
77             '.DEX' => "EUR", # Xetra
78             '.ME' => "RUB", # Russia Moscow
79             '.SG' => "EUR", # Stuttgart
80             '.DE' => "EUR", # XETRA
81             '.HK' => "HKD", # Hong Kong
82             '.BO' => "INR", # India Bombay
83             '.CL' => "INR", # Calcutta
84             '.NS' => "INR", # National Stock Exchange
85             '.JK' => "IDR", # Indonesia Jakarta
86             '.I' => "EUR", # Ireland Dublin
87             '.TA' => "ILS", # Israel Tel Aviv
88             '.MI' => "EUR", # Italy Milan
89             '.KS' => "KRW", # Korea Stock Exchange
90             '.KQ' => "KRW", # KOSDAQ
91             '.KL' => "MYR", # Malaysia Kuala Lampur
92             '.MX' => "MXP", # Mexico
93             '.NZ' => "NZD", # New Zealand
94             '.AS' => "EUR", # Netherlands Amsterdam
95             '.AMS' => "EUR", # Netherlands Amsterdam
96             '.OL' => "NOK", # Norway Oslo
97             '.LM' => "PEN", # Peru Lima
98             '.IN' => "EUR", # Portugal Lisbon
99             '.SI' => "SGD", # Singapore
100             '.BC' => "EUR", # Spain Barcelona
101             '.BI' => "EUR", # Bilbao
102             '.MF' => "EUR", # Madrid Fixed Income
103             '.MC' => "EUR", # Madrid SE CATS
104             '.MA' => "EUR", # Madrid
105             '.VA' => "EUR", # Valence
106             '.ST' => "SEK", # Sweden Stockholm
107             '.STO' => "SEK", # Sweden Stockholm
108             '.HE' => "EUR", # Finland Helsinki
109             '.S' => "CHF", # Switzerland Zurich
110             '.TW' => "TWD", # Taiwan Taiwan Stock Exchange
111             '.TWO' => "TWD", # OTC
112             '.BK' => "THB", # Thialand Thailand Stock Exchange
113             '.TH' => "THB", # ??? From Asia.pm, (in Thai Baht)
114             '.L' => "GBP", # United Kingdom London
115             '.IL' => "USD", # United Kingdom London USD*100
116             '.VX' => "CHF", # Switzerland
117             '.SW' => "CHF", # Switzerland
118             );
119              
120              
121             sub methods {
122 5     5 0 37 return ( alphavantage => \&alphavantage,
123             canada => \&alphavantage,
124             usa => \&alphavantage,
125             nyse => \&alphavantage,
126             nasdaq => \&alphavantage,
127             );
128             }
129              
130             sub parameters {
131 1     1 0 5 return ('API_KEY');
132             }
133              
134             {
135             my @labels = qw/date isodate open high low close volume last net p_change/;
136              
137             sub labels {
138 5     5 0 19 return ( alphavantage => \@labels, );
139             }
140             }
141              
142             sub sleep_before_query {
143             # wait till we can query again
144 0     0 0   my $q = $maxQueries->{quantity}-1;
145 0 0         if ( $#alphaqueries >= $q ) {
146 0           my $time_since_x_queries = time()-$alphaqueries[$q];
147             # print STDERR "LAST QUERY $time_since_x_queries\n";
148 0 0         if ($time_since_x_queries < $maxQueries->{seconds}) {
149 0           my $sleeptime = ($maxQueries->{seconds} - $time_since_x_queries) ;
150             # print STDERR "SLEEP $sleeptime\n";
151 0           sleep( $sleeptime );
152             # print STDERR "CONTINUE\n";
153             }
154             }
155 0           unshift @alphaqueries, time();
156 0           pop @alphaqueries while $#alphaqueries>$q; # remove unnecessary data
157             # print STDERR join(",",@alphaqueries)."\n";
158             }
159              
160             sub alphavantage {
161 0     0 0   my $quoter = shift;
162              
163 0           my @stocks = @_;
164 0           my $quantity = @stocks;
165 0           my ( %info, $reply, $url, $code, $desc, $body );
166 0           my $ua = $quoter->user_agent();
167 0           my $launch_time = time();
168              
169             my $token = exists $quoter->{module_specific_data}->{alphavantage}->{API_KEY} ?
170             $quoter->{module_specific_data}->{alphavantage}->{API_KEY} :
171 0 0         $ENV{"ALPHAVANTAGE_API_KEY"};
172              
173 0           foreach my $stock (@stocks) {
174              
175 0 0         if ( !defined $token ) {
176 0           $info{ $stock, 'success' } = 0;
177 0           $info{ $stock, 'errormsg' } =
178             'An AlphaVantage API is required. Get an API key at https://www.alphavantage.co';
179 0           next;
180             }
181              
182             $url =
183 0           $ALPHAVANTAGE_URL
184             . '&apikey='
185             . $token
186             . '&symbol='
187             . $stock;
188              
189             my $get_content = sub {
190 0     0     sleep_before_query();
191 0           my $time=int(time()-$launch_time);
192             # print STDERR "Query at:".$time."\n";
193 0           $reply = $ua->request( GET $url);
194              
195 0           $code = $reply->code;
196 0           $desc = HTTP::Status::status_message($code);
197 0           $body = $reply->content;
198             # print STDERR "AlphaVantage returned: $body\n";
199 0           };
200              
201 0           &$get_content();
202              
203 0 0         if ($code != 200) {
204 0           $info{ $stock, 'success' } = 0;
205 0           $info{ $stock, 'errormsg' } = $desc;
206 0           next;
207             }
208              
209 0           my $json_data;
210 0           eval {$json_data = JSON::decode_json $body};
  0            
211 0 0         if ($@) {
212 0           $info{ $stock, 'success' } = 0;
213 0           $info{ $stock, 'errormsg' } = $@;
214             }
215              
216 0           my $try_cnt = 0;
217 0   0       while (($try_cnt < 5) && ($json_data->{'Note'})) {
218             # print STDERR "NOTE:".$json_data->{'Note'}."\n";
219             # print STDERR "ADDITIONAL SLEEPING HERE !";
220 0           sleep (20);
221 0           &$get_content();
222 0           eval {$json_data = JSON::decode_json $body};
  0            
223 0           $try_cnt += 1;
224             }
225              
226 0 0         if ( !$json_data ) {
    0          
    0          
227 0           $info{ $stock, 'success' } = 0;
228 0           $info{ $stock, 'errormsg' } = 'Query returned no JSON';
229 0           next;
230             } elsif ( $json_data->{'Error Message'} ) {
231 0           $info{ $stock, 'success' } = 0;
232 0           $info{ $stock, 'errormsg' } = $json_data->{'Error Message'};
233 0           next;
234             } elsif ( $json_data->{'Information'} ) {
235 0           $info{ $stock, 'success' } = 0;
236 0           $info{ $stock, 'errormsg' } = $json_data->{'Information'};
237 0           next;
238             }
239              
240 0           my $quote = $json_data->{'Global Quote'};
241 0 0         if ( ! %{$quote} ) {
  0            
242 0           $info{ $stock, 'success' } = 0;
243 0           $info{ $stock, 'errormsg' } = "json_data doesn't contain Global Quote";
244 0           next;
245             }
246              
247             # %ts holds data as
248             # {
249             # "Global Quote": {
250             # "01. symbol": "SOLB.BR",
251             # "02. open": "104.2000",
252             # "03. high": "104.9500",
253             # "04. low": "103.4000",
254             # "05. price": "104.0000",
255             # "06. volume": "203059",
256             # "07. latest trading day": "2019-11-29",
257             # "08. previous close": "105.1500",
258             # "09. change": "-1.1500",
259             # "10. change percent": "-1.0937%"
260             # }
261             # }
262              
263             # remove trailing percent sign, if present
264 0           $quote->{'10. change percent'} =~ s/\%$//;
265              
266 0           $info{ $stock, 'success' } = 1;
267 0           $info{ $stock, 'success' } = 1;
268 0           $info{ $stock, 'symbol' } = $quote->{'01. symbol'};
269 0           $info{ $stock, 'open' } = $quote->{'02. open'};
270 0           $info{ $stock, 'high' } = $quote->{'03. high'};
271 0           $info{ $stock, 'low' } = $quote->{'04. low'};
272 0           $info{ $stock, 'last' } = $quote->{'05. price'};
273 0           $info{ $stock, 'volume' } = $quote->{'06. volume'};
274 0           $info{ $stock, 'close' } = $quote->{'08. previous close'};
275 0           $info{ $stock, 'net' } = $quote->{'09. change'};
276 0           $info{ $stock, 'p_change' } = $quote->{'10. change percent'};
277 0           $info{ $stock, 'method' } = 'alphavantage';
278 0           $quoter->store_date( \%info, $stock, { isodate => $quote->{'07. latest trading day'} } );
279              
280             # deduce currency
281 0 0         if ( $stock =~ /(\..*)/ ) {
282 0           my $suffix = uc $1;
283 0 0         if ( $currencies_by_suffix{$suffix} ) {
284 0           $info{ $stock, 'currency' } = $currencies_by_suffix{$suffix};
285              
286             # divide GBP quotes by 100
287 0 0 0       if ( ($info{ $stock, 'currency' } eq 'GBP') || ($info{$stock,'currency'} eq 'GBX') ) {
288 0           foreach my $field ( $quoter->default_currency_fields ) {
289 0 0         next unless ( $info{ $stock, $field } );
290             $info{ $stock, $field } =
291 0           $quoter->scale_field( $info{ $stock, $field },
292             0.01 );
293             }
294             }
295             # divide USD quotes by 100 if suffix is '.IL'
296 0 0 0       if ( ($suffix eq '.IL') && ($info{$stock,'currency'} eq 'USD') ) {
297 0           foreach my $field ( $quoter->default_currency_fields ) {
298 0 0         next unless ( $info{ $stock, $field } );
299             $info{ $stock, $field } =
300 0           $quoter->scale_field( $info{ $stock, $field },
301             0.01 );
302             }
303             }
304             }
305             }
306             else {
307 0           $info{ $stock, 'currency' } = 'USD';
308             }
309              
310 0           $info{ $stock, "currency_set_by_fq" } = 1;
311              
312             }
313              
314 0 0         return wantarray() ? %info : \%info;
315             }
316             1;
317              
318             =head1 NAME
319              
320             Finance::Quote::AlphaVantage - Obtain quotes from https://iexcloud.io
321              
322             =head1 SYNOPSIS
323              
324             use Finance::Quote;
325            
326             $q = Finance::Quote->new('AlphaVantage', alphavantage => {API_KEY => 'your-alphavantage-api-key'});
327              
328             %info = $q->fetch('alphavantage', 'IBM', 'AAPL');
329              
330             =head1 DESCRIPTION
331              
332             This module fetches information from https://www.alphavantage.co.
333              
334             This module is loaded by default on a Finance::Quote object. It's also possible
335             to load it explicitly by placing "AlphaVantage" in the argument list to
336             Finance::Quote->new().
337              
338             This module provides the "alphavantage" fetch method.
339              
340             =head1 API_KEY
341              
342             https://www.alphavantage.co requires users to register and obtain an API key, which
343             is also called a token. The token is a sequence of random characters.
344              
345             The API key may be set by either providing a module specific hash to
346             Finance::Quote->new as in the above example, or by setting the environment
347             variable ALPHAVANTAGE_API_KEY.
348              
349             =head1 LABELS RETURNED
350              
351             The following labels may be returned by Finance::Quote::AlphaVantage :
352             symbol, open, close, high, low, last, volume, method, isodate, currency.
353              
354             =cut