| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# London Metal Exchange (LME) setups. |
|
2
|
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# Copyright 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2016 Kevin Ryde |
|
4
|
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
# This file is part of Chart. |
|
6
|
|
|
|
|
|
|
# |
|
7
|
|
|
|
|
|
|
# Chart is free software; you can redistribute it and/or modify it under the |
|
8
|
|
|
|
|
|
|
# terms of the GNU General Public License as published by the Free Software |
|
9
|
|
|
|
|
|
|
# Foundation; either version 3, or (at your option) any later version. |
|
10
|
|
|
|
|
|
|
# |
|
11
|
|
|
|
|
|
|
# Chart is distributed in the hope that it will be useful, but WITHOUT ANY |
|
12
|
|
|
|
|
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13
|
|
|
|
|
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
|
14
|
|
|
|
|
|
|
# details. |
|
15
|
|
|
|
|
|
|
# |
|
16
|
|
|
|
|
|
|
# You should have received a copy of the GNU General Public License along |
|
17
|
|
|
|
|
|
|
# with Chart. If not, see <http://www.gnu.org/licenses/>. |
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
package App::Chart::Suffix::LME; |
|
20
|
1
|
|
|
1
|
|
446
|
use 5.010; |
|
|
1
|
|
|
|
|
3
|
|
|
21
|
1
|
|
|
1
|
|
5
|
use strict; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
17
|
|
|
22
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
22
|
|
|
23
|
1
|
|
|
1
|
|
3
|
use Carp; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
46
|
|
|
24
|
1
|
|
|
1
|
|
213
|
use Date::Calc; |
|
|
1
|
|
|
|
|
4328
|
|
|
|
1
|
|
|
|
|
35
|
|
|
25
|
1
|
|
|
1
|
|
264
|
use Date::Parse; |
|
|
1
|
|
|
|
|
5517
|
|
|
|
1
|
|
|
|
|
121
|
|
|
26
|
1
|
|
|
1
|
|
473
|
use File::Temp; |
|
|
1
|
|
|
|
|
11676
|
|
|
|
1
|
|
|
|
|
73
|
|
|
27
|
1
|
|
|
1
|
|
7
|
use File::Basename; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
55
|
|
|
28
|
1
|
|
|
1
|
|
359
|
use HTML::Form; |
|
|
1
|
|
|
|
|
19647
|
|
|
|
1
|
|
|
|
|
39
|
|
|
29
|
1
|
|
|
1
|
|
11
|
use List::Util; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
69
|
|
|
30
|
1
|
|
|
1
|
|
439
|
use File::Slurp; |
|
|
1
|
|
|
|
|
9309
|
|
|
|
1
|
|
|
|
|
64
|
|
|
31
|
1
|
|
|
1
|
|
10
|
use URI::Escape; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
51
|
|
|
32
|
1
|
|
|
1
|
|
427
|
use Locale::TextDomain ('App-Chart'); |
|
|
1
|
|
|
|
|
5662
|
|
|
|
1
|
|
|
|
|
6
|
|
|
33
|
|
|
|
|
|
|
|
|
34
|
1
|
|
|
1
|
|
5550
|
use App::Chart; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
use App::Chart::Database; |
|
36
|
|
|
|
|
|
|
use App::Chart::Download; |
|
37
|
|
|
|
|
|
|
use App::Chart::DownloadHandler; |
|
38
|
|
|
|
|
|
|
use App::Chart::Sympred; |
|
39
|
|
|
|
|
|
|
use App::Chart::Timebase::Months; |
|
40
|
|
|
|
|
|
|
use App::Chart::TZ; |
|
41
|
|
|
|
|
|
|
use App::Chart::Weblink; |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
use constant DEBUG => 0; |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
# As of July 2007, in https requests to secure.lme.com for the daily metals |
|
47
|
|
|
|
|
|
|
# prices it seems essential to use http/1.1 persistent connections. If |
|
48
|
|
|
|
|
|
|
# "Connection: close" is requested by the client something fishy happens and |
|
49
|
|
|
|
|
|
|
# the connection hangs at about byte 48887 out of about 62110 (waiting for |
|
50
|
|
|
|
|
|
|
# the last 16kbyte tls packet). This is with either gnutls or openssl and a |
|
51
|
|
|
|
|
|
|
# trace with gnutls shows it just stops sending, though the TCP connection |
|
52
|
|
|
|
|
|
|
# remains up. Either the default http/1.1 persistence (no Connection header |
|
53
|
|
|
|
|
|
|
# at all) or the compatibility "Connection: keep-alive" style seems to make |
|
54
|
|
|
|
|
|
|
# it better. Presumably it's something buggy in the server (Microsoft-IIS |
|
55
|
|
|
|
|
|
|
# 6.0). |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
my $pred = App::Chart::Sympred::Suffix->new ('.LME'); |
|
58
|
|
|
|
|
|
|
App::Chart::TZ->london->setup_for_symbol ($pred); |
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
# App::Chart::setup_source_help |
|
61
|
|
|
|
|
|
|
# ($pred, __p('manual-node','London Metal Exchange')); |
|
62
|
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
my %polypropylene_hash = ('PP'=>1,'PA'=>1,'PE'=>1,'PN'=>1); |
|
65
|
|
|
|
|
|
|
my %linearlow_hash = ('LP'=>1,'LA'=>1,'LE'=>1,'LN'=>1); |
|
66
|
|
|
|
|
|
|
my %steel_hash = ('FM'=>1,'FF'=>1); |
|
67
|
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
sub type { |
|
69
|
|
|
|
|
|
|
my ($symbol) = @_; |
|
70
|
|
|
|
|
|
|
my $commodity = App::Chart::symbol_commodity ($symbol); |
|
71
|
|
|
|
|
|
|
if ($polypropylene_hash{$commodity} || $linearlow_hash{$commodity}) { |
|
72
|
|
|
|
|
|
|
return 'plastics'; |
|
73
|
|
|
|
|
|
|
} |
|
74
|
|
|
|
|
|
|
if ($steel_hash{$commodity}) { |
|
75
|
|
|
|
|
|
|
return 'steels'; |
|
76
|
|
|
|
|
|
|
} |
|
77
|
|
|
|
|
|
|
return 'metals'; |
|
78
|
|
|
|
|
|
|
} |
|
79
|
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
81
|
|
|
|
|
|
|
# weblink - commodity pages |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
App::Chart::Weblink->new |
|
84
|
|
|
|
|
|
|
(pred => $pred, |
|
85
|
|
|
|
|
|
|
name => __('LME _Commodity Page'), |
|
86
|
|
|
|
|
|
|
desc => __('Open web browser at the London Metal Exchange page for this commodity'), |
|
87
|
|
|
|
|
|
|
proc => sub { |
|
88
|
|
|
|
|
|
|
my ($symbol) = @_; |
|
89
|
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
if ($symbol =~ /^AA/) { return 'http://www.lme.co.uk/aluminiumalloy.asp' } |
|
91
|
|
|
|
|
|
|
if ($symbol =~ /^AH/) { return 'http://www.lme.co.uk/aluminium.asp' } |
|
92
|
|
|
|
|
|
|
if ($symbol =~ /^CA/) { return 'http://www.lme.co.uk/copper.asp' } |
|
93
|
|
|
|
|
|
|
if ($symbol =~ /^NA/) { return 'http://www.lme.co.uk/nasaac.asp' } |
|
94
|
|
|
|
|
|
|
if ($symbol =~ /^NI/) { return 'http://www.lme.co.uk/nickel.asp' } |
|
95
|
|
|
|
|
|
|
if ($symbol =~ /^PB/) { return 'http://www.lme.co.uk/lead.asp' } |
|
96
|
|
|
|
|
|
|
if ($symbol =~ /^SN/) { return 'http://www.lme.co.uk/tin.asp' } |
|
97
|
|
|
|
|
|
|
if ($symbol =~ /^ZS/) { return 'http://www.lme.co.uk/zinc.asp' } |
|
98
|
|
|
|
|
|
|
if ($symbol =~ /^F/) { return 'http://www.lme.co.uk/steel.asp' } |
|
99
|
|
|
|
|
|
|
if ($symbol =~ /^P/) { return 'http://www.lme.co.uk/plastics.asp' } |
|
100
|
|
|
|
|
|
|
if ($symbol =~ /^L/) { return 'http://www.lme.co.uk/plastics.asp' } |
|
101
|
|
|
|
|
|
|
return undef; |
|
102
|
|
|
|
|
|
|
}); |
|
103
|
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
106
|
|
|
|
|
|
|
# HTTP::Cookies extras |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
# $jar is a HTTP::Cookies object, read $str into it with $jar->load (which |
|
109
|
|
|
|
|
|
|
# would normally read from a file) |
|
110
|
|
|
|
|
|
|
# |
|
111
|
|
|
|
|
|
|
sub http_cookies_set_string { |
|
112
|
|
|
|
|
|
|
my ($jar, $str) = @_; |
|
113
|
|
|
|
|
|
|
my $fh = File::Temp->new (TEMPLATE => 'chart-cookie-jar-XXXXXX', |
|
114
|
|
|
|
|
|
|
TMPDIR => 1); |
|
115
|
|
|
|
|
|
|
if (DEBUG) { print "cookie set tempfile ",$fh->filename,"\n"; } |
|
116
|
|
|
|
|
|
|
print $fh $str; |
|
117
|
|
|
|
|
|
|
close $fh or die; |
|
118
|
|
|
|
|
|
|
$jar->load ($fh->filename); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
# $jar is a HTTP::Cookies object, return a string which is $jar->save output |
|
122
|
|
|
|
|
|
|
# (which would normally go to a file) |
|
123
|
|
|
|
|
|
|
# |
|
124
|
|
|
|
|
|
|
sub http_cookies_get_string { |
|
125
|
|
|
|
|
|
|
my ($jar) = @_; |
|
126
|
|
|
|
|
|
|
my $fh = File::Temp->new (TEMPLATE => 'chart-cookie-jar-XXXXXX', |
|
127
|
|
|
|
|
|
|
TMPDIR => 1); |
|
128
|
|
|
|
|
|
|
if (DEBUG) { print "cookie get $fh tempfile ",$fh->filename,"\n"; } |
|
129
|
|
|
|
|
|
|
$jar->save ($fh->filename); |
|
130
|
|
|
|
|
|
|
close $fh or die; |
|
131
|
|
|
|
|
|
|
# not certain if File::Temp 0.21 blessed handle is ok, use the filename |
|
132
|
|
|
|
|
|
|
return File::Slurp::slurp ($fh->filename); |
|
133
|
|
|
|
|
|
|
} |
|
134
|
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
137
|
|
|
|
|
|
|
# secure login |
|
138
|
|
|
|
|
|
|
# |
|
139
|
|
|
|
|
|
|
# This logs in at the data service page, |
|
140
|
|
|
|
|
|
|
# |
|
141
|
|
|
|
|
|
|
use constant LOGIN_URL => |
|
142
|
|
|
|
|
|
|
'https://secure.lme.com/Data/Community/Login.aspx?ReturnUrl=%2fData%2fcommunity%2findex.aspx'; |
|
143
|
|
|
|
|
|
|
# |
|
144
|
|
|
|
|
|
|
# The result is a cookie ".ASPXAUTH" recorded under "lme-cookie-jar" in the |
|
145
|
|
|
|
|
|
|
# database ready for subsequent use. An extra cookie with a dummy domain, |
|
146
|
|
|
|
|
|
|
# |
|
147
|
|
|
|
|
|
|
use constant LOGIN_DOMAIN => 'chart-lme-logged-in.local'; |
|
148
|
|
|
|
|
|
|
# |
|
149
|
|
|
|
|
|
|
# is used to note success. Not sure how long a login is supposed to last |
|
150
|
|
|
|
|
|
|
# (the server doesn't put an expiry on the cookie), but for now consider it |
|
151
|
|
|
|
|
|
|
# expired after an hour, |
|
152
|
|
|
|
|
|
|
# |
|
153
|
|
|
|
|
|
|
use constant LOGIN_EXPIRY_SECONDS => 3600; |
|
154
|
|
|
|
|
|
|
# |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
# create and return a new HTTP::Cookies which is the jar in the database |
|
157
|
|
|
|
|
|
|
sub login_read_jar { |
|
158
|
|
|
|
|
|
|
require HTTP::Cookies; |
|
159
|
|
|
|
|
|
|
my $jar = HTTP::Cookies->new; |
|
160
|
|
|
|
|
|
|
my $str = App::Chart::Database->read_extra ('', 'lme-cookie-jar'); |
|
161
|
|
|
|
|
|
|
if ($str) { http_cookies_set_string ($jar, $str); } |
|
162
|
|
|
|
|
|
|
return $jar; |
|
163
|
|
|
|
|
|
|
} |
|
164
|
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
# $jar is a HTTP::Cookies object, save it to the database |
|
166
|
|
|
|
|
|
|
sub login_write_jar { |
|
167
|
|
|
|
|
|
|
my ($jar) = @_; |
|
168
|
|
|
|
|
|
|
App::Chart::Database->write_extra ('', 'lme-cookie-jar', |
|
169
|
|
|
|
|
|
|
http_cookies_get_string ($jar)); |
|
170
|
|
|
|
|
|
|
} |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# return true if we're still logged in |
|
173
|
|
|
|
|
|
|
sub login_is_logged_in { |
|
174
|
|
|
|
|
|
|
my $jar = login_read_jar(); |
|
175
|
|
|
|
|
|
|
my $login_timestamp = jar_get_login_timestamp ($jar); |
|
176
|
|
|
|
|
|
|
return App::Chart::Download::timestamp_within ($login_timestamp, |
|
177
|
|
|
|
|
|
|
LOGIN_EXPIRY_SECONDS); |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
sub login_ensure { |
|
181
|
|
|
|
|
|
|
if (login_is_logged_in()) { return; } |
|
182
|
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
App::Chart::Download::status (__('LME login')); |
|
184
|
|
|
|
|
|
|
App::Chart::Database->write_extra ('', 'lme-cookie-jar', undef); |
|
185
|
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
my $username = App::Chart::Database->preference_get ('lme-username', undef); |
|
187
|
|
|
|
|
|
|
my $password = App::Chart::Database->preference_get ('lme-password', ''); |
|
188
|
|
|
|
|
|
|
if (! defined $username || $username eq '') { |
|
189
|
|
|
|
|
|
|
die 'No LME username set in preferences'; |
|
190
|
|
|
|
|
|
|
} |
|
191
|
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
require App::Chart::UserAgent; |
|
193
|
|
|
|
|
|
|
require HTTP::Cookies; |
|
194
|
|
|
|
|
|
|
my $ua = App::Chart::UserAgent->instance->clone; |
|
195
|
|
|
|
|
|
|
my $jar = HTTP::Cookies->new; |
|
196
|
|
|
|
|
|
|
$ua->cookie_jar ($jar); |
|
197
|
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
my $login_url = LOGIN_URL; |
|
199
|
|
|
|
|
|
|
$login_url = 'http://localhost/Login.aspx'; |
|
200
|
|
|
|
|
|
|
my $resp = App::Chart::Download->get ($login_url, ua => $ua); |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
my $content = $resp->decoded_content(raise_error=>1); |
|
203
|
|
|
|
|
|
|
my $form = HTML::Form->parse($content, $login_url) |
|
204
|
|
|
|
|
|
|
or die "LME login page not a form"; |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
# these are literal "$" in the field name |
|
207
|
|
|
|
|
|
|
$form->value ("_logIn\$_userID", $username); |
|
208
|
|
|
|
|
|
|
$form->value ("_logIn\$_password", $password); |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
my $req = $form->click(); |
|
211
|
|
|
|
|
|
|
$ua->requests_redirectable ([]); |
|
212
|
|
|
|
|
|
|
$resp = $ua->request ($req); |
|
213
|
|
|
|
|
|
|
# The POST is to the Login.aspx page and success is a redirect to the main |
|
214
|
|
|
|
|
|
|
# data page /Data/community/index.aspx. So failure is anything other than |
|
215
|
|
|
|
|
|
|
# 302, or no Location, or a Location but containing "Login". |
|
216
|
|
|
|
|
|
|
if ($resp->code != 302 |
|
217
|
|
|
|
|
|
|
|| ! $resp->header ('Location') |
|
218
|
|
|
|
|
|
|
|| $resp->header ('Location') =~ /Login/) { |
|
219
|
|
|
|
|
|
|
die "LME: login failed"; |
|
220
|
|
|
|
|
|
|
} |
|
221
|
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
jar_set_login_timestamp ($jar); |
|
223
|
|
|
|
|
|
|
login_write_jar ($jar); |
|
224
|
|
|
|
|
|
|
} |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
sub jar_get_login_timestamp { |
|
228
|
|
|
|
|
|
|
my ($jar) = @_; |
|
229
|
|
|
|
|
|
|
my $login_timestamp; |
|
230
|
|
|
|
|
|
|
$jar->scan(sub { |
|
231
|
|
|
|
|
|
|
my ($version, $key, $val, $path, $domain, $port, $path_spec, |
|
232
|
|
|
|
|
|
|
$secure, $expires, $discard, $hash) = @_; |
|
233
|
|
|
|
|
|
|
if ($domain eq LOGIN_DOMAIN && $key eq 'timestamp') { |
|
234
|
|
|
|
|
|
|
$login_timestamp = $val; |
|
235
|
|
|
|
|
|
|
} |
|
236
|
|
|
|
|
|
|
}); |
|
237
|
|
|
|
|
|
|
return $login_timestamp; |
|
238
|
|
|
|
|
|
|
} |
|
239
|
|
|
|
|
|
|
sub jar_set_login_timestamp { |
|
240
|
|
|
|
|
|
|
my ($jar) = @_; |
|
241
|
|
|
|
|
|
|
$jar->set_cookie (0, # version |
|
242
|
|
|
|
|
|
|
'timestamp', # key |
|
243
|
|
|
|
|
|
|
App::Chart::Download::timestamp_now(), # value |
|
244
|
|
|
|
|
|
|
'/', # path |
|
245
|
|
|
|
|
|
|
LOGIN_DOMAIN, # domain |
|
246
|
|
|
|
|
|
|
0, # port |
|
247
|
|
|
|
|
|
|
0, # path_spec |
|
248
|
|
|
|
|
|
|
0, # secure |
|
249
|
|
|
|
|
|
|
LOGIN_EXPIRY_SECONDS, # maxage |
|
250
|
|
|
|
|
|
|
0); # discard |
|
251
|
|
|
|
|
|
|
} |
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
255
|
|
|
|
|
|
|
# Daily data |
|
256
|
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
# return tdate for available daily report |
|
258
|
|
|
|
|
|
|
# |
|
259
|
|
|
|
|
|
|
sub daily_available_date { |
|
260
|
|
|
|
|
|
|
my ($symbol) = @_; |
|
261
|
|
|
|
|
|
|
my $type = type($symbol); |
|
262
|
|
|
|
|
|
|
if ($type eq 'metals') { |
|
263
|
|
|
|
|
|
|
# http://www.lme.co.uk/who_how_ringtimes.asp |
|
264
|
|
|
|
|
|
|
# Prices after second ring session each trading day, which would be |
|
265
|
|
|
|
|
|
|
# 16:15 maybe, try at 16:30. |
|
266
|
|
|
|
|
|
|
return App::Chart::Download::weekday_date_after_time |
|
267
|
|
|
|
|
|
|
(16,30, App::Chart::TZ->london, -1); |
|
268
|
|
|
|
|
|
|
} |
|
269
|
|
|
|
|
|
|
if ($type eq 'plastics') { |
|
270
|
|
|
|
|
|
|
# https://secure.lme.com/Data/community/Dataprices_daily_prices_plastics.aspx |
|
271
|
|
|
|
|
|
|
# per prices page, available at 2am the following day |
|
272
|
|
|
|
|
|
|
return App::Chart::Download::weekday_date_after_time |
|
273
|
|
|
|
|
|
|
(2,0, App::Chart::TZ->london, -1); |
|
274
|
|
|
|
|
|
|
} |
|
275
|
|
|
|
|
|
|
if ($type eq 'steels') { |
|
276
|
|
|
|
|
|
|
# per prices page, available at 2am the following day |
|
277
|
|
|
|
|
|
|
return App::Chart::Download::weekday_date_after_time |
|
278
|
|
|
|
|
|
|
(2,0, App::Chart::TZ->london, -1); |
|
279
|
|
|
|
|
|
|
} |
|
280
|
|
|
|
|
|
|
die; |
|
281
|
|
|
|
|
|
|
} |
|
282
|
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
286
|
|
|
|
|
|
|
# Daily price page parsing |
|
287
|
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
sub daily_parse { |
|
289
|
|
|
|
|
|
|
my ($resp, $want_tdate) = @_; |
|
290
|
|
|
|
|
|
|
my @data = (); |
|
291
|
|
|
|
|
|
|
my $h = { source => __PACKAGE__, |
|
292
|
|
|
|
|
|
|
currency => 'USD', |
|
293
|
|
|
|
|
|
|
data => \@data }; |
|
294
|
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
my $content = $resp->decoded_content (raise_error => 1); |
|
296
|
|
|
|
|
|
|
$content = mung_1x1_tables ($content); |
|
297
|
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
# Eg. "Official Prices, US$ per tonne for\n\t\t19 September 2008" |
|
299
|
|
|
|
|
|
|
# Eg. "LME Official Prices, US$ per tonne for 18 September 2008" |
|
300
|
|
|
|
|
|
|
# |
|
301
|
|
|
|
|
|
|
$content =~ /Prices.*?for\s*\n?\s*([0-9]{1,2}\s+[A-Za-z]+\s+[0-9][0-9][0-9][0-9])/i |
|
302
|
|
|
|
|
|
|
or die "LME daily: date not found"; |
|
303
|
|
|
|
|
|
|
my $date = App::Chart::Download::Decode_Date_EU_to_iso ($1); |
|
304
|
|
|
|
|
|
|
if (defined $want_tdate) { |
|
305
|
|
|
|
|
|
|
my $want_date = App::Chart::tdate_to_iso($want_tdate); |
|
306
|
|
|
|
|
|
|
if ($date ne $want_tdate) { |
|
307
|
|
|
|
|
|
|
die "LME daily: didn't get expected date, got $date want $want_tdate"; |
|
308
|
|
|
|
|
|
|
} |
|
309
|
|
|
|
|
|
|
} |
|
310
|
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
require HTML::TableExtract; |
|
312
|
|
|
|
|
|
|
my $te = HTML::TableExtract->new (headers => [qr/PP.*Global/is], |
|
313
|
|
|
|
|
|
|
keep_headers => 1, |
|
314
|
|
|
|
|
|
|
slice_columns => 0); |
|
315
|
|
|
|
|
|
|
$te->parse($content); |
|
316
|
|
|
|
|
|
|
my $ts = $te->first_table_found(); |
|
317
|
|
|
|
|
|
|
if (! $ts) { |
|
318
|
|
|
|
|
|
|
$te = HTML::TableExtract->new (headers => [qr/COPPER|STEEL/i], |
|
319
|
|
|
|
|
|
|
keep_headers => 1, |
|
320
|
|
|
|
|
|
|
slice_columns => 0); |
|
321
|
|
|
|
|
|
|
$te->parse($content); |
|
322
|
|
|
|
|
|
|
$ts = $te->first_table_found() |
|
323
|
|
|
|
|
|
|
|| die "LME daily: prices table not found"; |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
my $rows = $ts->rows(); |
|
327
|
|
|
|
|
|
|
my $lastrow = $#$rows; |
|
328
|
|
|
|
|
|
|
my $lastcol = $#{$rows->[0]}; |
|
329
|
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
my @column; |
|
331
|
|
|
|
|
|
|
my @column_commodity; |
|
332
|
|
|
|
|
|
|
my @column_name; |
|
333
|
|
|
|
|
|
|
foreach my $c (2 .. $lastcol) { |
|
334
|
|
|
|
|
|
|
my $commodity = $rows->[0]->[$c] || next; |
|
335
|
|
|
|
|
|
|
my $name; |
|
336
|
|
|
|
|
|
|
if ($commodity =~ /ALUMINIUM ALLOY/i) { $commodity = 'AA'; } |
|
337
|
|
|
|
|
|
|
elsif ($commodity =~ /ALUMINIUM/i) { $commodity = 'AH'; } |
|
338
|
|
|
|
|
|
|
elsif ($commodity =~ /COPPER/i) { $commodity = 'CA'; } |
|
339
|
|
|
|
|
|
|
elsif ($commodity =~ /LEAD/i) { $commodity = 'PB'; } |
|
340
|
|
|
|
|
|
|
elsif ($commodity =~ /NICKEL/i) { $commodity = 'NI'; } |
|
341
|
|
|
|
|
|
|
elsif ($commodity =~ /TIN/i) { $commodity = 'SN'; } |
|
342
|
|
|
|
|
|
|
elsif ($commodity =~ /ZINC/i) { $commodity = 'ZS'; } |
|
343
|
|
|
|
|
|
|
elsif ($commodity =~ /NASAAC/i) { $commodity = 'NI'; } |
|
344
|
|
|
|
|
|
|
elsif ($commodity =~ /STEEL.*MEDITERRANEAN/s) { $commodity = 'FM'; } |
|
345
|
|
|
|
|
|
|
elsif ($commodity =~ /STEEL.*FAR EAST/s) { $commodity = 'FF'; } |
|
346
|
|
|
|
|
|
|
elsif ($commodity =~ /^([A-Z][A-Z])\s+(.*)/is) { $commodity = $1; $name = $2; } |
|
347
|
|
|
|
|
|
|
else { next; } |
|
348
|
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
push @column, $c; |
|
350
|
|
|
|
|
|
|
push @column_commodity, $commodity; |
|
351
|
|
|
|
|
|
|
push @column_name, $name; |
|
352
|
|
|
|
|
|
|
} |
|
353
|
|
|
|
|
|
|
if (DEBUG) { require Data::Dumper; |
|
354
|
|
|
|
|
|
|
print "columns ", Data::Dumper::Dumper(\@column); |
|
355
|
|
|
|
|
|
|
print "columns ", Data::Dumper::Dumper(\@column_commodity); |
|
356
|
|
|
|
|
|
|
print "columns ", Data::Dumper::Dumper(\@column_name); } |
|
357
|
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
my %bid; |
|
359
|
|
|
|
|
|
|
foreach my $r (1 .. $lastrow) { |
|
360
|
|
|
|
|
|
|
my $row = $rows->[$r]; |
|
361
|
|
|
|
|
|
|
if (DEBUG) { require Data::Dumper; |
|
362
|
|
|
|
|
|
|
print Data::Dumper::Dumper($row); } |
|
363
|
|
|
|
|
|
|
my $type = $row->[1]; |
|
364
|
|
|
|
|
|
|
if (! $type) { next; } |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
my $side; |
|
367
|
|
|
|
|
|
|
if ($type =~ /^\s*$/is) { |
|
368
|
|
|
|
|
|
|
next; # empty |
|
369
|
|
|
|
|
|
|
} elsif ($type =~ /buyer/i) { |
|
370
|
|
|
|
|
|
|
$side = 'bid'; |
|
371
|
|
|
|
|
|
|
} elsif ($type =~ /seller/i) { |
|
372
|
|
|
|
|
|
|
$side = 'offer'; |
|
373
|
|
|
|
|
|
|
} else { |
|
374
|
|
|
|
|
|
|
die "LME daily: unrecognised row type '$type'\n"; |
|
375
|
|
|
|
|
|
|
} |
|
376
|
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
my $month; |
|
378
|
|
|
|
|
|
|
my $post; |
|
379
|
|
|
|
|
|
|
if (DEBUG) { print "type $type\n"; } |
|
380
|
|
|
|
|
|
|
if ($type =~ /cash/i) { |
|
381
|
|
|
|
|
|
|
$post = ''; |
|
382
|
|
|
|
|
|
|
} elsif ($type =~ /([0-9]+)[- \t]*month/s) { |
|
383
|
|
|
|
|
|
|
$post = $1; |
|
384
|
|
|
|
|
|
|
} elsif ($type =~ /^(.*?)\s+(buyer|seller)/i) { |
|
385
|
|
|
|
|
|
|
$month = month_str_to_nearest_iso ($1); |
|
386
|
|
|
|
|
|
|
$post = " " . App::Chart::Download::iso_to_MMM_YY($month); |
|
387
|
|
|
|
|
|
|
} else { |
|
388
|
|
|
|
|
|
|
die "LME daily: unrecognised row type '$type'\n"; |
|
389
|
|
|
|
|
|
|
} |
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
foreach my $i (0 .. $#column) { |
|
392
|
|
|
|
|
|
|
my $c = $column[$i]; |
|
393
|
|
|
|
|
|
|
my $commodity = $column_commodity[$i]; |
|
394
|
|
|
|
|
|
|
my $price = $row->[$c]; |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
if ($side eq 'bid') { |
|
397
|
|
|
|
|
|
|
$bid{$commodity} = $price; |
|
398
|
|
|
|
|
|
|
next; |
|
399
|
|
|
|
|
|
|
} |
|
400
|
|
|
|
|
|
|
push @data, { symbol => "$commodity$post.LME", |
|
401
|
|
|
|
|
|
|
month => $month, |
|
402
|
|
|
|
|
|
|
name => $column_name[$i], |
|
403
|
|
|
|
|
|
|
date => $date, |
|
404
|
|
|
|
|
|
|
bid => delete $bid{$commodity}, |
|
405
|
|
|
|
|
|
|
offer => $price, |
|
406
|
|
|
|
|
|
|
close => $price, |
|
407
|
|
|
|
|
|
|
}; |
|
408
|
|
|
|
|
|
|
} |
|
409
|
|
|
|
|
|
|
} |
|
410
|
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
return $h; |
|
412
|
|
|
|
|
|
|
} |
|
413
|
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
# $str is some html (in wide chars) |
|
415
|
|
|
|
|
|
|
# flatten out any little 1x1 tables to their contents |
|
416
|
|
|
|
|
|
|
# such tables are found in the rows of the daily plastics page |
|
417
|
|
|
|
|
|
|
# |
|
418
|
|
|
|
|
|
|
sub mung_1x1_tables { |
|
419
|
|
|
|
|
|
|
my ($str) = @_; |
|
420
|
|
|
|
|
|
|
require HTML::TreeBuilder; |
|
421
|
|
|
|
|
|
|
my $top = HTML::TreeBuilder->new_from_content ($str); |
|
422
|
|
|
|
|
|
|
my $changed = 0; |
|
423
|
|
|
|
|
|
|
$top->traverse |
|
424
|
|
|
|
|
|
|
([sub { |
|
425
|
|
|
|
|
|
|
my ($elem) = @_; |
|
426
|
|
|
|
|
|
|
if ($elem->tag ne 'table') { return 1; } |
|
427
|
|
|
|
|
|
|
my $table = $elem; |
|
428
|
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
# possible tbody within |
|
430
|
|
|
|
|
|
|
my $tbody = List::Util::first {ref $_ && $_->tag eq 'tbody'} |
|
431
|
|
|
|
|
|
|
$table->content_list; |
|
432
|
|
|
|
|
|
|
if (! $tbody) { $tbody = $table; } |
|
433
|
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
my @rows = grep {ref $_ && $_->tag eq 'tr'} $tbody->content_list; |
|
435
|
|
|
|
|
|
|
if (@rows != 1) { return 1; } |
|
436
|
|
|
|
|
|
|
my $row = $rows[0]; |
|
437
|
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
my @cols = grep {ref $_ && $_->tag eq 'td' |
|
439
|
|
|
|
|
|
|
&& ! html_element_contains_only_img($_) } |
|
440
|
|
|
|
|
|
|
$row->content_list; |
|
441
|
|
|
|
|
|
|
if (@cols != 1) { return 1; } |
|
442
|
|
|
|
|
|
|
my $col = $cols[0]; |
|
443
|
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
$table->replace_with ($col->content_list); |
|
445
|
|
|
|
|
|
|
$changed = 1; |
|
446
|
|
|
|
|
|
|
return 0; # prune |
|
447
|
|
|
|
|
|
|
} |
|
448
|
|
|
|
|
|
|
], |
|
449
|
|
|
|
|
|
|
1); # pre-order, no text |
|
450
|
|
|
|
|
|
|
if (DEBUG) { print "mung_1x1 changed $changed\n"; } |
|
451
|
|
|
|
|
|
|
if ($changed) { |
|
452
|
|
|
|
|
|
|
return $top->as_HTML; |
|
453
|
|
|
|
|
|
|
} else { |
|
454
|
|
|
|
|
|
|
return $str; |
|
455
|
|
|
|
|
|
|
} |
|
456
|
|
|
|
|
|
|
} |
|
457
|
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
sub html_element_contains_only_img { |
|
459
|
|
|
|
|
|
|
my ($elem) = @_; |
|
460
|
|
|
|
|
|
|
my @list = $elem->content_list; |
|
461
|
|
|
|
|
|
|
return (@list == 1 |
|
462
|
|
|
|
|
|
|
&& ref $list[0] |
|
463
|
|
|
|
|
|
|
&& $list[0]->tag eq 'img'); |
|
464
|
|
|
|
|
|
|
} |
|
465
|
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
sub month_str_to_nearest_iso { |
|
467
|
|
|
|
|
|
|
my ($str) = @_; |
|
468
|
|
|
|
|
|
|
my $month = Date::Calc::Decode_Month ($str) |
|
469
|
|
|
|
|
|
|
|| die "LME parse: unrecognised month: '$str'"; |
|
470
|
|
|
|
|
|
|
my $year = App::Chart::Download::month_to_nearest_year ($month); |
|
471
|
|
|
|
|
|
|
return App::Chart::ymd_to_iso ($year, $month, 1); |
|
472
|
|
|
|
|
|
|
} |
|
473
|
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
476
|
|
|
|
|
|
|
# historical download page |
|
477
|
|
|
|
|
|
|
# |
|
478
|
|
|
|
|
|
|
# This uses the historical data at |
|
479
|
|
|
|
|
|
|
# |
|
480
|
|
|
|
|
|
|
use constant HISTORICAL_XLS_URL => |
|
481
|
|
|
|
|
|
|
'http://www.lme.co.uk/dataprices_historical.asp'; |
|
482
|
|
|
|
|
|
|
# |
|
483
|
|
|
|
|
|
|
# That page is downloaded to get urls of XLS files for prices and volumes |
|
484
|
|
|
|
|
|
|
# for each calendar month. A price file is like |
|
485
|
|
|
|
|
|
|
# |
|
486
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/January_2007.xls |
|
487
|
|
|
|
|
|
|
# |
|
488
|
|
|
|
|
|
|
# and a volumes file |
|
489
|
|
|
|
|
|
|
# |
|
490
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/volumes_September_2007.xls |
|
491
|
|
|
|
|
|
|
# |
|
492
|
|
|
|
|
|
|
# Sometimes there's a rev num like |
|
493
|
|
|
|
|
|
|
# |
|
494
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/historic_data/May_2008(1).xls |
|
495
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/historic_data/December_2008_3.xls |
|
496
|
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
sub historical_xls_files { |
|
498
|
|
|
|
|
|
|
require App::Chart::Pagebits; |
|
499
|
|
|
|
|
|
|
my $h = App::Chart::Pagebits::get |
|
500
|
|
|
|
|
|
|
(name => __('LME historical downloads page'), |
|
501
|
|
|
|
|
|
|
url => HISTORICAL_XLS_URL, |
|
502
|
|
|
|
|
|
|
method => 'POST', |
|
503
|
|
|
|
|
|
|
data => 'disclaimer=agreed', |
|
504
|
|
|
|
|
|
|
key => 'lme-historical-xls', |
|
505
|
|
|
|
|
|
|
freq_days => 2, |
|
506
|
|
|
|
|
|
|
timezone => App::Chart::TZ->london, |
|
507
|
|
|
|
|
|
|
parse => \&historical_xls_parse); |
|
508
|
|
|
|
|
|
|
my $aref = $h->{'files'} || []; |
|
509
|
|
|
|
|
|
|
return @$aref; |
|
510
|
|
|
|
|
|
|
} |
|
511
|
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
# $content is the "dataprices_historical.asp" page. |
|
513
|
|
|
|
|
|
|
# Return a hashref like { 'files' => [ {elem}, {elem}, ...] } |
|
514
|
|
|
|
|
|
|
# |
|
515
|
|
|
|
|
|
|
# At the start of the year there can be nothing available (the previous year |
|
516
|
|
|
|
|
|
|
# files being made chargable items) so it's possible for 'urls' to be empty. |
|
517
|
|
|
|
|
|
|
# |
|
518
|
|
|
|
|
|
|
# There's a size in the text following each link, but since there's no |
|
519
|
|
|
|
|
|
|
# overlapping files to choose between there's no need to pick that out. |
|
520
|
|
|
|
|
|
|
# |
|
521
|
|
|
|
|
|
|
sub historical_xls_parse { |
|
522
|
|
|
|
|
|
|
my ($content) = @_; |
|
523
|
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
my %urls; |
|
525
|
|
|
|
|
|
|
require HTML::LinkExtor; |
|
526
|
|
|
|
|
|
|
my $p = HTML::LinkExtor->new |
|
527
|
|
|
|
|
|
|
(sub { |
|
528
|
|
|
|
|
|
|
my($tag, %links) = @_; |
|
529
|
|
|
|
|
|
|
$tag eq 'a' or return; |
|
530
|
|
|
|
|
|
|
my $link = $links{'href'} or return; |
|
531
|
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
# only the .xls files |
|
533
|
|
|
|
|
|
|
$link =~ /\.xls$/i or return; |
|
534
|
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
# exclude warehouse stocks |
|
536
|
|
|
|
|
|
|
if ($link =~ /stocks/i) { return; } |
|
537
|
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
$urls{$link} = 1; |
|
539
|
|
|
|
|
|
|
}, HISTORICAL_XLS_URL); |
|
540
|
|
|
|
|
|
|
$p->parse($content); |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
my @files; |
|
543
|
|
|
|
|
|
|
foreach my $url (keys %urls) { |
|
544
|
|
|
|
|
|
|
if (DEBUG) { print "url $url\n"; } |
|
545
|
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
$url =~ m{([^/]+)$} or die; # only a plain file |
|
547
|
|
|
|
|
|
|
my $basename = $1; |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
# rev num in parens like "May_2008(1).xls" |
|
550
|
|
|
|
|
|
|
$basename =~ s/%28.*%29//; |
|
551
|
|
|
|
|
|
|
$basename =~ s/\(.*\)//; |
|
552
|
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
# rev num with underscore like "December_2008_3.xls" |
|
554
|
|
|
|
|
|
|
$basename =~ s/(\d\d\d\d)_\d+(\.)/$1$2/; |
|
555
|
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
$basename =~ s/volumes//i; |
|
557
|
|
|
|
|
|
|
my $month = App::Chart::Download::Decode_Date_EU_to_iso ("1 $basename"); |
|
558
|
|
|
|
|
|
|
push @files, { url => $url, |
|
559
|
|
|
|
|
|
|
month_iso => $month }; |
|
560
|
|
|
|
|
|
|
} |
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
@files = sort {$a->{'month_iso'} cmp $b->{'month_iso'} |
|
563
|
|
|
|
|
|
|
|| $a->{'url'} cmp $b->{'url'} |
|
564
|
|
|
|
|
|
|
} @files; |
|
565
|
|
|
|
|
|
|
return { 'files' => \@files }; |
|
566
|
|
|
|
|
|
|
} |
|
567
|
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
# return mdate for STR like "January_2007" or "Jan_07", or #f if not that |
|
569
|
|
|
|
|
|
|
# format |
|
570
|
|
|
|
|
|
|
# sub Mmm_yyy_str_to_mdate { |
|
571
|
|
|
|
|
|
|
# my ($str) = @_; |
|
572
|
|
|
|
|
|
|
# # drop "(1)" part of "http://www.lme.co.uk/downloads/March_2008(1).xls" |
|
573
|
|
|
|
|
|
|
# $str =~ s/\(.*\)//; |
|
574
|
|
|
|
|
|
|
# $str = '1_' . $str; |
|
575
|
|
|
|
|
|
|
# my ($year, $month, $day) = Date::Calc::Decode_Date_EU ($str); |
|
576
|
|
|
|
|
|
|
# if (! $year || ! $month) { die "LME: unrecognised filename month: $str"; } |
|
577
|
|
|
|
|
|
|
# return App::Chart::Timebase::Months::ymd_to_mdate ($year, $month, 1); |
|
578
|
|
|
|
|
|
|
# } |
|
579
|
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
582
|
|
|
|
|
|
|
# download - month price xls files |
|
583
|
|
|
|
|
|
|
# |
|
584
|
|
|
|
|
|
|
# This crunches files like |
|
585
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/April_2008.xls |
|
586
|
|
|
|
|
|
|
# |
|
587
|
|
|
|
|
|
|
|
|
588
|
|
|
|
|
|
|
App::Chart::DownloadHandler->new |
|
589
|
|
|
|
|
|
|
(name => __('LME month xls'), |
|
590
|
|
|
|
|
|
|
pred => $pred, |
|
591
|
|
|
|
|
|
|
proc => \&monthxls_download, |
|
592
|
|
|
|
|
|
|
# backto => \&monthxls_backto, |
|
593
|
|
|
|
|
|
|
available_tdate => \&monthxls_available_tdate); |
|
594
|
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
# Return tdate of anticipated available montly .xls download, that being |
|
596
|
|
|
|
|
|
|
# the end of the previous month. |
|
597
|
|
|
|
|
|
|
# |
|
598
|
|
|
|
|
|
|
# Don't know exactly when a new month full of data becomes available, |
|
599
|
|
|
|
|
|
|
# assume here midnight at the start of the second trading day of the new |
|
600
|
|
|
|
|
|
|
# month. |
|
601
|
|
|
|
|
|
|
# |
|
602
|
|
|
|
|
|
|
sub monthxls_available_tdate { |
|
603
|
|
|
|
|
|
|
my $tdate = App::Chart::Download::tdate_today |
|
604
|
|
|
|
|
|
|
(App::Chart::TZ->london); |
|
605
|
|
|
|
|
|
|
$tdate--; # not until second business day into this month |
|
606
|
|
|
|
|
|
|
$tdate = tdate_start_of_month ($tdate); |
|
607
|
|
|
|
|
|
|
return $tdate - 1; # last day of previous month |
|
608
|
|
|
|
|
|
|
} |
|
609
|
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
sub monthxls_download { |
|
611
|
|
|
|
|
|
|
my ($symbol_list) = @_; |
|
612
|
|
|
|
|
|
|
if (DEBUG) { print "LME ",@$symbol_list,"\n"; } |
|
613
|
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
my $lo_tdate = App::Chart::Download::start_tdate_for_update (@$symbol_list); |
|
615
|
|
|
|
|
|
|
my $hi_tdate = monthxls_available_tdate(); |
|
616
|
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
my @files = grep {$_->{'url'} !~ /volume/i} historical_xls_files(); |
|
618
|
|
|
|
|
|
|
my $files = App::Chart::Download::choose_files (\@files, $lo_tdate, $hi_tdate); |
|
619
|
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
foreach my $f (@$files) { |
|
621
|
|
|
|
|
|
|
my $url = $f->{'url'}; |
|
622
|
|
|
|
|
|
|
require File::Basename; |
|
623
|
|
|
|
|
|
|
my $filename = File::Basename::basename($url); |
|
624
|
|
|
|
|
|
|
App::Chart::Download::status (__x('LME data {filename}', |
|
625
|
|
|
|
|
|
|
filename => $filename)); |
|
626
|
|
|
|
|
|
|
my $resp = App::Chart::Download->get ($url); |
|
627
|
|
|
|
|
|
|
my $h = monthxls_parse ($resp); |
|
628
|
|
|
|
|
|
|
App::Chart::Download::write_daily_group ($h); |
|
629
|
|
|
|
|
|
|
} |
|
630
|
|
|
|
|
|
|
} |
|
631
|
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
sub tdate_start_of_month { |
|
633
|
|
|
|
|
|
|
my ($tdate) = @_; |
|
634
|
|
|
|
|
|
|
my ($year,$month,$day) = App::Chart::tdate_to_ymd ($tdate); |
|
635
|
|
|
|
|
|
|
return App::Chart::ymd_to_tdate_ceil ($year, $month, 1); |
|
636
|
|
|
|
|
|
|
} |
|
637
|
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
my %monthxls_sheet_to_commodity = |
|
639
|
|
|
|
|
|
|
('Copper' => 'CA', |
|
640
|
|
|
|
|
|
|
'Al. Alloy' => 'AA', |
|
641
|
|
|
|
|
|
|
'NASAAC' => 'NA', |
|
642
|
|
|
|
|
|
|
'Zinc' => 'ZS', |
|
643
|
|
|
|
|
|
|
'Lead' => 'PB', |
|
644
|
|
|
|
|
|
|
'Pr. Aluminium' => 'AH', |
|
645
|
|
|
|
|
|
|
'Tin' => 'SN', |
|
646
|
|
|
|
|
|
|
'Nickel' => 'NI', |
|
647
|
|
|
|
|
|
|
'Far East' => 'FF', # steel |
|
648
|
|
|
|
|
|
|
'Med' => 'FM', # steel |
|
649
|
|
|
|
|
|
|
'Averages' => undef, |
|
650
|
|
|
|
|
|
|
'Plastic Avg' => undef, |
|
651
|
|
|
|
|
|
|
'Averages inc. Euro Eq' => undef); |
|
652
|
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
sub monthxls_parse { |
|
654
|
|
|
|
|
|
|
my ($resp) = @_; |
|
655
|
|
|
|
|
|
|
my $content = $resp->decoded_content (charset => 'none', raise_error => 1); |
|
656
|
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
require Spreadsheet::ParseExcel; |
|
658
|
|
|
|
|
|
|
require Spreadsheet::ParseExcel::Utility; |
|
659
|
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
my @data = (); |
|
661
|
|
|
|
|
|
|
my $h = { source => __PACKAGE__, |
|
662
|
|
|
|
|
|
|
cover_pred => $pred, |
|
663
|
|
|
|
|
|
|
data => \@data }; |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
my $excel = Spreadsheet::ParseExcel::Workbook->Parse (\$content); |
|
666
|
|
|
|
|
|
|
foreach my $sheet (@{$excel->{Worksheet}}) { |
|
667
|
|
|
|
|
|
|
my $sheet_name = $sheet->{'Name'}; |
|
668
|
|
|
|
|
|
|
if (DEBUG) { print "Sheet: $sheet_name\n"; } |
|
669
|
|
|
|
|
|
|
my $commodity; |
|
670
|
|
|
|
|
|
|
if ($sheet_name =~ /^[A-Z][A-Z]$/) { |
|
671
|
|
|
|
|
|
|
# plastics symbol |
|
672
|
|
|
|
|
|
|
$commodity = $sheet_name; |
|
673
|
|
|
|
|
|
|
} elsif (exists $monthxls_sheet_to_commodity{$sheet_name}) { |
|
674
|
|
|
|
|
|
|
$commodity = $monthxls_sheet_to_commodity{$sheet_name} |
|
675
|
|
|
|
|
|
|
// next; # undef for ignored sheets |
|
676
|
|
|
|
|
|
|
} else { |
|
677
|
|
|
|
|
|
|
warn "LME: unrecognised month data sheet: $sheet_name\n"; |
|
678
|
|
|
|
|
|
|
next; |
|
679
|
|
|
|
|
|
|
} |
|
680
|
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
my ($minrow, $maxrow) = $sheet->RowRange; |
|
682
|
|
|
|
|
|
|
my ($mincol, $maxcol) = $sheet->ColRange; |
|
683
|
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
my $heading_row = $minrow; |
|
685
|
|
|
|
|
|
|
my $date_col; |
|
686
|
|
|
|
|
|
|
my $seller_col; |
|
687
|
|
|
|
|
|
|
HEADING: for (;; $heading_row++) { |
|
688
|
|
|
|
|
|
|
if ($heading_row > $maxrow) { die "LME: headings row not found\n"; } |
|
689
|
|
|
|
|
|
|
for ($seller_col = $mincol; $seller_col <= $maxcol; $seller_col++) { |
|
690
|
|
|
|
|
|
|
my $cell = $sheet->Cell($heading_row,$seller_col) // next; |
|
691
|
|
|
|
|
|
|
my $str = $cell->Value; |
|
692
|
|
|
|
|
|
|
if (DEBUG >= 2) { print " cell $heading_row,$seller_col $str\n"; } |
|
693
|
|
|
|
|
|
|
if ($str =~ /SELLER/i) { last HEADING; } |
|
694
|
|
|
|
|
|
|
} |
|
695
|
|
|
|
|
|
|
} |
|
696
|
|
|
|
|
|
|
$date_col = $seller_col - 2; |
|
697
|
|
|
|
|
|
|
if (DEBUG) { print " heading row $heading_row seller col $seller_col\n"; } |
|
698
|
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
my @column_num = (); |
|
700
|
|
|
|
|
|
|
my @column_symbol = (); |
|
701
|
|
|
|
|
|
|
for (my $col = $seller_col; $col+2 <= $maxcol; $col += 3) { |
|
702
|
|
|
|
|
|
|
my $cell = $sheet->Cell($heading_row,$col) || last; |
|
703
|
|
|
|
|
|
|
$cell->Value =~ /SELLER/i or next; |
|
704
|
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
my $period = $sheet->Cell($heading_row-1,$col)->Value; |
|
706
|
|
|
|
|
|
|
if (DEBUG >= 2) { print " col=$col period=$period\n"; } |
|
707
|
|
|
|
|
|
|
if ($period =~ /cash/i) { |
|
708
|
|
|
|
|
|
|
$period = ''; |
|
709
|
|
|
|
|
|
|
} elsif ($period =~ /([0-9]+).*(months|mths)/i) { |
|
710
|
|
|
|
|
|
|
$period = $1; |
|
711
|
|
|
|
|
|
|
} elsif ($period eq '') { |
|
712
|
|
|
|
|
|
|
last; |
|
713
|
|
|
|
|
|
|
} else { |
|
714
|
|
|
|
|
|
|
die "LME: month sheet '$sheet_name' heading row=$heading_row col=$col period unrecognised: '$period'\n"; |
|
715
|
|
|
|
|
|
|
} |
|
716
|
|
|
|
|
|
|
push @column_num, $col; |
|
717
|
|
|
|
|
|
|
push @column_symbol, "$commodity$period.LME"; |
|
718
|
|
|
|
|
|
|
} |
|
719
|
|
|
|
|
|
|
if (! @column_num) { |
|
720
|
|
|
|
|
|
|
die "LME: oops, sheet '$sheet_name' month data columns not matched\n"; |
|
721
|
|
|
|
|
|
|
} |
|
722
|
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
my $seen_date = 0; |
|
724
|
|
|
|
|
|
|
foreach my $row ($heading_row+1 .. $maxrow) { |
|
725
|
|
|
|
|
|
|
my $datecell = $sheet->Cell($row,$date_col) or next; |
|
726
|
|
|
|
|
|
|
# skip blanks at end, avoid "Total" |
|
727
|
|
|
|
|
|
|
$datecell->{'Type'} eq 'Date' or next; |
|
728
|
|
|
|
|
|
|
# default format is like 31-Jan-08, go straight to ISO to be unambiguous |
|
729
|
|
|
|
|
|
|
my $date = Spreadsheet::ParseExcel::Utility::ExcelFmt |
|
730
|
|
|
|
|
|
|
('yyyy-mm-dd', $datecell->{'Val'}, $excel->{'Flg1904'}); |
|
731
|
|
|
|
|
|
|
$seen_date = 1; |
|
732
|
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
foreach my $i (0 .. $#column_num) { |
|
734
|
|
|
|
|
|
|
my $col = $column_num[$i]; |
|
735
|
|
|
|
|
|
|
my $symbol = $column_symbol[$i]; |
|
736
|
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
# unformatted value gets '1490.00' instead of '$1,490.00' |
|
738
|
|
|
|
|
|
|
my $seller = $sheet->Cell($row,$col)->{'Val'}; |
|
739
|
|
|
|
|
|
|
push @data, { symbol => $symbol, |
|
740
|
|
|
|
|
|
|
date => $date, |
|
741
|
|
|
|
|
|
|
close => $seller, |
|
742
|
|
|
|
|
|
|
}; |
|
743
|
|
|
|
|
|
|
} |
|
744
|
|
|
|
|
|
|
} |
|
745
|
|
|
|
|
|
|
if (! $seen_date) { |
|
746
|
|
|
|
|
|
|
die "LME month data: no dates found in sheet '$sheet_name'"; |
|
747
|
|
|
|
|
|
|
} |
|
748
|
|
|
|
|
|
|
} |
|
749
|
|
|
|
|
|
|
my $date = $data[0]->{'date'}; |
|
750
|
|
|
|
|
|
|
my ($year, $month, $day) = App::Chart::iso_to_ymd ($date); |
|
751
|
|
|
|
|
|
|
$h->{'cover_lo_date'} = App::Chart::ymd_to_iso ($year, $month, 1); |
|
752
|
|
|
|
|
|
|
($year, $month, $day) = Date::Calc::Add_Delta_YMD ($year, $month, $day, |
|
753
|
|
|
|
|
|
|
0, 1, -1); |
|
754
|
|
|
|
|
|
|
$h->{'cover_hi_date'} = App::Chart::ymd_to_iso ($year, $month, $day); |
|
755
|
|
|
|
|
|
|
return $h; |
|
756
|
|
|
|
|
|
|
} |
|
757
|
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
759
|
|
|
|
|
|
|
# download - volume xls files |
|
760
|
|
|
|
|
|
|
# |
|
761
|
|
|
|
|
|
|
# This crunches files like |
|
762
|
|
|
|
|
|
|
# http://www.lme.co.uk/downloads/volumes_Jan_08.xls |
|
763
|
|
|
|
|
|
|
# |
|
764
|
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
# App::Chart::DownloadHandler->new |
|
766
|
|
|
|
|
|
|
# (name => __('LME month volumes'), |
|
767
|
|
|
|
|
|
|
# pred => $pred, |
|
768
|
|
|
|
|
|
|
# proc => \&volume_download, |
|
769
|
|
|
|
|
|
|
# # backto => \&volume_backto, |
|
770
|
|
|
|
|
|
|
# available_tdate => \&monthxls_available_tdate); |
|
771
|
|
|
|
|
|
|
|
|
772
|
|
|
|
|
|
|
sub volume_download { |
|
773
|
|
|
|
|
|
|
my ($symbol_list) = @_; |
|
774
|
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
my $lo_tdate = App::Chart::Download::start_tdate_for_update (@$symbol_list); |
|
776
|
|
|
|
|
|
|
my $hi_tdate = monthxls_available_tdate(); |
|
777
|
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
my @files = grep {$_->{'url'} =~ /volume/i} historical_xls_files(); |
|
779
|
|
|
|
|
|
|
my $files = App::Chart::Download::choose_files (\@files, $lo_tdate, $hi_tdate); |
|
780
|
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
foreach my $f (@$files) { |
|
782
|
|
|
|
|
|
|
my $url = $f->{'url'}; |
|
783
|
|
|
|
|
|
|
require File::Basename; |
|
784
|
|
|
|
|
|
|
my $filename = File::Basename::basename($url); |
|
785
|
|
|
|
|
|
|
App::Chart::Download::status (__x('LME volumes {filename}', |
|
786
|
|
|
|
|
|
|
filename => $filename)); |
|
787
|
|
|
|
|
|
|
my $resp = App::Chart::Download->get ($url); |
|
788
|
|
|
|
|
|
|
my $h = volume_parse ($resp); |
|
789
|
|
|
|
|
|
|
App::Chart::Download::write_daily_group ($h); |
|
790
|
|
|
|
|
|
|
} |
|
791
|
|
|
|
|
|
|
} |
|
792
|
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
sub volume_parse { |
|
794
|
|
|
|
|
|
|
my ($resp) = @_; |
|
795
|
|
|
|
|
|
|
my $content = $resp->decoded_content (charset => 'none', raise_error => 1); |
|
796
|
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
require Spreadsheet::ParseExcel; |
|
798
|
|
|
|
|
|
|
require Spreadsheet::ParseExcel::Utility; |
|
799
|
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
my @data = (); |
|
801
|
|
|
|
|
|
|
my $h = { source => __PACKAGE__, |
|
802
|
|
|
|
|
|
|
data => \@data }; |
|
803
|
|
|
|
|
|
|
|
|
804
|
|
|
|
|
|
|
my $excel = Spreadsheet::ParseExcel::Workbook->Parse (\$content); |
|
805
|
|
|
|
|
|
|
my $sheet = $excel->Worksheet (0); |
|
806
|
|
|
|
|
|
|
if (DEBUG) { print "Sheet: ",$sheet->{'Name'},"\n"; } |
|
807
|
|
|
|
|
|
|
|
|
808
|
|
|
|
|
|
|
my ($minrow, $maxrow) = $sheet->RowRange; |
|
809
|
|
|
|
|
|
|
my ($mincol, $maxcol) = $sheet->ColRange; |
|
810
|
|
|
|
|
|
|
|
|
811
|
|
|
|
|
|
|
# headings are like "AAFUT" for Aluminium Alloy, find that row |
|
812
|
|
|
|
|
|
|
my $heading_row; |
|
813
|
|
|
|
|
|
|
HEADINGROW: foreach my $row ($minrow .. $maxrow) { |
|
814
|
|
|
|
|
|
|
foreach my $col ($mincol .. $maxcol) { |
|
815
|
|
|
|
|
|
|
my $cell = $sheet->Cell($row,$col) or next; |
|
816
|
|
|
|
|
|
|
if ($cell->Value =~ /FUT$/) { |
|
817
|
|
|
|
|
|
|
$heading_row = $row; |
|
818
|
|
|
|
|
|
|
last HEADINGROW; |
|
819
|
|
|
|
|
|
|
} |
|
820
|
|
|
|
|
|
|
} |
|
821
|
|
|
|
|
|
|
} |
|
822
|
|
|
|
|
|
|
if (! $heading_row) { die 'LME Volumes: unrecognised headings'; } |
|
823
|
|
|
|
|
|
|
if (DEBUG) { print " heading row $heading_row\n"; } |
|
824
|
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
# look for each "AAFUT" etc column in the heading row |
|
826
|
|
|
|
|
|
|
my @column_num = (); |
|
827
|
|
|
|
|
|
|
my @column_symbol = (); |
|
828
|
|
|
|
|
|
|
foreach my $col ($mincol .. $maxcol) { |
|
829
|
|
|
|
|
|
|
my $cell = $sheet->Cell($heading_row,$col) // next; # skip empties |
|
830
|
|
|
|
|
|
|
$cell->{'Type'} eq 'Text' or next; # skip dates in heading |
|
831
|
|
|
|
|
|
|
my $str = $cell->Value; |
|
832
|
|
|
|
|
|
|
$str =~ /(.*)FUT$/ or next; |
|
833
|
|
|
|
|
|
|
my $commodity = $1; |
|
834
|
|
|
|
|
|
|
push @column_num, $col; |
|
835
|
|
|
|
|
|
|
push @column_symbol, $commodity . '.LME'; |
|
836
|
|
|
|
|
|
|
} |
|
837
|
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
my $seen_date = 0; |
|
839
|
|
|
|
|
|
|
foreach my $row ($heading_row+1 .. $maxrow) { |
|
840
|
|
|
|
|
|
|
my $date; |
|
841
|
|
|
|
|
|
|
# Jan 2008 has 'Date' type in column 1 |
|
842
|
|
|
|
|
|
|
# May 2008 onwards has text d-Mmm-yy in column 0 |
|
843
|
|
|
|
|
|
|
my $datecell = $sheet->Cell($row,0); |
|
844
|
|
|
|
|
|
|
if ($datecell->{'Type'} eq 'Text') { |
|
845
|
|
|
|
|
|
|
$date = App::Chart::Download::Decode_Date_EU_to_iso($datecell->{'Val'},1); |
|
846
|
|
|
|
|
|
|
# skip blanks at end, avoid "Total" |
|
847
|
|
|
|
|
|
|
if (! defined $date) { next; } |
|
848
|
|
|
|
|
|
|
} else { |
|
849
|
|
|
|
|
|
|
$datecell = $sheet->Cell($row,1); |
|
850
|
|
|
|
|
|
|
# skip blanks at end, avoid "Total" |
|
851
|
|
|
|
|
|
|
$datecell->{'Type'} eq 'Date' or next; |
|
852
|
|
|
|
|
|
|
# default format is like 31-Jan-08, go straight to ISO to be unambiguous |
|
853
|
|
|
|
|
|
|
$date = Spreadsheet::ParseExcel::Utility::ExcelFmt |
|
854
|
|
|
|
|
|
|
('yyyy-mm-dd', $datecell->{'Val'}, $excel->{'Flg1904'}); |
|
855
|
|
|
|
|
|
|
} |
|
856
|
|
|
|
|
|
|
$seen_date = 1; |
|
857
|
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
foreach my $i (0 .. $#column_num) { |
|
859
|
|
|
|
|
|
|
my $col = $column_num[$i]; |
|
860
|
|
|
|
|
|
|
my $symbol = $column_symbol[$i]; |
|
861
|
|
|
|
|
|
|
my $volume = $sheet->Cell($row,$col)->Value; |
|
862
|
|
|
|
|
|
|
push @data, { symbol => $symbol, |
|
863
|
|
|
|
|
|
|
date => $date, |
|
864
|
|
|
|
|
|
|
volume => $volume, |
|
865
|
|
|
|
|
|
|
}; |
|
866
|
|
|
|
|
|
|
} |
|
867
|
|
|
|
|
|
|
} |
|
868
|
|
|
|
|
|
|
if (! $seen_date) { |
|
869
|
|
|
|
|
|
|
die 'LME volumes: no dates found'; |
|
870
|
|
|
|
|
|
|
} |
|
871
|
|
|
|
|
|
|
|
|
872
|
|
|
|
|
|
|
return $h; |
|
873
|
|
|
|
|
|
|
} |
|
874
|
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
876
|
|
|
|
|
|
|
# download - daily |
|
877
|
|
|
|
|
|
|
# |
|
878
|
|
|
|
|
|
|
# This uses the metals and plastics settlement pages (login required) at |
|
879
|
|
|
|
|
|
|
# |
|
880
|
|
|
|
|
|
|
# https://secure.lme.com/Data/community/Dataprices_daily_metals.aspx |
|
881
|
|
|
|
|
|
|
# https://secure.lme.com/Data/community/Dataprices_daily_prices_plastics.aspx |
|
882
|
|
|
|
|
|
|
# https://secure.lme.com/Data/community/Dataprices_Steels_OfficialPrices.aspx |
|
883
|
|
|
|
|
|
|
# |
|
884
|
|
|
|
|
|
|
|
|
885
|
|
|
|
|
|
|
my $daily_pred = App::Chart::Sympred::Proc->new (\&is_daily_symbol); |
|
886
|
|
|
|
|
|
|
sub is_daily_symbol { |
|
887
|
|
|
|
|
|
|
my ($symbol) = @_; |
|
888
|
|
|
|
|
|
|
return ($pred->match ($symbol) && is_enabled()); |
|
889
|
|
|
|
|
|
|
} |
|
890
|
|
|
|
|
|
|
sub is_enabled { |
|
891
|
|
|
|
|
|
|
my $username = App::Chart::Database->preference_get ('lme-username', undef); |
|
892
|
|
|
|
|
|
|
return (defined $username && $username ne ''); |
|
893
|
|
|
|
|
|
|
} |
|
894
|
|
|
|
|
|
|
|
|
895
|
|
|
|
|
|
|
# App::Chart::DownloadHandler->new |
|
896
|
|
|
|
|
|
|
# (name => __('LME daily'), |
|
897
|
|
|
|
|
|
|
# pred => $daily_pred, |
|
898
|
|
|
|
|
|
|
# proc => \&daily_download, |
|
899
|
|
|
|
|
|
|
# available_tdate_by_symbol => \&daily_available_tdate); |
|
900
|
|
|
|
|
|
|
# |
|
901
|
|
|
|
|
|
|
# sub daily_available_tdate { |
|
902
|
|
|
|
|
|
|
# my ($symbol) = @_; |
|
903
|
|
|
|
|
|
|
# return |
|
904
|
|
|
|
|
|
|
# App::Chart::Download::iso_to_tdate_floor (daily_available_date ($symbol)); |
|
905
|
|
|
|
|
|
|
# } |
|
906
|
|
|
|
|
|
|
|
|
907
|
|
|
|
|
|
|
sub daily_download { |
|
908
|
|
|
|
|
|
|
my ($symbol_list) = @_; |
|
909
|
|
|
|
|
|
|
|
|
910
|
|
|
|
|
|
|
my $sm = partition_by_key ($symbol_list, \&type); |
|
911
|
|
|
|
|
|
|
while (my ($type, $symbol_list) = each %$sm) { |
|
912
|
|
|
|
|
|
|
App::Chart::Download::verbose_message ('LME', $type, @$symbol_list); |
|
913
|
|
|
|
|
|
|
|
|
914
|
|
|
|
|
|
|
login_ensure(); |
|
915
|
|
|
|
|
|
|
my $l = daily_latest ($type); |
|
916
|
|
|
|
|
|
|
|
|
917
|
|
|
|
|
|
|
my $lo_tdate = App::Chart::Download::start_tdate_for_update (@$symbol_list); |
|
918
|
|
|
|
|
|
|
my $hi_tdate = $l->{'tdate'} - 1; |
|
919
|
|
|
|
|
|
|
|
|
920
|
|
|
|
|
|
|
foreach my $tdate ($lo_tdate .. $hi_tdate) { |
|
921
|
|
|
|
|
|
|
my $resp = daily_download_one ($type, $tdate, $l); |
|
922
|
|
|
|
|
|
|
my $h = daily_parse ($resp, $tdate); |
|
923
|
|
|
|
|
|
|
App::Chart::Download::write_daily_group ($h); |
|
924
|
|
|
|
|
|
|
} |
|
925
|
|
|
|
|
|
|
App::Chart::Download::write_daily_group ($l->{'h'}); |
|
926
|
|
|
|
|
|
|
} |
|
927
|
|
|
|
|
|
|
} |
|
928
|
|
|
|
|
|
|
|
|
929
|
|
|
|
|
|
|
sub partition_by_key { |
|
930
|
|
|
|
|
|
|
my ($list, $func) = @_; |
|
931
|
|
|
|
|
|
|
require Tie::IxHash; |
|
932
|
|
|
|
|
|
|
my %sm; |
|
933
|
|
|
|
|
|
|
tie %sm, 'Tie::IxHash'; |
|
934
|
|
|
|
|
|
|
foreach my $elem (@$list) { |
|
935
|
|
|
|
|
|
|
my $key = $func->($elem); |
|
936
|
|
|
|
|
|
|
push @{$sm{$key}}, $elem; |
|
937
|
|
|
|
|
|
|
} |
|
938
|
|
|
|
|
|
|
return \%sm; |
|
939
|
|
|
|
|
|
|
} |
|
940
|
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
sub daily_download_one { |
|
942
|
|
|
|
|
|
|
my ($type, $tdate, $l) = @_; |
|
943
|
|
|
|
|
|
|
|
|
944
|
|
|
|
|
|
|
require HTML::Form; |
|
945
|
|
|
|
|
|
|
my $content = $l->{'content'}; |
|
946
|
|
|
|
|
|
|
my $url = $l->{'url'}; |
|
947
|
|
|
|
|
|
|
my $form = HTML::Form->parse($content, $url) |
|
948
|
|
|
|
|
|
|
or die "LME metals page not a form"; |
|
949
|
|
|
|
|
|
|
|
|
950
|
|
|
|
|
|
|
my ($year, $month, $day) = App::Chart::tdate_to_ymd ($tdate); |
|
951
|
|
|
|
|
|
|
# these are literal "$" in the field name |
|
952
|
|
|
|
|
|
|
$form->value ("_searchForm\$_lstdate", $day); |
|
953
|
|
|
|
|
|
|
$form->value ("_searchForm\$_lstmonth", $month); |
|
954
|
|
|
|
|
|
|
$form->value ("_searchForm\$_lstyear", $year); |
|
955
|
|
|
|
|
|
|
|
|
956
|
|
|
|
|
|
|
App::Chart::Download::status |
|
957
|
|
|
|
|
|
|
(__x('LME daily {type} {date}', |
|
958
|
|
|
|
|
|
|
type => $type, |
|
959
|
|
|
|
|
|
|
date => App::Chart::Download::tdate_range_string ($tdate))); |
|
960
|
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
require App::Chart::UserAgent; |
|
962
|
|
|
|
|
|
|
require HTTP::Cookies; |
|
963
|
|
|
|
|
|
|
my $ua = App::Chart::UserAgent->instance->clone; |
|
964
|
|
|
|
|
|
|
$ua->requests_redirectable ([]); |
|
965
|
|
|
|
|
|
|
my $jar = HTTP::Cookies->new; |
|
966
|
|
|
|
|
|
|
$ua->cookie_jar ($jar); |
|
967
|
|
|
|
|
|
|
|
|
968
|
|
|
|
|
|
|
my $req = $form->click(); |
|
969
|
|
|
|
|
|
|
my $resp = $ua->request ($req); |
|
970
|
|
|
|
|
|
|
|
|
971
|
|
|
|
|
|
|
if (! $resp->is_success) { |
|
972
|
|
|
|
|
|
|
die "Cannot download $url\n",$resp->headers->as_string,"\n"; |
|
973
|
|
|
|
|
|
|
} |
|
974
|
|
|
|
|
|
|
return $resp; |
|
975
|
|
|
|
|
|
|
} |
|
976
|
|
|
|
|
|
|
|
|
977
|
|
|
|
|
|
|
my %type_to_daily_url |
|
978
|
|
|
|
|
|
|
= (metals => 'https://secure.lme.com/Data/community/Dataprices_daily_metals.aspx', |
|
979
|
|
|
|
|
|
|
plastics => 'https://secure.lme.com/Data/community/Dataprices_daily_prices_plastics.aspx', |
|
980
|
|
|
|
|
|
|
steels => 'https://secure.lme.com/Data/community/Dataprices_Steels_OfficialPrices.aspx'); |
|
981
|
|
|
|
|
|
|
|
|
982
|
|
|
|
|
|
|
sub daily_latest { |
|
983
|
|
|
|
|
|
|
my ($type) = @_; |
|
984
|
|
|
|
|
|
|
require App::Chart::Pagebits; |
|
985
|
|
|
|
|
|
|
return App::Chart::Pagebits::get |
|
986
|
|
|
|
|
|
|
(name => __x('LME daily latest {type}', |
|
987
|
|
|
|
|
|
|
type => $type), |
|
988
|
|
|
|
|
|
|
url => $type_to_daily_url{$type}, |
|
989
|
|
|
|
|
|
|
key => "lme-daily-latest-$type", |
|
990
|
|
|
|
|
|
|
freq_days => 0, |
|
991
|
|
|
|
|
|
|
timezone => App::Chart::TZ->london, |
|
992
|
|
|
|
|
|
|
parse => \&daily_latest_parse); |
|
993
|
|
|
|
|
|
|
} |
|
994
|
|
|
|
|
|
|
|
|
995
|
|
|
|
|
|
|
sub daily_latest_parse { |
|
996
|
|
|
|
|
|
|
my ($resp) = @_; |
|
997
|
|
|
|
|
|
|
my $content = $resp->decoded_content (raise_error => 1); |
|
998
|
|
|
|
|
|
|
my $h = daily_parse ($resp); |
|
999
|
|
|
|
|
|
|
return { h => $h, |
|
1000
|
|
|
|
|
|
|
date => $h->{'data'}->[0]->{'date'}, |
|
1001
|
|
|
|
|
|
|
url => $resp->uri->as_string, |
|
1002
|
|
|
|
|
|
|
content => $content }; |
|
1003
|
|
|
|
|
|
|
} |
|
1004
|
|
|
|
|
|
|
|
|
1005
|
|
|
|
|
|
|
|
|
1006
|
|
|
|
|
|
|
1; |
|
1007
|
|
|
|
|
|
|
__END__ |
|
1008
|
|
|
|
|
|
|
|
|
1009
|
|
|
|
|
|
|
|
|
1010
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
1011
|
|
|
|
|
|
|
# download - daily |
|
1012
|
|
|
|
|
|
|
# |
|
1013
|
|
|
|
|
|
|
# |
|
1014
|
|
|
|
|
|
|
|
|
1015
|
|
|
|
|
|
|
# LST has elements (SYMBOL NAME TDATE BUY-STR SELL-STR MDATE) per |
|
1016
|
|
|
|
|
|
|
# `daily-html-parse' |
|
1017
|
|
|
|
|
|
|
# |
|
1018
|
|
|
|
|
|
|
# The sell price is used. The report for cash prices has the seller marked |
|
1019
|
|
|
|
|
|
|
# as the settlement and for the forwards the historical files can be seen |
|
1020
|
|
|
|
|
|
|
# with the seller price. |
|
1021
|
|
|
|
|
|
|
# |
|
1022
|
|
|
|
|
|
|
(define (daily-process symbol-list lst) |
|
1023
|
|
|
|
|
|
|
(download-process |
|
1024
|
|
|
|
|
|
|
#:module (_ "LME") |
|
1025
|
|
|
|
|
|
|
#:symbol-list symbol-list |
|
1026
|
|
|
|
|
|
|
#:currency "USD" |
|
1027
|
|
|
|
|
|
|
#:row-list |
|
1028
|
|
|
|
|
|
|
(map (lambda (row) |
|
1029
|
|
|
|
|
|
|
(receive-list (symbol name tdate buy sell mdate) |
|
1030
|
|
|
|
|
|
|
row |
|
1031
|
|
|
|
|
|
|
(list #:tdate tdate |
|
1032
|
|
|
|
|
|
|
#:mdate mdate |
|
1033
|
|
|
|
|
|
|
#:commodity (chart-symbol-commodity symbol) |
|
1034
|
|
|
|
|
|
|
#:close sell))) |
|
1035
|
|
|
|
|
|
|
lst))) |
|
1036
|
|
|
|
|
|
|
|
|
1037
|
|
|
|
|
|
|
(define (lme-daily-download symbol-list type) |
|
1038
|
|
|
|
|
|
|
(define selector (case type |
|
1039
|
|
|
|
|
|
|
((metals) lme-metal-symbol?) |
|
1040
|
|
|
|
|
|
|
((plastics) lme-plastics-symbol?))) |
|
1041
|
|
|
|
|
|
|
|
|
1042
|
|
|
|
|
|
|
(set! symbol-list (filter selector symbol-list)) |
|
1043
|
|
|
|
|
|
|
(if (not (null? symbol-list)) |
|
1044
|
|
|
|
|
|
|
|
|
1045
|
|
|
|
|
|
|
(let* ((end-data (assq-ref (daily-latest-info type) 'data)) |
|
1046
|
|
|
|
|
|
|
(end-tdate (if end-data |
|
1047
|
|
|
|
|
|
|
(data-tdate end-data) |
|
1048
|
|
|
|
|
|
|
(daily-available-tdate type)))) |
|
1049
|
|
|
|
|
|
|
|
|
1050
|
|
|
|
|
|
|
(set! symbol-list |
|
1051
|
|
|
|
|
|
|
(download-also symbol-list #:selector selector)) |
|
1052
|
|
|
|
|
|
|
|
|
1053
|
|
|
|
|
|
|
# only go back 25 days for LMEX or others without yearly data, |
|
1054
|
|
|
|
|
|
|
# since at 70kbytes per day it quickly becomes slow |
|
1055
|
|
|
|
|
|
|
# |
|
1056
|
|
|
|
|
|
|
(do ((t (apply min (map (lambda (symbol) |
|
1057
|
|
|
|
|
|
|
(download-start-tdate symbol #:initial 25)) |
|
1058
|
|
|
|
|
|
|
symbol-list)) |
|
1059
|
|
|
|
|
|
|
(1+ t))) |
|
1060
|
|
|
|
|
|
|
((>= t end-tdate)) |
|
1061
|
|
|
|
|
|
|
(and-let* ((data (lme-daily-download-tdate type t))) |
|
1062
|
|
|
|
|
|
|
(daily-process symbol-list data))) |
|
1063
|
|
|
|
|
|
|
|
|
1064
|
|
|
|
|
|
|
(if end-data |
|
1065
|
|
|
|
|
|
|
(daily-process symbol-list end-data))))) |
|
1066
|
|
|
|
|
|
|
|
|
1067
|
|
|
|
|
|
|
|
|
1068
|
|
|
|
|
|
|
(define (lme-historical-download symbol-list) |
|
1069
|
|
|
|
|
|
|
|
|
1070
|
|
|
|
|
|
|
(let* ((avail-tdate (monthxls-available-tdate))) |
|
1071
|
|
|
|
|
|
|
|
|
1072
|
|
|
|
|
|
|
# whether can update prices for SYMBOL using xls |
|
1073
|
|
|
|
|
|
|
(define (want-monthxls? symbol) |
|
1074
|
|
|
|
|
|
|
(>= avail-tdate (download-start-tdate symbol))) |
|
1075
|
|
|
|
|
|
|
|
|
1076
|
|
|
|
|
|
|
# whether can update volume for SYMBOL |
|
1077
|
|
|
|
|
|
|
(define (want-volume? symbol) |
|
1078
|
|
|
|
|
|
|
# no volumes for forward symbols like "ZINC 3.LME" or futures |
|
1079
|
|
|
|
|
|
|
# specific symbols like "PP MAY 06.LME" |
|
1080
|
|
|
|
|
|
|
(and (not (string-any char-numeric? symbol)) |
|
1081
|
|
|
|
|
|
|
(let ((last-tdate (database-last-volume symbol))) |
|
1082
|
|
|
|
|
|
|
(or (not last-tdate) |
|
1083
|
|
|
|
|
|
|
(< last-tdate avail-tdate))))) |
|
1084
|
|
|
|
|
|
|
|
|
1085
|
|
|
|
|
|
|
# whether can update anything for SYMBOL |
|
1086
|
|
|
|
|
|
|
(define (want-update? symbol) |
|
1087
|
|
|
|
|
|
|
(or (want-monthxls? symbol) |
|
1088
|
|
|
|
|
|
|
(want-volume? symbol))) |
|
1089
|
|
|
|
|
|
|
|
|
1090
|
|
|
|
|
|
|
(if (any want-update? symbol-list) |
|
1091
|
|
|
|
|
|
|
(begin |
|
1092
|
|
|
|
|
|
|
(if (any want-monthxls? symbol-list) |
|
1093
|
|
|
|
|
|
|
(monthxls-download symbol-list)) |
|
1094
|
|
|
|
|
|
|
(if (any want-volume? symbol-list) |
|
1095
|
|
|
|
|
|
|
(volume-download symbol-list)))))) |
|
1096
|
|
|
|
|
|
|
|
|
1097
|
|
|
|
|
|
|
|
|
1098
|
|
|
|
|
|
|
|
|
1099
|
|
|
|
|
|
|
(let ((vol-tdate (database-last-volume symbol))) |
|
1100
|
|
|
|
|
|
|
|
|
1101
|
|
|
|
|
|
|
|
|
1102
|
|
|
|
|
|
|
|
|
1103
|
|
|
|
|
|
|
# date is: "26 Jan 2005 (Data >1 day old) </b></td>" |
|
1104
|
|
|
|
|
|
|
# or: "3 Feb 2005 </b></td>" |
|
1105
|
|
|
|
|
|
|
(let* ((m (must-match (string-match " Prices[ ,][^\n]*for +([0-9]+) ([A-Za-z]+) ([0-9][0-9][0-9][0-9])" body))) |
|
1106
|
|
|
|
|
|
|
(tdate (ymd->tdate (string->number (match:substring m 3)) |
|
1107
|
|
|
|
|
|
|
(Mmm-str->month (match:substring m 2)) |
|
1108
|
|
|
|
|
|
|
(string->number (match:substring m 1)))) |
|
1109
|
|
|
|
|
|
|
(row-list (html-table-rows body (match:end m)))) |
|
1110
|
|
|
|
|
|
|
|
|
1111
|
|
|
|
|
|
|
# blank separator lines |
|
1112
|
|
|
|
|
|
|
(set! row-list (remove! (lambda (row) |
|
1113
|
|
|
|
|
|
|
(every string-null? row)) |
|
1114
|
|
|
|
|
|
|
row-list)) |
|
1115
|
|
|
|
|
|
|
|
|
1116
|
|
|
|
|
|
|
(let ((commodity-list (map daily-heading->commodity+name |
|
1117
|
|
|
|
|
|
|
(first row-list)))) |
|
1118
|
|
|
|
|
|
|
(set! row-list (cdr row-list)) |
|
1119
|
|
|
|
|
|
|
|
|
1120
|
|
|
|
|
|
|
(for-each-two |
|
1121
|
|
|
|
|
|
|
(lambda (buyer-row seller-row) |
|
1122
|
|
|
|
|
|
|
# row like ("" "September Buyer" "" "932" "" "931" "") |
|
1123
|
|
|
|
|
|
|
# ("" "Cash buyer" "" "1,555.00" "" "1,737.00" "" ...) |
|
1124
|
|
|
|
|
|
|
(for-each |
|
1125
|
|
|
|
|
|
|
(lambda (commodity+name buy sell) |
|
1126
|
|
|
|
|
|
|
(if commodity+name |
|
1127
|
|
|
|
|
|
|
(receive-list (commodity name) |
|
1128
|
|
|
|
|
|
|
commodity+name |
|
1129
|
|
|
|
|
|
|
|
|
1130
|
|
|
|
|
|
|
(define symbol (commodity+label->symbol |
|
1131
|
|
|
|
|
|
|
commodity (second buyer-row))) |
|
1132
|
|
|
|
|
|
|
(define (lat sym) |
|
1133
|
|
|
|
|
|
|
(set! ret (cons (list sym name tdate buy sell |
|
1134
|
|
|
|
|
|
|
(chart-symbol-mdate symbol)) |
|
1135
|
|
|
|
|
|
|
ret))) |
|
1136
|
|
|
|
|
|
|
|
|
1137
|
|
|
|
|
|
|
(set! buy (crunch-price buy)) |
|
1138
|
|
|
|
|
|
|
(set! sell (crunch-price sell)) |
|
1139
|
|
|
|
|
|
|
|
|
1140
|
|
|
|
|
|
|
# first row as front month |
|
1141
|
|
|
|
|
|
|
(if (and (eq? buyer-row (first row-list)) |
|
1142
|
|
|
|
|
|
|
(chart-symbol-mdate symbol)) |
|
1143
|
|
|
|
|
|
|
(lat (string-append commodity ".LME"))) |
|
1144
|
|
|
|
|
|
|
|
|
1145
|
|
|
|
|
|
|
# all rows with month in symbol |
|
1146
|
|
|
|
|
|
|
(lat symbol)))) |
|
1147
|
|
|
|
|
|
|
|
|
1148
|
|
|
|
|
|
|
commodity-list buyer-row seller-row)) |
|
1149
|
|
|
|
|
|
|
row-list))) |
|
1150
|
|
|
|
|
|
|
|
|
1151
|
|
|
|
|
|
|
(and-let* ((m (string-match "LMEX Index value [^0-9\n]*([0-9]+ [A-Za-z]+ [0-9][0-9][0-9][0-9])[^0-9.\n]+([0-9.]+)" body))) |
|
1152
|
|
|
|
|
|
|
(set! ret (cons (list "LMEX.LME" |
|
1153
|
|
|
|
|
|
|
#f |
|
1154
|
|
|
|
|
|
|
(d/m/y-str->tdate (match:substring m 1)) |
|
1155
|
|
|
|
|
|
|
#f # no separate buy price |
|
1156
|
|
|
|
|
|
|
(match:substring m 2) |
|
1157
|
|
|
|
|
|
|
#f) |
|
1158
|
|
|
|
|
|
|
ret))) |
|
1159
|
|
|
|
|
|
|
|
|
1160
|
|
|
|
|
|
|
ret) |
|
1161
|
|
|
|
|
|
|
|
|
1162
|
|
|
|
|
|
|
|
|
1163
|
|
|
|
|
|
|
|
|
1164
|
|
|
|
|
|
|
(define (daily-latest-parse body) |
|
1165
|
|
|
|
|
|
|
(list |
|
1166
|
|
|
|
|
|
|
(cons 'form (html-form-parse body)) |
|
1167
|
|
|
|
|
|
|
(cons 'prices (daily-html-parse body)))) |
|
1168
|
|
|
|
|
|
|
|
|
1169
|
|
|
|
|
|
|
(define (daily-latest-info type) |
|
1170
|
|
|
|
|
|
|
(lme-ensure-login) |
|
1171
|
|
|
|
|
|
|
|
|
1172
|
|
|
|
|
|
|
(pagebits-read #:filename (case type |
|
1173
|
|
|
|
|
|
|
((metals) "lme-latest-metals") |
|
1174
|
|
|
|
|
|
|
((plastics) "lme-latest-plastics")) |
|
1175
|
|
|
|
|
|
|
#:status (list (_ "LME") |
|
1176
|
|
|
|
|
|
|
(case type |
|
1177
|
|
|
|
|
|
|
((metals) (_ "metals latest")) |
|
1178
|
|
|
|
|
|
|
((plastics) (_ "plastics latest")))) |
|
1179
|
|
|
|
|
|
|
#:url (list (case type |
|
1180
|
|
|
|
|
|
|
((metals) |
|
1181
|
|
|
|
|
|
|
((plastics) "https://secure.lme.com/Data/community/Dataprices_daily_prices_plastics.aspx")) |
|
1182
|
|
|
|
|
|
|
#:cookiejar lme-cookiejar-filename |
|
1183
|
|
|
|
|
|
|
#:follow #f) |
|
1184
|
|
|
|
|
|
|
#:timezone (timezone-london) |
|
1185
|
|
|
|
|
|
|
#:parse daily-latest-parse)) |
|
1186
|
|
|
|
|
|
|
|
|
1187
|
|
|
|
|
|
|
|
|
1188
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
1189
|
|
|
|
|
|
|
# latest |
|
1190
|
|
|
|
|
|
|
# |
|
1191
|
|
|
|
|
|
|
# This uses the daily prices in the login "free data service", login |
|
1192
|
|
|
|
|
|
|
# required, at |
|
1193
|
|
|
|
|
|
|
# |
|
1194
|
|
|
|
|
|
|
# https://secure.lme.com/Data/community/Dataprices_daily_metals.aspx |
|
1195
|
|
|
|
|
|
|
# |
|
1196
|
|
|
|
|
|
|
# This plain url gives the most recent prices, which we take as a quote for |
|
1197
|
|
|
|
|
|
|
# the indicated day then work back with form-data fetching previous days to |
|
1198
|
|
|
|
|
|
|
# find price change amounts. Or the database is used if it covers the |
|
1199
|
|
|
|
|
|
|
# desired symbol(s). |
|
1200
|
|
|
|
|
|
|
# |
|
1201
|
|
|
|
|
|
|
# Unfortunately there's no Last-Modified or ETag to save refetching if the |
|
1202
|
|
|
|
|
|
|
# latest GET contents have not yet updated. (???) |
|
1203
|
|
|
|
|
|
|
|
|
1204
|
|
|
|
|
|
|
|
|
1205
|
|
|
|
|
|
|
(define (lme-latest-update-database type newest-data prev-data) |
|
1206
|
|
|
|
|
|
|
(let* ((end-tdate (data-tdate newest-data)) |
|
1207
|
|
|
|
|
|
|
(start-tdate (if prev-data |
|
1208
|
|
|
|
|
|
|
(data-tdate prev-data) |
|
1209
|
|
|
|
|
|
|
end-tdate)) |
|
1210
|
|
|
|
|
|
|
(db-list (download-also '() #:selector (lme-type->selector type) |
|
1211
|
|
|
|
|
|
|
#:start-tdate start-tdate |
|
1212
|
|
|
|
|
|
|
#:end-tdate end-tdate))) |
|
1213
|
|
|
|
|
|
|
(if prev-data |
|
1214
|
|
|
|
|
|
|
(daily-process db-list prev-data)) |
|
1215
|
|
|
|
|
|
|
(daily-process db-list newest-data))) |
|
1216
|
|
|
|
|
|
|
|
|
1217
|
|
|
|
|
|
|
(define (lme-latest-process newest-data prev-data proc) |
|
1218
|
|
|
|
|
|
|
(define lst '()) |
|
1219
|
|
|
|
|
|
|
|
|
1220
|
|
|
|
|
|
|
(for-each |
|
1221
|
|
|
|
|
|
|
(lambda (elem) |
|
1222
|
|
|
|
|
|
|
(receive-list (symbol name tdate buy sell mdate) |
|
1223
|
|
|
|
|
|
|
elem |
|
1224
|
|
|
|
|
|
|
|
|
1225
|
|
|
|
|
|
|
(and-let* ((prev-elem (assoc symbol prev-data))) # match car |
|
1226
|
|
|
|
|
|
|
(let ((prev-sell (fifth prev-elem))) |
|
1227
|
|
|
|
|
|
|
|
|
1228
|
|
|
|
|
|
|
(receive-list (decimals buy sell prev-sell) |
|
1229
|
|
|
|
|
|
|
(strings->numbers+decimals buy sell prev-sell) |
|
1230
|
|
|
|
|
|
|
|
|
1231
|
|
|
|
|
|
|
# need both buy and sell to show as quote |
|
1232
|
|
|
|
|
|
|
(define bid buy) |
|
1233
|
|
|
|
|
|
|
(define offer (and buy sell)) |
|
1234
|
|
|
|
|
|
|
(define quote-tdate (and buy sell tdate)) |
|
1235
|
|
|
|
|
|
|
|
|
1236
|
|
|
|
|
|
|
# sell is normally always present, but have seen entire page |
|
1237
|
|
|
|
|
|
|
# blank (empty fields "" which become #f) 31aug05 after |
|
1238
|
|
|
|
|
|
|
# 29aug05 bank holiday |
|
1239
|
|
|
|
|
|
|
|
|
1240
|
|
|
|
|
|
|
(set! lst |
|
1241
|
|
|
|
|
|
|
(cons (latest-new #:symbol symbol |
|
1242
|
|
|
|
|
|
|
#:name name |
|
1243
|
|
|
|
|
|
|
#:quote-tdate quote-tdate |
|
1244
|
|
|
|
|
|
|
#:bid bid |
|
1245
|
|
|
|
|
|
|
#:offer offer |
|
1246
|
|
|
|
|
|
|
#:last-tdate tdate |
|
1247
|
|
|
|
|
|
|
#:last sell |
|
1248
|
|
|
|
|
|
|
#:prev prev-sell |
|
1249
|
|
|
|
|
|
|
#:decimals decimals |
|
1250
|
|
|
|
|
|
|
#:contract-mdate mdate |
|
1251
|
|
|
|
|
|
|
#:source 'lme) |
|
1252
|
|
|
|
|
|
|
lst))))))) |
|
1253
|
|
|
|
|
|
|
newest-data) |
|
1254
|
|
|
|
|
|
|
|
|
1255
|
|
|
|
|
|
|
(proc lst)) |
|
1256
|
|
|
|
|
|
|
|
|
1257
|
|
|
|
|
|
|
(define (lme-latest-type symbol-list type proc) |
|
1258
|
|
|
|
|
|
|
|
|
1259
|
|
|
|
|
|
|
(and-let* ((newest-data (assq-ref (daily-latest-info type) 'prices))) |
|
1260
|
|
|
|
|
|
|
|
|
1261
|
|
|
|
|
|
|
# COVERED-TDATE is the data we already have for all of SYMBOL-LIST (or |
|
1262
|
|
|
|
|
|
|
# rather for the worst among that list), default to a dummy 100 days |
|
1263
|
|
|
|
|
|
|
# ago |
|
1264
|
|
|
|
|
|
|
(let* ((newest-tdate (data-tdate newest-data)) |
|
1265
|
|
|
|
|
|
|
(covered-data (daily-from-database symbol-list)) |
|
1266
|
|
|
|
|
|
|
(covered-tdate (if covered-data |
|
1267
|
|
|
|
|
|
|
(data-tdate covered-data) |
|
1268
|
|
|
|
|
|
|
(- (daily-available-tdate type) 100)))) |
|
1269
|
|
|
|
|
|
|
(let more ((attempt 1)) |
|
1270
|
|
|
|
|
|
|
(if (> attempt 5) |
|
1271
|
|
|
|
|
|
|
(error "LME: can't find previous daily data")) |
|
1272
|
|
|
|
|
|
|
|
|
1273
|
|
|
|
|
|
|
(let ((prev-tdate (- newest-tdate attempt))) |
|
1274
|
|
|
|
|
|
|
(if (>= covered-tdate prev-tdate) |
|
1275
|
|
|
|
|
|
|
(begin |
|
1276
|
|
|
|
|
|
|
(lme-latest-update-database type newest-data #f) |
|
1277
|
|
|
|
|
|
|
(lme-latest-process newest-data covered-data proc)) |
|
1278
|
|
|
|
|
|
|
|
|
1279
|
|
|
|
|
|
|
(let ((prev-data (lme-daily-download-tdate type prev-tdate))) |
|
1280
|
|
|
|
|
|
|
(if prev-data |
|
1281
|
|
|
|
|
|
|
(begin |
|
1282
|
|
|
|
|
|
|
(lme-latest-update-database type newest-data prev-data) |
|
1283
|
|
|
|
|
|
|
(lme-latest-process newest-data prev-data proc)) |
|
1284
|
|
|
|
|
|
|
|
|
1285
|
|
|
|
|
|
|
(more (1+ attempt)))))))))) |
|
1286
|
|
|
|
|
|
|
|
|
1287
|
|
|
|
|
|
|
(define (lme-symbol->type symbol) |
|
1288
|
|
|
|
|
|
|
(if (lme-metal-symbol? symbol) 'metals 'plastics)) |
|
1289
|
|
|
|
|
|
|
|
|
1290
|
|
|
|
|
|
|
(define (lme-latest-get symbol-list extra-list proc) |
|
1291
|
|
|
|
|
|
|
|
|
1292
|
|
|
|
|
|
|
(if (string-null? (preference-get 'lme-username "")) |
|
1293
|
|
|
|
|
|
|
(proc (map (lambda (symbol) |
|
1294
|
|
|
|
|
|
|
(latest-new #:symbol symbol |
|
1295
|
|
|
|
|
|
|
#:note (_ "must register") |
|
1296
|
|
|
|
|
|
|
#:source 'lme)) |
|
1297
|
|
|
|
|
|
|
(append symbol-list extra-list))) |
|
1298
|
|
|
|
|
|
|
|
|
1299
|
|
|
|
|
|
|
# look for one or both metal and plastics in symbol-list, do the two in |
|
1300
|
|
|
|
|
|
|
# the order they appear in SYMBOL-LIST |
|
1301
|
|
|
|
|
|
|
(for-each (lambda (type) |
|
1302
|
|
|
|
|
|
|
(lme-latest-type symbol-list type proc)) |
|
1303
|
|
|
|
|
|
|
(delete-duplicates (map lme-symbol->type symbol-list))))) |
|
1304
|
|
|
|
|
|
|
|
|
1305
|
|
|
|
|
|
|
(define (lme-quote-adate-time symbol) |
|
1306
|
|
|
|
|
|
|
(list (tdate->adate (daily-available-tdate (lme-symbol->type symbol))) #f)) |
|
1307
|
|
|
|
|
|
|
|
|
1308
|
|
|
|
|
|
|
(latest-handler! #:selector lme-symbol? |
|
1309
|
|
|
|
|
|
|
#:handler lme-latest-get |
|
1310
|
|
|
|
|
|
|
#:adate-time lme-quote-adate-time) |
|
1311
|
|
|
|
|
|
|
|
|
1312
|
|
|
|
|
|
|
|
|
1313
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
1314
|
|
|
|
|
|
|
# download - historical prices and/or volumes |
|
1315
|
|
|
|
|
|
|
|
|
1316
|
|
|
|
|
|
|
# return tdate of last volume value recorded for SYMBOL, or #f if none ever |
|
1317
|
|
|
|
|
|
|
(define (database-last-volume symbol) |
|
1318
|
|
|
|
|
|
|
(and-let* ((series (database-read-series symbol))) |
|
1319
|
|
|
|
|
|
|
(series-array series # initial request past month to look at |
|
1320
|
|
|
|
|
|
|
(- (series-hi series) 25) |
|
1321
|
|
|
|
|
|
|
(series-hi series)) |
|
1322
|
|
|
|
|
|
|
(let more ((i (series-hi series))) |
|
1323
|
|
|
|
|
|
|
(and (>= i (series-lo series)) |
|
1324
|
|
|
|
|
|
|
(if (array-ref (series-array series i i) i 4) |
|
1325
|
|
|
|
|
|
|
i |
|
1326
|
|
|
|
|
|
|
(more (1- i))))))) |
|
1327
|
|
|
|
|
|
|
|
|
1328
|
|
|
|
|
|
|
|
|
1329
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
|
1330
|
|
|
|
|
|
|
# download |
|
1331
|
|
|
|
|
|
|
|
|
1332
|
|
|
|
|
|
|
(define (lme-download-available-tdate) |
|
1333
|
|
|
|
|
|
|
(daily-available-tdate 'metals) |
|
1334
|
|
|
|
|
|
|
(monthxls-available-tdate)) |
|
1335
|
|
|
|
|
|
|
|
|
1336
|
|
|
|
|
|
|
(download-now-handler! (lambda (symbol-list) |
|
1337
|
|
|
|
|
|
|
(and (any lme-symbol? symbol-list) |
|
1338
|
|
|
|
|
|
|
(download-now-all-commodities-and-months)))) |