File Coverage

blib/lib/Finance/Quote/MGEX.pm
Criterion Covered Total %
statement 23 170 13.5
branch 4 52 7.6
condition 3 15 20.0
subroutine 7 17 41.1
pod 0 5 0.0
total 37 259 14.2


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016, 2019 Kevin Ryde
2              
3             # This file is part of Finance-Quote-Grab.
4             #
5             # Finance-Quote-Grab is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # Finance-Quote-Grab is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13             # Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License
16             # along with Finance-Quote-Grab. If not, see <http://www.gnu.org/licenses/>.
17              
18             package Finance::Quote::MGEX;
19 1     1   966 use 5.005;
  1         4  
20 1     1   5 use strict;
  1         2  
  1         24  
21              
22 1     1   4 use vars '$VERSION';
  1         2  
  1         121  
23             $VERSION = 15;
24              
25             # uncomment this to run the ### lines
26             # use Smart::Comments;
27              
28             sub methods {
29 0     0 0 0 return (mgex => \&mgex_quotes);
30             }
31             sub labels {
32 0     0 0 0 return (mgex => [ qw(name currency
33             bid ask
34             open high low last close net
35             method source success errormsg
36              
37             contract_month_iso time
38             ) ]);
39             }
40              
41             # These are about 30 kbytes and 25 kbytes, and update every 60 seconds
42             # apparently, but there's no ETag or Last-Modified to save re-downloading.
43             #
44 1         94 use constant MGEX_AQUOTES_URL =>
45 1     1   14 'http://sites.barchart.com/pl/mgex/aquotes.htx';
  1         2  
46 1         2319 use constant MGEX_WQUOTES_URL =>
47 1     1   6 'http://sites.barchart.com/pl/mgex/wquotes_js.js';
  1         3  
