File Coverage

blib/lib/Finance/Quote/AlphaVantage.pm
Criterion Covered Total %
statement 12 108 11.1
branch 0 36 0.0
condition 0 6 0.0
subroutine 6 9 66.6
pod 0 5 0.0
total 18 164 10.9


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   2830 use strict;
  5         23  
  5         186  
26 5     5   27 use JSON qw( decode_json );
  5         10  
  5         26  
27 5     5   530 use HTTP::Request::Common;
  5         14  
  5         7471  
28              
29             our $VERSION = '1.58'; # 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 47 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 16 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, $ticker, $adjust );
166 0           my $ua = $quoter->user_agent();
167 0           my $launch_time = time();
168              
169             # Since the JSON returned by the GLOBAL_QUOTE API does not specify
170             # the currency of the price data, there is no way to determine the
171             # correct currency without an additional call to the SYMBOL_SEARCH
172             # API. To avoid even slower throttling, this module expects the
173             # user to know which securties from certain countries may be traded
174             # in the non-ISO4217 currency.
175             # Example is LSE traded GBP.L and GBPG.L. GBP.L is traded in GBX,
176             # which is also known as GBp, and GBPG.L is traded in the iso-4217
177             # currency GBP (Great Britain Pounds).
178             # The user will add ".X" to symbols to return GBX priced securities
179             # as GBP.
180              
181             my $token = exists $quoter->{module_specific_data}->{alphavantage}->{API_KEY} ?
182             $quoter->{module_specific_data}->{alphavantage}->{API_KEY} :
183 0 0         $ENV{"ALPHAVANTAGE_API_KEY"};
184              
185 0           foreach my $stock (@stocks) {
186              
187 0 0         if ($stock =~ /\.X$/) {
188 0           $adjust = 1;
189 0           ($ticker = $stock) =~ s/\.X$//;
190             } else {
191 0           $adjust = 0;
192 0           $ticker = $stock
193             }
194              
195 0 0         if ( !defined $token ) {
196 0           $info{ $stock, 'success' } = 0;
197 0           $info{ $stock, 'errormsg' } =
198             'An AlphaVantage API is required. Get an API key at https://www.alphavantage.co';
199 0           next;
200             }
201              
202             $url =
203 0           $ALPHAVANTAGE_URL
204             . '&apikey='
205             . $token
206             . '&symbol='
207             . $ticker;
208              
209             my $get_content = sub {
210 0     0     sleep_before_query();
211 0           my $time=int(time()-$launch_time);
212             # print STDERR "Query at:".$time."\n";
213 0           $reply = $ua->request( GET $url);
214              
215 0           $code = $reply->code;
216 0           $desc = HTTP::Status::status_message($code);
217 0           $body = $reply->content;
218             # print STDERR "AlphaVantage returned: $body\n";
219 0           };
220              
221 0           &$get_content();
222              
223 0 0         if ($code != 200) {
224 0           $info{ $stock, 'success' } = 0;
225 0           $info{ $stock, 'errormsg' } = $desc;
226 0           next;
227             }
228              
229 0           my $json_data;
230 0           eval {$json_data = JSON::decode_json $body};
  0            
231 0 0         if ($@) {
232 0           $info{ $stock, 'success' } = 0;
233 0           $info{ $stock, 'errormsg' } = $@;
234             }
235              
236 0           my $try_cnt = 0;
237 0   0       while (($try_cnt < 5) && ($json_data->{'Note'})) {
238             # print STDERR "NOTE:".$json_data->{'Note'}."\n";
239             # print STDERR "ADDITIONAL SLEEPING HERE !";
240 0           sleep (20);
241 0           &$get_content();
242 0           eval {$json_data = JSON::decode_json $body};
  0            
243 0           $try_cnt += 1;
244             }
245              
246 0 0         if ( !$json_data ) {
    0          
    0          
247 0           $info{ $stock, 'success' } = 0;
248 0           $info{ $stock, 'errormsg' } = 'Query returned no JSON';
249 0           next;
250             } elsif ( $json_data->{'Error Message'} ) {
251 0           $info{ $stock, 'success' } = 0;
252 0           $info{ $stock, 'errormsg' } = $json_data->{'Error Message'};
253 0           next;
254             } elsif ( $json_data->{'Information'} ) {
255 0           $info{ $stock, 'success' } = 0;
256 0           $info{ $stock, 'errormsg' } = $json_data->{'Information'};
257 0           next;
258             }
259              
260 0           my $quote = $json_data->{'Global Quote'};
261 0 0         if ( ! %{$quote} ) {
  0            
262 0           $info{ $stock, 'success' } = 0;
263 0           $info{ $stock, 'errormsg' } = "json_data does not contain Global Quote";
264 0           next;
265             }
266              
267             # %ts holds data as
268             # {
269             # "Global Quote": {
270             # "01. symbol": "SOLB.BR",
271             # "02. open": "104.2000",
272             # "03. high": "104.9500",
273             # "04. low": "103.4000",
274             # "05. price": "104.0000",
275             # "06. volume": "203059",
276             # "07. latest trading day": "2019-11-29",
277             # "08. previous close": "105.1500",
278             # "09. change": "-1.1500",
279             # "10. change percent": "-1.0937%"
280             # }
281             # }
282              
283             # remove trailing percent sign, if present
284 0           $quote->{'10. change percent'} =~ s/\%$//;
285              
286 0           $info{ $stock, 'success' } = 1;
287 0           $info{ $stock, 'success' } = 1;
288 0           $info{ $stock, 'symbol' } = $quote->{'01. symbol'};
289 0           $info{ $stock, 'open' } = $quote->{'02. open'};
290 0           $info{ $stock, 'high' } = $quote->{'03. high'};
291 0           $info{ $stock, 'low' } = $quote->{'04. low'};
292 0           $info{ $stock, 'last' } = $quote->{'05. price'};
293 0           $info{ $stock, 'volume' } = $quote->{'06. volume'};
294 0           $info{ $stock, 'close' } = $quote->{'08. previous close'};
295 0           $info{ $stock, 'net' } = $quote->{'09. change'};
296 0           $info{ $stock, 'p_change' } = $quote->{'10. change percent'};
297 0           $info{ $stock, 'method' } = 'alphavantage';
298 0           $quoter->store_date( \%info, $stock, { isodate => $quote->{'07. latest trading day'} } );
299              
300             # deduce currency
301 0 0         if ( $ticker =~ /(\..*)/ ) {
302 0           my $suffix = uc $1;
303 0 0         if ( $currencies_by_suffix{$suffix} ) {
304 0           $info{ $stock, 'currency' } = $currencies_by_suffix{$suffix};
305              
306             # divide .X quotes by 100
307 0 0         if ( $adjust == 1 ) {
308 0           foreach my $field ( $quoter->default_currency_fields ) {
309 0 0         next unless ( $info{ $stock, $field } );
310             $info{ $stock, $field } =
311 0           $quoter->scale_field( $info{ $stock, $field },
312             0.01 );
313             }
314             }
315             # divide USD quotes by 100 if suffix is '.IL'
316 0 0 0       if ( ($suffix eq '.IL') && ($info{$stock,'currency'} eq 'USD') ) {
317 0           foreach my $field ( $quoter->default_currency_fields ) {
318 0 0         next unless ( $info{ $stock, $field } );
319             $info{ $stock, $field } =
320 0           $quoter->scale_field( $info{ $stock, $field },
321             0.01 );
322             }
323             }
324             }
325             }
326             else {
327 0           $info{ $stock, 'currency' } = 'USD';
328             }
329              
330 0           $info{ $stock, "currency_set_by_fq" } = 1;
331              
332             }
333              
334 0 0         return wantarray() ? %info : \%info;
335             }
336             1;
337              
338             =head1 NAME
339              
340             Finance::Quote::AlphaVantage - Obtain quotes from https://iexcloud.io
341              
342             =head1 SYNOPSIS
343              
344             use Finance::Quote;
345            
346             $q = Finance::Quote->new('AlphaVantage', alphavantage => {API_KEY => 'your-alphavantage-api-key'});
347              
348             %info = $q->fetch('alphavantage', 'IBM', 'AAPL');
349              
350             =head1 DESCRIPTION
351              
352             This module fetches information from https://www.alphavantage.co.
353              
354             This module is loaded by default on a Finance::Quote object. It's also possible
355             to load it explicitly by placing "AlphaVantage" in the argument list to
356             Finance::Quote->new().
357              
358             This module provides the "alphavantage" fetch method.
359              
360             =head1 API_KEY
361              
362             https://www.alphavantage.co requires users to register and obtain an API key, which
363             is also called a token. The token is a sequence of random characters.
364              
365             The API key may be set by either providing a module specific hash to
366             Finance::Quote->new as in the above example, or by setting the environment
367             variable ALPHAVANTAGE_API_KEY.
368              
369             =head1 LABELS RETURNED
370              
371             The following labels may be returned by Finance::Quote::AlphaVantage :
372             symbol, open, close, high, low, last, volume, method, isodate, currency.
373              
374             =head1 CAVEATs
375              
376             Since the JSON returned by the GLOBAL_QUOTE API does not specify
377             the currency of the price data, there is no way to determine the
378             correct currency without an additional call to the SYMBOL_SEARCH
379             API. To avoid even slower throttling, this module expects the
380             user to know which securties from certain countries may be traded
381             in the non-ISO4217 currency.
382              
383             An example are London Stock Exchange traded GBP.L (Global Petroleum Limited)
384             and GBPG.L
385             (Goldman Sachs Access UK Gilts 1-10 Years UCITS ETF CLASS GBP (Dist)).
386             GBP.L is traded in GBX, which is also known as GBp (Great Britain Pence),
387             and GBPG.L is traded in the iso-4217 currency GBP (Great Britain Pounds).
388             The user will need to add ".X" to symbols to return GBX priced securities
389             as GBP. For the example above the user would use the symbol GBP.L.X in
390             the call to the alphavantage method for the prices to be output as GBP.
391              
392             =cut