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
|
|
499
|
use 5.010; |
|
1
|
|
|
|
|
3
|
|
21
|
1
|
|
|
1
|
|
4
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
16
|
|
22
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
24
|
|
23
|
1
|
|
|
1
|
|
4
|
use Carp; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
46
|
|
24
|
1
|
|
|
1
|
|
227
|
use Date::Calc; |
|
1
|
|
|
|
|
4329
|
|
|
1
|
|
|
|
|
35
|
|
25
|
1
|
|
|
1
|
|
260
|
use Date::Parse; |
|
1
|
|
|
|
|
5267
|
|
|
1
|
|
|
|
|
116
|
|
26
|
1
|
|
|
1
|
|
495
|
use File::Temp; |
|
1
|
|
|
|
|
11743
|
|
|
1
|
|
|
|
|
67
|
|
27
|
1
|
|
|
1
|
|
7
|
use File::Basename; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
46
|
|
28
|
1
|
|
|
1
|
|
349
|
use HTML::Form; |
|
1
|
|
|
|
|
19389
|
|
|
1
|
|
|
|
|
35
|
|
29
|
1
|
|
|
1
|
|
10
|
use List::Util; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
57
|
|
30
|
1
|
|
|
1
|
|
404
|
use File::Slurp; |
|
1
|
|
|
|
|
4447
|
|
|
1
|
|
|
|
|
58
|
|
31
|
1
|
|
|
1
|
|
8
|
use URI::Escape; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
47
|
|
32
|
1
|
|
|
1
|
|
356
|
use Locale::TextDomain ('App-Chart'); |
|
1
|
|
|
|
|
5557
|
|
|
1
|
|
|
|
|
5
|
|
33
|
|
|
|
|
|
|
|
34
|
1
|
|
|
1
|
|
5564
|
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)))) |