File Coverage

blib/lib/App/Chart/Suffix/FQ.pm
Criterion Covered Total %
statement 21 23 91.3
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 29 31 93.5


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2015 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17             package App::Chart::Suffix::FQ;
18 1     1   393 use 5.010;
  1         2  
19 1     1   5 use strict;
  1         2  
  1         16  
20 1     1   4 use warnings;
  1         2  
  1         25  
21 1     1   4 use Carp 'carp','croak';
  1         2  
  1         85  
22 1     1   6 use List::Util qw(min max);
  1         2  
  1         73  
23 1     1   328 use List::MoreUtils;
  1         8980  
  1         6  
24 1     1   1108 use Locale::TextDomain 'App-Chart';
  1         17524  
  1         6  
25              
26 1     1   6000 use App::Chart::Download;
  0            
  0            
27             use App::Chart::LatestHandler;
28             use App::Chart::Sympred;
29             use App::Chart::Weblink;
30              
31             use constant DEBUG => 0;
32              
33             my $pred = App::Chart::Sympred::Suffix->new ('.FQ');
34              
35              
36             #-----------------------------------------------------------------------------
37             # specifics
38              
39             { my $tsp_pred = App::Chart::Sympred::Suffix->new ('.tsp.FQ');
40             App::Chart::TZ->newyork->setup_for_symbol ($tsp_pred);
41              
42             # only home page, per their "Linkage to the TSP Web Site"
43             App::Chart::Weblink->new
44             (pred => $tsp_pred,
45             name => __('_TSP Home Page'),
46             desc => __('Open web browser at the US Government Thrift Savings Plan'),
47             proc => sub {
48             eval { require Finance::Quote::TSP }
49             && $Finance::Quote::TSP::TSP_MAIN_URL;
50             });
51             }
52              
53             { my $usfed_pred = App::Chart::Sympred::Suffix->new ('.usfedbonds.FQ');
54             App::Chart::Weblink->new
55             (pred => $usfed_pred,
56             name => __('_Fed Bonds Home Page'),
57             desc => __('Open web browser at the US Treasury Federal Bonds site'),
58             # $TREASURY_MAINURL is private
59             url => 'http://www.publicdebt.treas.gov/');
60             }
61              
62             #-----------------------------------------------------------------------------
63              
64             App::Chart::LatestHandler->new
65             (pred => $pred,
66             proc => \&latest_download);
67              
68             my $FQ_re = qr/\.([^.]+)\.FQ$/p;
69              
70             sub fq_symbol_method {
71             my ($symbol) = @_;
72             $symbol =~ $FQ_re or croak "Not an FQ symbol '$symbol'";
73             return $1;
74             }
75             sub fq_symbol_sans_suffix {
76             my ($symbol) = @_;
77             $symbol =~ $FQ_re or croak "Not an FQ symbol '$symbol'";
78             return ${^PREMATCH};
79             }
80              
81             sub latest_download {
82             my ($symbol_list) = @_;
83              
84             # split by method, preserving order among methods
85             # ENHANCE-ME: could preserve order for "separate request" sources, just
86             # join up the multiple request ones
87             require Tie::IxHash;
88             my %sm;
89             tie %sm, 'Tie::IxHash';
90             foreach my $symbol (@$symbol_list) {
91             $symbol =~ $FQ_re or croak "Not an FQ symbol: $symbol";
92             $symbol = ${^PREMATCH};
93             my $method = $1;
94             push @{$sm{$method}}, $symbol;
95             }
96              
97             foreach my $method (keys %sm) {
98             my $symbol_list = $sm{$method};
99             if (DEBUG) { require Data::Dumper;
100             print "method $method ",
101             Data::Dumper->Dumper([$symbol_list],['symbol_list']); }
102              
103             App::Chart::Download::status
104             (__x('Finance::Quote {method} {symbol_range}',
105             method => $method,
106             symbol_range => App::Chart::Download::symbol_range_string ($symbol_list)));
107              
108             my $q = quoter_for_method ($method);
109             my $quotes = $q->fetch ($method, @$symbol_list);
110             my $h = quotes_to_group (".$method.FQ", $symbol_list, $quotes);
111              
112             App::Chart::Download::write_latest_group ($h);
113             }
114             }
115              
116             # Return a Finance::Quote->new object, hopefully able to fetch $method.
117             #
118             # If FQ_LOAD_QUOTELET and/or the defaults don't offer $method then it's
119             # attempted with method_to_modules() below added.
120             #
121             # It'd be possible to try method_to_modules() first, and that would normally
122             # load much less code than the defaults, but it might also miss something in
123             # the defaults, or grab something no wanted, so start with the defaults and
124             # only then search further.
125             #
126             sub quoter_for_method {
127             my ($method) = @_;
128             require Finance::Quote;
129             my $q = Finance::Quote->new;
130             if (! List::Util::first {$_ eq $method} $q->sources) {
131             my @modules = method_to_modules ($method);
132             if (DEBUG) { require Data::Dumper;
133             print "FQ attempt method='$method' in ",
134             Data::Dumper->new([\@modules],['modules'])->Dump; }
135             if (@modules) {
136             ## no critic (RequireCheckingReturnValueOfEval)
137             eval { Finance::Quote->new (@modules) };
138             $q = Finance::Quote->new;
139             }
140             }
141             return $q;
142             }
143              
144             # Return a list of modules which seem likely candidates for $method.
145             #
146             # The return is ready to pass to Finance::Quote->new, so it doesn't have a
147             # "Finance::Quote::" prefix, so for instance $method "tsp" might give just
148             # "TSP".
149             #
150             # This is meant to automatically pickup modules not in the defaults or in
151             # FQ_LOAD_QUOTELET. It can't cope with fallbacks offering extra sources for
152             # a given country etc, but it's much easier than adding to the env var
153             # whenever you install a new add-on.
154             #
155             sub method_to_modules {
156             my ($method) = @_;
157              
158             # 'ftportfolios_direct' -> FTPortfolios.pm
159             # 'seb_funds' -> SEB.pm
160             # 'unionfunds' -> Union.pm
161             # 'aex_options' -> AEX.pm
162             # 'aex_futures' -> AEX.pm
163             # 'stockhousecanada_fund' -> StockHouseCanada.pm
164             $method =~ s/(_direct|_futures|_options|_?funds?)$//;
165              
166             require Module::Find;
167             my @modules = Module::Find::findsubmod ('Finance::Quote');
168             foreach (@modules) { s/^Finance::Quote::// }
169             return grep /^\Q$method/i, @modules;
170             }
171              
172             sub quotes_to_group {
173             my ($symbol_suffix, $symbol_list, $quotes) = @_;
174             if (DEBUG) { require Data::Dumper;
175             print Data::Dumper::Dumper($quotes); }
176              
177             my @data = ();
178             my $h = { source => __PACKAGE__,
179             data => \@data };
180             foreach my $symbol (@$symbol_list) {
181              
182             my $last = $quotes->{$symbol,'last'};
183             my $high = $quotes->{$symbol,'high'};
184             my $low = $quotes->{$symbol,'low'};
185             my $change = $quotes->{$symbol,'net'};
186              
187             # Finance::Quote::Fidelity version 1.05 gives 'price' rather than 'last'
188             if (! defined $last) {
189             my $price = $quotes->{$symbol,'price'};
190             if (defined $price) {
191             $last = $price;
192             }
193             }
194              
195             # Try making $change from 'close' (the previous close) and 'last'.
196             #
197             if (defined $last && ! defined $change) {
198             my $prev = $quotes->{$symbol,'close'};
199             if (defined $prev) {
200             $change = decimal_subtract ($last, $prev);
201             }
202             }
203              
204             # Try making $change from 'p_change' and 'last'.
205             #
206             # $prev * (100 + $p_change) / 100 == $last
207             # $prev == $last * 100 / (100 + $p_change)
208             # $change == $last - $prev
209             # == $last - $last * 100 / (100 + $p_change)
210             # == $last * (1 - 100 / (100 + $p_change))
211             # == $last * $p_change / (100 + $p_change)
212             #
213             if (defined $last && ! defined $change) {
214             my $p_change = $quotes->{$symbol,'p_change'};
215             if (defined $p_change) {
216             $change = $last * $p_change / (100 + $p_change);
217             $change = sprintf ('%.*f', App::Chart::count_decimals($last), $change);
218             }
219             }
220              
221             # Separate high/low in for instance Finance::Quote::ZA
222             # But Yahoo methods give just 'day_range'.
223             #
224             if (! defined $high) {
225             my $day_range = $quotes->{$symbol,'day_range'};
226             if (defined $day_range) {
227             if ($day_range =~ /^([0-9.]+)-([0-9.]+)$/) {
228             $high = $1;
229             $low = $2;
230             } else {
231             carp "Unrecognised $symbol 'day_range': $day_range";
232             }
233             }
234             }
235              
236             # Not needed any more
237             # my $volume = $quotes->{$symbol,'volume'};
238             # # Try approximating $volume from 'dollar_volume' and 'last'.
239             # # dollar_volume given by Finance::Quote::Casablanca
240             # #
241             # if ($last && ! defined $volume) {
242             # my $dollar_volume = $quotes->{$symbol,'dollar_volume'};
243             # if (defined $dollar_volume) {
244             # $volume = int ($dollar_volume / $last + 0.5); # round to nearest
245             # }
246             # }
247              
248             my $errormsg;
249             if (! $quotes->{$symbol,'success'}) {
250             $errormsg = $quotes->{$symbol,'errormsg'};
251             if (! defined $errormsg) {
252             if (List::MoreUtils::any {/^$symbol$;/} keys %$quotes) {
253             $errormsg = __('Unknown error');
254             } else {
255             $errormsg = __('No data from FQ method');
256             }
257             }
258             }
259              
260             # If 'ex_div' is yahoo style then it needs munging ...
261             # $quotes->{$symbol,'div'},
262             # $quotes->{$symbol,'ex_div'},
263              
264             push @data, { symbol => $symbol . $symbol_suffix,
265             name => $quotes->{$symbol,'name'},
266              
267             quote_date => $quotes->{$symbol,'isodate'},
268             quote_time => $quotes->{$symbol,'time'},
269             bid => $quotes->{$symbol,'bid'},
270             offer => $quotes->{$symbol,'ask'},
271              
272             last_date => $quotes->{$symbol,'isodate'},
273             last_time => $quotes->{$symbol,'time'},
274             open => $quotes->{$symbol,'open'},
275             high => $high,
276             low => $low,
277             last => $last,
278             change => $change,
279              
280             volume => $quotes->{$symbol,'volume'},
281             currency => $quotes->{$symbol,'currency'},
282             error => $errormsg,
283              
284             # various of mine
285             copyright => $quotes->{$symbol,'copyright_url'},
286             };
287             }
288             if (DEBUG) { require Data::Dumper;
289             print Data::Dumper::Dumper($h); }
290             return $h;
291             }
292              
293             # Return the difference $x - $y, done as a "decimal" subtract, so retaining
294             # as many decimal places there are on $x and $y.
295             # It's done with some sprint %f fakery, not actual decimal arithmetic, but
296             # that's close enough for 4 decimal place currencies.
297             sub decimal_subtract {
298             my ($x, $y) = @_;
299             my $decimals = max (App::Chart::count_decimals($x),
300             App::Chart::count_decimals($y));
301             return sprintf ('%.*f', $decimals, $x - $y);
302             }
303              
304             1;
305             __END__