48              
49             my %aq_url = (a => MGEX_AQUOTES_URL,
50             w => MGEX_WQUOTES_URL);
51              
52              
53             # For individual quotes, but the pages are bigger than the wquote/aquote
54             # # eg. http://www.mgex.com/quotes.html?page=quote&sym=MW
55             # use constant MGEX_QUOTES_BASE =>
56             # 'http://www.mgex.com/quotes.html?page=quote&sym=';
57              
58             sub mgex_quotes {
59 0     0 0 0 my ($fq, @symbol_list) = @_;
60             ### mgex_quotes() ...
61             ### @symbol_list
62              
63 0         0 my $ua = $fq->user_agent;
64 0         0 my %quotes;
65              
66             # while (@symbol_list) {
67             # my $symbol = shift @symbol_list;
68             # my $commodity = symbol_to_commodity($symbol);
69             # ### $commodity
70             # unless ($commodity) {
71             # _errormsg (\%quotes, [$symbol], 'No such symbol');
72             # next;
73             # }
74             # my $this_list = [ $symbol ];
75             #
76             # }
77              
78              
79             # split into symbols Ixxxx and AJxxx which are aquote and the rest wquote
80             my @aq_keys;
81 0         0 my %aq_symbol_list;
82 0         0 foreach my $symbol (@symbol_list) {
83 0 0       0 my $key = ($symbol =~ /^[AI]/ ? 'a' : 'w');
84 0 0       0 unless ($aq_symbol_list{$key}) {
85 0         0 push @aq_keys, $key;
86             }
87 0         0 push @{$aq_symbol_list{$key}}, $symbol;
  0         0  
88             }
89             ### @aq_keys
90             ### %aq_symbol_list
91              
92 0         0 foreach my $aq (@aq_keys) {
93 0         0 require HTTP::Request;
94 0         0 my $req = HTTP::Request->new ('GET', $aq_url{$aq});
95 0         0 $ua->prepare_request ($req);
96 0         0 $req->accept_decodable; # we use decoded_content() below
97 0         0 $req->user_agent (__PACKAGE__."/$VERSION " . $req->user_agent);
98             ### req: $req->as_string
99              
100 0         0 my $resp = $ua->request ($req);
101 0         0 resp_to_quotes ($fq, $resp, \%quotes, $aq_symbol_list{$aq});
102             }
103 0 0       0 return wantarray() ? %quotes : \%quotes;
104             }
105              
106             sub symbol_to_commodity {
107 0     0 0 0 my ($str) = @_;
108 0         0 $str =~ s/[A-Z][0-9]+$//;
109 0         0 return $str;
110             }
111              
112             my %aquote_name_to_commodity
113             = ('PIT NCI' => 'IC',
114             'NCI' => 'IC',
115             'HRWI' => 'IH',
116             'HRSI' => 'IP',
117             'SRWI' => 'IW',
118             'NSI' => 'IS',
119              
120             # Apple Juice gone in 2019.
121             # 'AJC' => 'AJ',
122             );
123              
124             my %month_code_to_month = ('F' => 1,
125             'G' => 2,
126             'H' => 3,
127             'J' => 4,
128             'K' => 5,
129             'M' => 6,
130             'N' => 7,
131             'Q' => 8,
132             'U' => 9,
133             'V' => 10,
134             'X' => 11,
135             'Z' => 12);
136             my @month_to_month_code
137             = (undef, 'F','G','H','J','K','M','N','Q','U','V','X','Z');
138              
139             my %month_name_to_number = ('jan' => 1,
140             'feb' => 2,
141             'mar' => 3,
142             'apr' => 4,
143             'may' => 5,
144             'jun' => 6,
145             'jul' => 7,
146             'aug' => 8,
147             'sep' => 9,
148             'oct' => 10,
149             'nov' => 11,
150             'dec' => 12);
151              
152              
153             sub _name_to_NSCM {
154 0     0   0 my ($name) = @_;
155             ### _name_to_NSCM(): $name
156 0         0 my ($symbol, $commodity, $month, $y);
157              
158 0 0       0 if ($name =~ /^((PIT )?[A-Z]+) ([A-Za-z]+) '([0-9][0-9])$/) {
    0          
159             ### aquotes.htx name ...
160             # "SRWI Feb '06"
161             # "PIT NCI Jan '06"
162             #
163             # in the past there were call options too, but not now
164             # "NCI Mar '07 1900 Call"
165             #
166 0         0 $name = $1;
167 0         0 $commodity = $1;
168 0         0 my $month_name = $3;
169 0         0 $y = $4;
170              
171 0   0     0 $commodity = $aquote_name_to_commodity{$commodity}
172             || return; # if unrecognised
173 0   0     0 $month = $month_name_to_number{lc($month_name)}
174             || return; # if unrecognised
175 0         0 $symbol = $commodity
176             . $month_to_month_code[$month]
177             . $y; # two digit year
178              
179             } elsif ($name =~ m{\((([A-Z]+)([A-Z])([0-9]+))\)}) {
180             # wquotes_js.js name like
181             # "MGEX (MWN9)"
182             # "KCBT (KEZ9)"
183             #
184 0         0 $name = undef;
185 0         0 $symbol = $1;
186 0         0 $commodity = $2;
187 0         0 my $month_code = $3;
188 0   0     0 $month = $month_code_to_month{$month_code}
189             || return; # if unrecognised
190 0         0 $y = $4;
191              
192             } else {
193 0         0 return;
194             }
195              
196 0         0 my $year = _y_to_year($y);
197             ### $year
198 0         0 my $contract_month = sprintf ('%04d-%02d-01', $year, $month);
199              
200 0         0 return ($name, $symbol, $commodity, $contract_month);
201             }
202              
203             sub _y_to_year {
204 0     0   0 my ($y) = @_;
205 0 0       0 my $modulus = (length($y) == 1 ? 10 : 100);
206 0         0 my $half = $modulus / 2;
207 0         0 my $base = _this_year() - $half;
208 0         0 return $base + (($y - $base) % $modulus);
209             }
210             sub _this_year {
211 0     0   0 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time());
212 0         0 return $year + 1900;
213             }
214              
215             # store to hashref $quotes all the $symbol_list symbols picked out of a
216             # HTTP::Response in $resp
217             sub resp_to_quotes {
218 0     0 0 0 my ($fq, $resp, $quotes, $symbol_list) = @_;
219              
220 0         0 my %want_symbol;
221 0         0 @want_symbol{@$symbol_list} = (); # hash slice
222 0         0 my %seen_symbol;
223              
224 0         0 foreach my $symbol (@$symbol_list) {
225 0         0 $quotes->{$symbol,'method'} = 'mgex';
226 0         0 $quotes->{$symbol,'source'} = __PACKAGE__;
227 0         0 $quotes->{$symbol,'success'} = 0; # false if not in returned
228             }
229              
230 0 0       0 if (! $resp->is_success) {
231 0         0 _errormsg ($quotes, $symbol_list, $resp->status_line);
232 0         0 return;
233             }
234 0         0 my $content = $resp->decoded_content (raise_error => 1);
235              
236 0         0 $content = _javascript_document_write ($content);
237             ### $content
238 0         0 $content =~ s/&nbsp;/ /g;
239              
240 0         0 my $page_date;
241 0 0       0 if ($content =~ /for ([a-zA-Z]+ [0-9]{1,2}, [0-9]{4})/) {
242 0         0 $page_date = $1;
243             ### $page_date
244             }
245              
246 0         0 require HTML::TableExtract;
247 0         0 my $te = HTML::TableExtract->new
248             (headers => [ qr/Contract/i,
249             qr/Last/i,
250             qr/Change/i,
251             qr/Bid/,
252             qr/Ask/i,
253             qr/Open/i,
254             qr/High/,
255             qr/Low/i,
256             qr/Settle/i,
257             qr/Time/i ]);
258 0         0 $te->parse ($content);
259 0 0       0 if (! $te->tables) {
260 0         0 _errormsg ($quotes, $symbol_list, 'table not matched');
261 0         0 return;
262             }
263              
264 0         0 foreach my $row ($te->rows) {
265             ### $row
266 0 0       0 if (! defined $row->[0]) {
267             ### undef empty row, skip ...
268 0         0 next;
269             }
270              
271             my ($orig_name, $last, $change, $bid, $ask, $open, $high, $low,
272             $prev, $last_time)
273 0         0 = map { my $str = $_;
  0         0  
274 0         0 $str =~ s/^\s+//;
275 0         0 $str =~ s/\s+$//;
276 0         0 $str } @$row;
277              
278 0         0 my ($name, $symbol, $commodity, $contract_month)
279             = _name_to_NSCM ($orig_name);
280 0 0       0 if (! defined $symbol) {
281             ### unrecognised row: $orig_name
282 0         0 next;
283             }
284             ### $name
285             ### $symbol
286             ### $commodity
287             ### $contract_month
288 0 0       0 if (! exists $want_symbol{$symbol}) {
289             ### not wanted: $symbol
290 0         0 next;
291             }
292              
293             # "5 x 195-2" or whatever for count of bid/offers
294             # seen in 2006, but maybe no longer generated
295 0         0 my ($bid_count, $ask_count);
296 0 0       0 if ($bid =~ s/([0-9]+) x //) { $bid_count = $1; }
  0         0  
297 0 0       0 if ($ask =~ s/([0-9]+) x //) { $ask_count = $1; }
  0         0  
298              
299             # trailing "s" for settlement price
300 0         0 $last =~ s/s$//i;
301              
302             # "unch" for no change
303 0 0       0 if ($change =~ /unch/i) { $change = 0; }
  0         0  
304              
305 0         0 $bid = _dash_frac_to_decimals ($bid);
306 0         0 $ask = _dash_frac_to_decimals ($ask);
307              
308 0         0 $open = _dash_frac_to_decimals ($open);
309 0         0 $high = _dash_frac_to_decimals ($high);
310 0         0 $low = _dash_frac_to_decimals ($low);
311 0         0 $last = _dash_frac_to_decimals ($last);
312 0         0 $prev = _dash_frac_to_decimals ($prev);
313 0         0 $change = _dash_frac_to_decimals ($change);
314              
315 0         0 my $date = $page_date;
316             ### $last_time
317 0 0       0 if ($last_time =~ m{^\d+/\d+/\d+$}) {
318             ### "Time" field like "09/26/11" in wquote ...
319 0         0 $date = $last_time;
320 0         0 undef $last_time;
321             }
322              
323 0         0 $quotes->{$symbol,'name'} = $name;
324 0         0 $quotes->{$symbol,'currency'} = 'USD';
325 0         0 $quotes->{$symbol,'contract_month_iso'} = $contract_month;
326              
327 0 0       0 if (defined $date) {
328 0         0 $fq->store_date($quotes, $symbol, {usdate => $date});
329             }
330 0         0 $quotes->{$symbol,'time'} = $last_time;
331              
332 0         0 $quotes->{$symbol,'bid'} = $bid;
333 0         0 $quotes->{$symbol,'ask'} = $ask;
334 0 0       0 if (defined $bid_count) {
335 0         0 $quotes->{$symbol,'bid_count'} = $bid_count;
336             }
337 0 0       0 if (defined $ask_count) {
338 0         0 $quotes->{$symbol,'ask_count'} = $ask_count;
339             }
340 0         0 $quotes->{$symbol,'open'} = $open;
341 0         0 $quotes->{$symbol,'high'} = $high;
342 0         0 $quotes->{$symbol,'low'} = $low;
343 0         0 $quotes->{$symbol,'last'} = $last;
344 0         0 $quotes->{$symbol,'net'} = $change;
345 0         0 $quotes->{$symbol,'close'} = $prev;
346 0         0 $quotes->{$symbol,'success'} = 1;
347              
348 0         0 $seen_symbol{$symbol} = 1;
349             }
350              
351             # message in any not seen in page
352 0         0 delete @want_symbol{keys %seen_symbol}; # hash slice
353 0         0 _errormsg ($quotes, [keys %want_symbol], 'No such symbol');
354             }
355              
356             sub _errormsg {
357 0     0   0 my ($quotes, $symbol_list, $errormsg) = @_;
358 0         0 foreach my $symbol (@$symbol_list) {
359 0         0 $quotes->{$symbol,'errormsg'} = $errormsg;
360             }
361             }
362              
363             #------------------------------------------------------------------------------
364             # generic
365              
366             # convert number like "99-1" with dash fraction to decimals like "99.125"
367             # single dash digit is 1/8s
368             # three dash digits -xxy is xx 1/32s and y is 0,2,5,7 for further 1/4, 2/4,
369             # or 3/4 of 1/32
370             #
371             my %qu_to_quarter = (''=>0, 0=>0, 2=>1, 5=>2, 7=>3);
372             sub _dash_frac_to_decimals {
373 0     0   0 my ($str) = @_;
374              
375 0 0       0 $str =~ /^\+?(.+)-(.*)/ or return $str;
376 0         0 my $int = $1;
377 0         0 my $frac = $2;
378              
379 0 0 0     0 if (length ($frac) == 1) {
    0          
380             # 99-1
381             # only 2 decimals for 1/4s, since for various commodities that's the
382             # minimum tick
383 0         0 return $int + ($frac / 8);
384              
385             } elsif (length ($frac) == 2 || length ($frac) == 3) {
386             # 109-30, in 1/32nds
387             # 99-130, in 1/32s then last dig 0,2,5,7 further 1/4s of that
388 0         0 my $th = substr $frac, 0, 2;
389 0 0       0 if ($th > 31) {
390 0         0 die "Barchart: dash thirtyseconds out of range: $str";
391             }
392 0         0 my $qu = substr($frac, 2, 1);
393 0 0       0 if (! exists $qu_to_quarter{$qu}) {
394 0         0 die "Barchart: dash thirtyseconds further quarters unrecognised: $str";
395             }
396 0         0 $qu = $qu_to_quarter{$qu};
397 0         0 return $int + (($th + $qu / 4) / 32);
398              
399             } else {
400 0         0 die "Barchart: unrecognised dash number: $str";
401             }
402             }
403              
404             #------------------------------------------------------------------------------
405             # javascript mangling
406              
407             # $str contains javascript style calls
408             # document.write('foo')
409             # return a string of the output produced by those calls
410             # this only works for constant strings
411             # escaped quotes \' are turned into just ' in the return
412             #
413             sub _javascript_document_write {
414 1     1   324 my ($str) = @_;
415 1         2 my $ret = '';
416 1         12 while ($str =~ /document\.write\('((\\.|[^\'])*)'\)/sg) {
417 1         3 $ret .= _javascript_string_unquote($1);
418             }
419 1         4 return $ret;
420             }
421              
422             # undo javascript string backslash quoting in STR, per
423             #
424             # https://developer.mozilla.org/en/JavaScript/Guide/Values,_Variables,_and_Literals#String_Literals
425             #
426             # Encode::JavaScript::UCS does \u, but not the rest
427             #
428             # cf Java as such not quite the same:
429             # unicode: http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#100850
430             # strings: http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#101089
431             #
432             my %javascript_backslash = ('b' => "\b", # backspace
433             'f' => "\f", # formfeed
434             'n' => "\n", # newline
435             'r' => "\r",
436             't' => "\t", # tab
437             'v' => "\013", # vertical tab
438             );
439             sub _javascript_string_unquote {
440 3     3   128 my ($str) = @_;
441 3         16 $str =~ s{\\(?:
442             ((?:[0-3]?[0-7])?[0-7]) # $1 \377 octal latin-1
443             |x([0-9a-fA-F]{2}) # $2 \xFF hex latin-1
444             |u([0-9a-fA-F]{4}) # $3 \uFFFF hex unicode
445             |(.) # $4 \n etc escapes
446             )
447             }{
448             (defined $1 ? chr(oct($1))
449 8 100 66     73 : defined $4 ? ($javascript_backslash{$4} || $4)
    100 33        
450             : chr(hex($2||$3))) # \x,\u hex
451             }egx;
452 3         11 return $str;
453             }
454              
455             1;
456             __END__
457              
458             =for stopwords MGEX Ryde
459              
460             =head1 NAME
461              
462             Finance::Quote::MGEX - download Minneapolis Grain Exchange quotes
463              
464             =for Finance_Quote_Grab symbols MWZ19
465              
466             =head1 SYNOPSIS
467              
468             use Finance::Quote;
469             my $fq = Finance::Quote->new ('MGEX');
470             my %quotes = $fq->fetch('mgex', 'MWZ19');
471              
472             =head1 DESCRIPTION
473              
474             This module downloads commodity futures quotes from the Minneapolis Grain
475             Exchange (MGEX),
476              
477             =over
478              
479             L<http://www.mgex.com>
480              
481             =back
482              
483             Using the futures page
484              
485             =over
486              
487             L<http://www.mgex.com/data_charts.html>
488              
489             =back
490              
491             which is
492              
493             =over
494              
495             L<http://sites.barchart.com/pl/mgex/aquotes.htx>
496              
497             L<http://sites.barchart.com/pl/mgex/wquotes_js.js>
498              
499             =back
500              
501             =head2 Symbols
502              
503             The available symbols are for example
504              
505             =for Finance_Quote_Grab symbols MWZ19 KEZ19 ZWZ19 ICH19 IHH19 IPH19 ISH19 IWH19
506              
507             MWZ19 Minneapolis wheat
508             KEZ19 Kansas wheat
509             ZWZ19 CBOT wheat
510              
511             ICH19 national corn index
512             IHH19 hard red winter wheat index
513             IPH19 hard red spring wheat index
514             ISH19 national soybean index
515             IWH19 soft red spring wheat index
516              
517             The "Z19" etc is the contract month letter and the year "19" for 2019. The
518             month letters are the usual U.S. futures style
519              
520             F January
521             G February
522             H March
523             J April
524             K May
525             M June
526             N July
527             Q August
528             U September
529             V October
530             X November
531             Z December
532              
533             =head2 Fields
534              
535             The following standard C<Finance::Quote> fields are returned
536              
537             =for Finance_Quote_Grab fields flowed standard
538              
539             name currency
540             bid ask
541             open high low last close net
542             method source success errormsg
543              
544             Plus the following extras
545              
546             =for Finance_Quote_Grab fields table extra
547              
548             time ISO string "HH:MM"
549             contract_month_iso ISO format YYYY-MM-DD contract month
550              
551             Prices on the web pages are in eighths but are always returned here as
552             decimals so they can be used arithmetically. For instance "195-2" meaning
553             S<195 + 2/8> becomes "195.25".
554              
555             =head1 SEE ALSO
556              
557             L<Finance::Quote>, L<LWP>
558              
559             MGEX web site L<http://www.mgex.com>
560              
561             =head1 HOME PAGE
562              
563             L<http://user42.tuxfamily.org/finance-quote-grab/index.html>
564              
565             =head1 LICENCE
566              
567             Copyright 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016, 2019 Kevin Ryde
568              
569             Finance-Quote-Grab is free software; you can redistribute it and/or modify
570             it under the terms of the GNU General Public License as published by the
571             Free Software Foundation; either version 3, or (at your option) any later
572             version.
573              
574             Finance-Quote-Grab is distributed in the hope that it will be useful, but
575             WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
576             or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
577             more details.
578              
579             You should have received a copy of the GNU General Public License along with
580             Finance-Quote-Grab; see the file F<COPYING>. If not, see
581             L<http://www.gnu.org/licenses/>
582              
583             =cut