File Coverage

blib/lib/Finance/QuoteHist/Yahoo.pm
Criterion Covered Total %
statement 18 128 14.0
branch 0 36 0.0
condition 0 51 0.0
subroutine 6 15 40.0
pod 2 4 50.0
total 26 234 11.1


line stmt bran cond sub pod time code
1             package Finance::QuoteHist::Yahoo;
2              
3 1     1   9688 use strict;
  1         2  
  1         32  
4 1     1   5 use vars qw(@ISA $VERSION);
  1         2  
  1         43  
5 1     1   6 use Carp;
  1         2  
  1         55  
6              
7             $VERSION = "1.26";
8              
9 1     1   6 use Finance::QuoteHist::Generic;
  1         2  
  1         27  
10             @ISA = qw(Finance::QuoteHist::Generic);
11              
12 1     1   5 use Date::Manip;
  1         2  
  1         234  
13 1     1   715 use JSON;
  1         10059  
  1         5  
14              
15             # curl 'https://query2.finance.yahoo.com/v8/finance/chart/TLSA?formatted=true&crumb=l92p7dftYe%2F&lang=en-US®ion=US&includeAdjustedClose=true&interval=1d&period1=1455840000&period2=1613692800&events=div%7Csplit&useYfid=true&corsDomain=finance.yahoo.com' \
16             # -H 'authority: query2.finance.yahoo.com' \
17             # -H 'pragma: no-cache' \
18             # -H 'cache-control: no-cache' \
19             # -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36' \
20             # -H 'dnt: 1' \
21             # -H 'accept: */*' \
22             # -H 'origin: https://finance.yahoo.com' \
23             # -H 'sec-fetch-site: same-site' \
24             # -H 'sec-fetch-mode: cors' \
25             # -H 'sec-fetch-dest: empty' \
26             # -H 'referer: https://finance.yahoo.com/quote/TLSA/history?period1=1455840000&period2=1613692800&interval=1d&filter=history&frequency=1d&includeAdjustedClose=true' \
27             # -H 'accept-language: en-US,en;q=0.9' \
28             # -H 'cookie: B=a912p61g2vk8r&b=3&s=tt; GUC=AQEBAQFgMSJgOUIaCwP5; A1=d=AQABBBvRL2ACEAlRbREoQmTRqOBpeDBZhKQFEgEBAQEiMWA5YAAAAAAA_SMAAAcIG9EvYDBZhKQ&S=AQAAApHY37COo3qJCJvBwk9D-TA; A3=d=AQABBBvRL2ACEAlRbREoQmTRqOBpeDBZhKQFEgEBAQEiMWA5YAAAAAAA_SMAAAcIG9EvYDBZhKQ&S=AQAAApHY37COo3qJCJvBwk9D-TA; A1S=d=AQABBBvRL2ACEAlRbREoQmTRqOBpeDBZhKQFEgEBAQEiMWA5YAAAAAAA_SMAAAcIG9EvYDBZhKQ&S=AQAAApHY37COo3qJCJvBwk9D-TA&j=US; cmp=t=1613746462&j=0; PRF=t%3DTLSA%252BIBM%252B%255EYHQ' \
29             # -H 'sec-gpc: 1' \
30             # --compressed
31              
32              
33             # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=1495391410&period2=1498069810&interval=1d&events=history&crumb=bB6k340lPXt
34             # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1wk&events=history&crumb=bB6k340lPXt
35             # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1mo&events=history&crumb=bB6k340lPXt
36             #
37             # Dividends:
38             # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1d&events=div&crumb=bB6k340lPXt
39             #
40             # Splits:
41             # https://query1.finance.yahoo.com/v7/finance/download/NKE?period1=993096000&period2=1498017600&interval=1d&events=split&crumb=bB6k340lPXt
42              
43             sub new {
44 0     0 1   my $that = shift;
45 0   0       my $class = ref($that) || $that;
46 0           my %parms = @_;
47              
48 0           $parms{parse_mode} = 'json';
49 0   0       $parms{ua_params} ||= {};
50 0   0       $parms{ua_params}{cookie_jar} ||= {};
51              
52 0           my $self = __PACKAGE__->SUPER::new(%parms);
53 0           bless $self, $class;
54              
55             # set initial cookie (the cookie crumbs are hashed out of this)
56             # https://finance.yahoo.com/quote/IBM/history
57 0           my $ticker = $parms{symbols};
58 0 0         $ticker = $ticker->[0] if ref $ticker eq 'ARRAY';
59 0           my $html = $self->fetch("https://finance.yahoo.com/quote/$ticker/history");
60              
61             # extract the cookie crumb
62 0           my %crumbs;
63 0           for my $c ($html =~ /"crumb"\s*:\s*"([^"]+)"/g) {
64 0 0         next if $c =~ /[{}]/;
65 0           $c =~ s/\\u002F/\//;
66 0           ++$crumbs{$c};
67             }
68 0           my $crumb = '';
69 0           my $max = 0;
70 0           for my $c (keys %crumbs) {
71 0 0         if ($crumbs{$c} >= $max) {
72 0           $crumb = $c;
73 0           $max = $crumbs{$c};
74             }
75             }
76              
77 0           $self->{crumb} = $crumb;
78              
79 0           $self;
80             }
81              
82 0     0 0   sub granularities { qw( daily weekly monthly ) }
83              
84             sub url_maker {
85 0     0 1   my($self, %parms) = @_;
86 0   0       my $target_mode = $parms{target_mode} || $self->target_mode;
87 0   0       my $parse_mode = $parms{parse_mode} || $self->parse_mode;
88              
89             # *always* block unknown target mode and parse mode combinations for
90             # cascade to work properly!
91 0 0 0       return undef unless $target_mode eq 'quote' ||
      0        
92             $target_mode eq 'split' ||
93             $target_mode eq 'dividend';
94              
95 0           $parse_mode = "json";
96              
97 0   0       my $granularity = lc($parms{granularity} || $self->granularity);
98 0           my $grain = 'd';
99 0           $granularity =~ /^\s*(\w)/;
100 0 0 0       $grain = $1 if $1 eq 'w' || $1 eq 'm';
101             my($ticker, $start_date, $end_date) =
102 0           @parms{qw(symbol start_date end_date)};
103 0   0       $start_date ||= $self->start_date;
104 0   0       $end_date ||= $self->end_date;
105 0 0 0       if ($start_date && $end_date && $start_date gt $end_date) {
      0        
106 0           ($start_date, $end_date) = ($end_date, $start_date);
107             }
108              
109             #my $host = "query1.finance.yahoo.com";
110             #my $base_url = "https://$host/v7/finance/download/$ticker?";
111            
112             # https://query2.finance.yahoo.com/v8/finance/chart/TLSA?
113             # formatted=true
114             # crumb=l92p7dftYe%2F
115             # lang=en-US
116             # region=US
117             # includeAdjustedClose=true
118             # interval=1d
119             # period1=1455840000
120             # period2=1613692800
121             # events=div%7Csplit
122             # useYfid=true
123             # corsDomain=finance.yahoo.com
124              
125 0           my $host = "query2.finance.yahoo.com";
126 0           my $base_url = "https://$host/v8/finance/chart/$ticker?";
127 0           my @base_parms;
128 0 0         if ($start_date) {
129 0           my($y, $m, $d) = $self->ymd($start_date);
130 0           my $ts = Date_SecsSince1970($m, $d, $y, 0, 0, 0);
131 0           push(@base_parms, "period1=$ts");
132             }
133 0 0         if ($end_date) {
134 0           my($y, $m, $d) = $self->ymd($end_date);
135 0           my $ts = Date_SecsSince1970($m, $d, $y, 0, 0, 0);
136 0           $ts += 24*60*60;
137 0           push(@base_parms, "period2=$ts");
138             }
139              
140 0           my $interval = "1d";
141 0 0         if ($grain eq 'w') {
    0          
142 0           $interval = "1wk";
143             }
144             elsif ($grain eq 'm') {
145 0           $interval = "1mo";
146             }
147 0           push(@base_parms, "interval=$interval");
148              
149 0 0         if ($target_mode eq "quote") {
    0          
    0          
150 0           push(@base_parms, "events=history");
151 0           push(@base_parms, "includeAdjustedClose=true")
152             }
153             elsif ($target_mode eq "dividend") {
154 0           push(@base_parms, "events=div");
155             }
156             elsif ($target_mode eq "split") {
157 0           push(@base_parms, "events=split");
158             }
159              
160 0           push(@base_parms, "crumb=" . $self->{crumb});
161              
162 0           my @urls = $base_url . join('&', @base_parms);
163 0     0     return sub { pop @urls };
  0            
164             }
165              
166             sub json_parser {
167 0     0 0   my $self = shift;
168 0           my $target_mode = $self->target_mode();
169             my $json_quote_parse = sub {
170 0     0     my $data_result = shift;
171 0   0       my $data_indicators = $data_result->{indicators} || {};
172 0   0       my $data_quote = ($data_indicators->{quote} || [])->[0];
173 0           my @rows = [];
174 0 0         if ($data_quote) {
175 0           my $data_high = $data_quote->{high};
176 0           my $data_close = $data_quote->{close};
177 0           my $data_open = $data_quote->{open};
178 0           my $data_low = $data_quote->{low};
179 0           my $data_volume = $data_quote->{volume};
180 0           my $data_adj_close = $data_indicators->{adjclose}[0]{adjclose};
181 0           my $data_timestamp = $data_result->{timestamp};
182 0           for my $i (0 .. $#{$data_timestamp}) {
  0            
183 0           push(@rows, [
184             $data_timestamp->[$i],
185             $data_open->[$i],
186             $data_high->[$i],
187             $data_low->[$i],
188             $data_close->[$i],
189             $data_volume->[$i],
190             ]);
191             }
192             }
193 0           \@rows;
194 0           };
195             my $json_split_parse = sub {
196 0     0     my $data_result = shift;
197 0   0       my $data_events = $data_result->{events} || {};
198 0   0       my $data_splits = $data_events->{splits} || {};
199 0           my @rows;
200 0 0         if ($data_splits) {
201 0           for my $rec (sort values %$data_splits) {
202             push(@rows, [
203             $rec->{date},
204             $rec->{numerator},
205             $rec->{denominator},
206 0           ]);
207             }
208             }
209 0           \@rows;
210 0           };
211             my $json_div_parse = sub {
212             # "date" "amount"
213 0     0     my $data_result = shift;
214 0   0       my $data_events = $data_result->{events} || {};
215 0   0       my $data_dividends = $data_events->{dividends} || {};
216 0           my @rows;
217 0           for my $rec (sort values %$data_dividends) {
218             push(@rows, [
219             $rec->{date},
220             $rec->{amount},
221 0           ]);
222             }
223 0           \@rows;
224 0           };
225             sub {
226 0     0     my $data = shift;
227 0           $data = decode_json($data);
228 0   0       my $data_result = $data->{chart}{result}[0] || {};
229 0 0         if ($target_mode eq "quote") {
    0          
    0          
230 0           return $json_quote_parse->($data_result);
231             }
232             elsif ($target_mode eq "split") {
233 0           return $json_split_parse->($data_result);
234             }
235             elsif ($target_mode eq "dividend") {
236 0           return $json_div_parse->($data_result);
237             }
238 0           else { die "unknown mode: $target_mode" }
239 0           };
240             }
241              
242             1;
243              
244             __END__